diff --git a/DiscordChatExporter.Core/Discord/DiscordClient.cs b/DiscordChatExporter.Core/Discord/DiscordClient.cs index 9e94369..169261e 100644 --- a/DiscordChatExporter.Core/Discord/DiscordClient.cs +++ b/DiscordChatExporter.Core/Discord/DiscordClient.cs @@ -33,7 +33,7 @@ public class DiscordClient CancellationToken cancellationToken = default ) { - return await Http.ResponseResiliencePolicy.ExecuteAsync( + return await Http.ResponseResiliencePipeline.ExecuteAsync( async innerCancellationToken => { using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_baseUri, url)); diff --git a/DiscordChatExporter.Core/Exporting/ExportAssetDownloader.cs b/DiscordChatExporter.Core/Exporting/ExportAssetDownloader.cs index bc3173b..2be9efd 100644 --- a/DiscordChatExporter.Core/Exporting/ExportAssetDownloader.cs +++ b/DiscordChatExporter.Core/Exporting/ExportAssetDownloader.cs @@ -53,44 +53,47 @@ internal partial class ExportAssetDownloader Directory.CreateDirectory(_workingDirPath); - await Http.ResiliencePolicy.ExecuteAsync(async () => - { - // Download the file - using var response = await Http.Client.GetAsync(url, cancellationToken); - await using (var output = File.Create(filePath)) - await response.Content.CopyToAsync(output, cancellationToken); - - // Try to set the file date according to the last-modified header - try + await Http.ResiliencePipeline.ExecuteAsync( + async innerCancellationToken => { - var lastModified = response.Content.Headers - .TryGetValue("Last-Modified") - ?.Pipe( - s => - DateTimeOffset.TryParse( - s, - CultureInfo.InvariantCulture, - DateTimeStyles.None, - out var instant - ) - ? instant - : (DateTimeOffset?)null - ); - - if (lastModified is not null) + // Download the file + using var response = await Http.Client.GetAsync(url, innerCancellationToken); + await using (var output = File.Create(filePath)) + await response.Content.CopyToAsync(output, innerCancellationToken); + + // Try to set the file date according to the last-modified header + try { - File.SetCreationTimeUtc(filePath, lastModified.Value.UtcDateTime); - File.SetLastWriteTimeUtc(filePath, lastModified.Value.UtcDateTime); - File.SetLastAccessTimeUtc(filePath, lastModified.Value.UtcDateTime); + var lastModified = response.Content.Headers + .TryGetValue("Last-Modified") + ?.Pipe( + s => + DateTimeOffset.TryParse( + s, + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out var instant + ) + ? instant + : (DateTimeOffset?)null + ); + + if (lastModified is not null) + { + File.SetCreationTimeUtc(filePath, lastModified.Value.UtcDateTime); + File.SetLastWriteTimeUtc(filePath, lastModified.Value.UtcDateTime); + File.SetLastAccessTimeUtc(filePath, lastModified.Value.UtcDateTime); + } } - } - catch - { - // This can apparently fail for some reason. - // Updating the file date is not a critical task, so we'll just ignore exceptions thrown here. - // https://github.com/Tyrrrz/DiscordChatExporter/issues/585 - } - }); + catch + { + // This can apparently fail for some reason. + // Updating the file date is not a critical task, so we'll just ignore exceptions thrown here. + // https://github.com/Tyrrrz/DiscordChatExporter/issues/585 + } + }, + cancellationToken + ); return _previousPathsByUrl[url] = filePath; } diff --git a/DiscordChatExporter.Core/Utils/Http.cs b/DiscordChatExporter.Core/Utils/Http.cs index 041deae..f08984f 100644 --- a/DiscordChatExporter.Core/Utils/Http.cs +++ b/DiscordChatExporter.Core/Utils/Http.cs @@ -7,6 +7,7 @@ using System.Security.Authentication; using System.Threading.Tasks; using DiscordChatExporter.Core.Utils.Extensions; using Polly; +using Polly.Retry; namespace DiscordChatExporter.Core.Utils; @@ -31,29 +32,45 @@ public static class Http && IsRetryableStatusCode(hrex.StatusCode ?? HttpStatusCode.OK) ); - public static IAsyncPolicy ResiliencePolicy { get; } = - Policy - .Handle(IsRetryableException) - .WaitAndRetryAsync(4, i => TimeSpan.FromSeconds(Math.Pow(2, i) + 1)); + public static ResiliencePipeline ResiliencePipeline { get; } = + new ResiliencePipelineBuilder() + .AddRetry( + new RetryStrategyOptions + { + ShouldHandle = new PredicateBuilder().Handle(IsRetryableException), + MaxRetryAttempts = 4, + BackoffType = DelayBackoffType.Exponential, + Delay = TimeSpan.FromSeconds(1) + } + ) + .Build(); - public static IAsyncPolicy ResponseResiliencePolicy { get; } = - Policy - .Handle(IsRetryableException) - .OrResult(m => IsRetryableStatusCode(m.StatusCode)) - .WaitAndRetryAsync( - 8, - (i, result, _) => + public static ResiliencePipeline ResponseResiliencePipeline { get; } = + new ResiliencePipelineBuilder() + .AddRetry( + new RetryStrategyOptions { - // If rate-limited, use retry-after header as the guide. - // The response can be null here if an exception was thrown. - if (result.Result?.Headers.RetryAfter?.Delta is { } retryAfter) + ShouldHandle = new PredicateBuilder() + .Handle(IsRetryableException) + .HandleResult(m => IsRetryableStatusCode(m.StatusCode)), + MaxRetryAttempts = 8, + DelayGenerator = args => { - // Add some buffer just in case - return retryAfter + TimeSpan.FromSeconds(1); - } + // If rate-limited, use retry-after header as the guide. + // The response can be null here if an exception was thrown. + if (args.Outcome.Result?.Headers.RetryAfter?.Delta is { } retryAfter) + { + // Add some buffer just in case + return ValueTask.FromResult( + retryAfter + TimeSpan.FromSeconds(1) + ); + } - return TimeSpan.FromSeconds(Math.Pow(2, i) + 1); - }, - (_, _, _, _) => Task.CompletedTask - ); + return ValueTask.FromResult( + TimeSpan.FromSeconds(Math.Pow(2, args.AttemptNumber) + 1) + ); + } + } + ) + .Build(); }