/* * Copyright (c) 2023 Proton AG * * This file is part of ProtonVPN. * * ProtonVPN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ProtonVPN is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ProtonVPN. If not, see . */ using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; using Polly; using Polly.Retry; using ProtonVPN.Api.Extensions; using ProtonVPN.Common.Core.Extensions; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.ApiLogs; namespace ProtonVPN.Api.Handlers.Retries { public class RetryPolicyProvider : IRetryPolicyProvider { private readonly ILogger _logger; private readonly SleepDurationProvider _sleepDurationProvider; private readonly IRetryCountProvider _retryCountProvider; private readonly IRequestTimeoutProvider _requestTimeoutProvider; public RetryPolicyProvider( ILogger logger, SleepDurationProvider sleepDurationProvider, IRetryCountProvider retryCountProvider, IRequestTimeoutProvider requestTimeoutProvider) { _logger = logger; _sleepDurationProvider = sleepDurationProvider; _retryCountProvider = retryCountProvider; _requestTimeoutProvider = requestTimeoutProvider; } public AsyncPolicy GetRetryPolicy(HttpRequestMessage request) { return GetResponseMessageRetryPolicy(request) .WrapAsync(GetResponseMessageRetryOncePolicy(request)) .WrapAsync(GetExceptionRetryPolicy(request)) .WrapAsync(GetTimeoutPolicy(request)); } private AsyncPolicy GetTimeoutPolicy(HttpRequestMessage request) { return Policy.TimeoutAsync(_requestTimeoutProvider.GetTimeout(request)); } private AsyncPolicy GetResponseMessageRetryOncePolicy(HttpRequestMessage request) { return Policy .HandleResult(response => response.IsToRetryOnce()) .WaitAndRetryAsync(1, _ => TimeSpan.Zero); } private Func, TimeSpan, int, Context, Task> OnRetryAsync(HttpRequestMessage request) { Task RetryAsyncFunction(DelegateResult response, TimeSpan timeSpan, int retryCount, Context _) { _logger.Info(GetRetryLogMessage(request, timeSpan, retryCount, GetReasonByHttpStatusCode(response.Result.StatusCode))); return Task.CompletedTask; } return RetryAsyncFunction; } private AsyncPolicy GetResponseMessageRetryPolicy(HttpRequestMessage request) { return Policy .HandleResult(response => response.IsToRetry() && !response.IsToRetryOnce()) .WaitAndRetryAsync(_retryCountProvider.GetRetryCount(request), _sleepDurationProvider.ResponseMessageDurationFunction, OnRetryAsync(request)); } private string GetRetryLogMessage(HttpRequestMessage request, TimeSpan timeSpan, int retryCount, string reason) { return $"Waiting for {timeSpan} before retrying {request.Method} {request.RequestUri} due to {reason}. " + $"Retry count: {retryCount}."; } private AsyncRetryPolicy GetExceptionRetryPolicy(HttpRequestMessage request) { return Policy .Handle() .WaitAndRetryAsync(_retryCountProvider.GetRetryCount(request), _sleepDurationProvider.RequestExceptionDurationFunction, (exception, timeSpan, retryCount, _) => { _logger.Info(GetRetryLogMessage(request, timeSpan, retryCount, GetReasonByException(exception))); }); } private string GetReasonByHttpStatusCode(HttpStatusCode statusCode) { return $"{statusCode} status code"; } private string GetReasonByException(Exception e) { return $"exception {e.CombinedMessage()}"; } } }