Rework retry policy

Fixes #832
This commit is contained in:
Oleksii Holub 2022-04-12 23:55:03 +03:00
parent f7f6ac9494
commit bb81cf06ae
4 changed files with 96 additions and 64 deletions

View file

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
@ -52,7 +51,7 @@ public class DiscordClient
string url, string url,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
return await Http.ResponsePolicy.ExecuteAsync(async innerCancellationToken => return await Http.ResponseResiliencePolicy.ExecuteAsync(async innerCancellationToken =>
{ {
if (_tokenKind == TokenKind.User) if (_tokenKind == TokenKind.User)
return await GetResponseAsync(url, false, innerCancellationToken); return await GetResponseAsync(url, false, innerCancellationToken);

View file

@ -35,20 +35,16 @@ internal partial class MediaDownloader
var filePath = Path.Combine(_workingDirPath, fileName); var filePath = Path.Combine(_workingDirPath, fileName);
// Reuse existing files if we're allowed to // Reuse existing files if we're allowed to
if (_reuseMedia && File.Exists(filePath)) if (!_reuseMedia || !File.Exists(filePath))
return _pathCache[url] = filePath; {
Directory.CreateDirectory(_workingDirPath); Directory.CreateDirectory(_workingDirPath);
// This retries on IOExceptions which is dangerous as we're also working with files await Http.ResiliencePolicy.ExecuteAsync(async () =>
await Http.ExceptionPolicy.ExecuteAsync(async () =>
{ {
// Download the file // Download the file
using var response = await Http.Client.GetAsync(url, cancellationToken); using var response = await Http.Client.GetAsync(url, cancellationToken);
await using (var output = File.Create(filePath)) await using (var output = File.Create(filePath))
{
await response.Content.CopyToAsync(output, cancellationToken); await response.Content.CopyToAsync(output, cancellationToken);
}
// Try to set the file date according to the last-modified header // Try to set the file date according to the last-modified header
try try
@ -74,6 +70,7 @@ internal partial class MediaDownloader
// ignore exceptions thrown here. // ignore exceptions thrown here.
} }
}); });
}
return _pathCache[url] = filePath; return _pathCache[url] = filePath;
} }

View file

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
using System.Net.Http;
using System.Text.RegularExpressions;
namespace DiscordChatExporter.Core.Utils.Extensions;
public static class ExceptionExtensions
{
private static void PopulateChildren(this Exception exception, ICollection<Exception> children)
{
if (exception is AggregateException aggregateException)
{
foreach (var innerException in aggregateException.InnerExceptions)
{
children.Add(innerException);
PopulateChildren(innerException, children);
}
}
else if (exception.InnerException is not null)
{
children.Add(exception.InnerException);
PopulateChildren(exception.InnerException, children);
}
}
public static IReadOnlyList<Exception> GetSelfAndChildren(this Exception exception)
{
var children = new List<Exception> {exception};
PopulateChildren(exception, children);
return children;
}
public static HttpStatusCode? TryGetStatusCode(this HttpRequestException ex) =>
// This is extremely frail, but there's no other way
Regex
.Match(ex.Message, @": (\d+) \(")
.Groups[1]
.Value
.NullIfWhiteSpace()?
.Pipe(s => (HttpStatusCode) int.Parse(s, CultureInfo.InvariantCulture));
}

View file

@ -1,9 +1,8 @@
using System; using System;
using System.Globalization; using System.Linq;
using System.IO;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text.RegularExpressions; using System.Net.Sockets;
using System.Threading.Tasks; using System.Threading.Tasks;
using DiscordChatExporter.Core.Utils.Extensions; using DiscordChatExporter.Core.Utils.Extensions;
using Polly; using Polly;
@ -14,13 +13,26 @@ public static class Http
{ {
public static HttpClient Client { get; } = new(); public static HttpClient Client { get; } = new();
public static IAsyncPolicy<HttpResponseMessage> ResponsePolicy { get; } = private static bool IsRetryableStatusCode(HttpStatusCode statusCode) => statusCode is
HttpStatusCode.TooManyRequests or
HttpStatusCode.RequestTimeout or
HttpStatusCode.InternalServerError;
private static bool IsRetryableException(Exception exception) =>
exception.GetSelfAndChildren().Any(ex =>
ex is TimeoutException or SocketException ||
ex is HttpRequestException hrex && IsRetryableStatusCode(hrex.TryGetStatusCode() ?? HttpStatusCode.OK)
);
public static IAsyncPolicy ResiliencePolicy { get; } =
Policy Policy
.Handle<IOException>() .Handle<Exception>(IsRetryableException)
.Or<HttpRequestException>() .WaitAndRetryAsync(4, i => TimeSpan.FromSeconds(Math.Pow(2, i) + 1));
.OrResult<HttpResponseMessage>(m => m.StatusCode == HttpStatusCode.TooManyRequests)
.OrResult(m => m.StatusCode == HttpStatusCode.RequestTimeout) public static IAsyncPolicy<HttpResponseMessage> ResponseResiliencePolicy { get; } =
.OrResult(m => m.StatusCode >= HttpStatusCode.InternalServerError) Policy
.Handle<Exception>(IsRetryableException)
.OrResult<HttpResponseMessage>(m => IsRetryableStatusCode(m.StatusCode))
.WaitAndRetryAsync( .WaitAndRetryAsync(
8, 8,
(i, result, _) => (i, result, _) =>
@ -43,24 +55,4 @@ public static class Http
}, },
(_, _, _, _) => Task.CompletedTask (_, _, _, _) => Task.CompletedTask
); );
private static HttpStatusCode? TryGetStatusCodeFromException(HttpRequestException ex) =>
// This is extremely frail, but there's no other way
Regex
.Match(ex.Message, @": (\d+) \(")
.Groups[1]
.Value
.NullIfWhiteSpace()?
.Pipe(s => (HttpStatusCode) int.Parse(s, CultureInfo.InvariantCulture));
public static IAsyncPolicy ExceptionPolicy { get; } =
Policy
.Handle<IOException>() // dangerous
.Or<HttpRequestException>(ex =>
TryGetStatusCodeFromException(ex) is
HttpStatusCode.TooManyRequests or
HttpStatusCode.RequestTimeout or
HttpStatusCode.InternalServerError
)
.WaitAndRetryAsync(4, i => TimeSpan.FromSeconds(Math.Pow(2, i) + 1));
} }