aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorShadowghost <Shadowghost@users.noreply.github.com>2026-09-27 16:30:42 -0400
committerCody Robibero <cody@robibe.ro>2026-09-27 16:30:42 -0400
commitb1787cbc36d44d7feccd7e9adfadf739a56cc190 (patch)
tree4fac2069d33cdec64af3195bbdc5e337400f525b /tests
parented1147b2492bf0e1d0f9ca544669b182350400b2 (diff)
Backport pull request #18095 from jellyfin/release-12.z
Retry MusicBrainz requests when the server is busy Original-merge: 1478011dfc67c46d52d22b89a49e4519ec42bdb8 Merged-by: crobibero <cody@robibe.ro> Backported-by: Cody Robibero <cody@robibe.ro>
Diffstat (limited to 'tests')
-rw-r--r--tests/Jellyfin.Providers.Tests/Music/MusicBrainzQueryExtensionsTests.cs136
1 files changed, 136 insertions, 0 deletions
diff --git a/tests/Jellyfin.Providers.Tests/Music/MusicBrainzQueryExtensionsTests.cs b/tests/Jellyfin.Providers.Tests/Music/MusicBrainzQueryExtensionsTests.cs
new file mode 100644
index 0000000000..6b0d3a2ab2
--- /dev/null
+++ b/tests/Jellyfin.Providers.Tests/Music/MusicBrainzQueryExtensionsTests.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Globalization;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Providers.Plugins.MusicBrainz;
+using MetaBrainz.Common;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+
+namespace Jellyfin.Providers.Tests.Music;
+
+public static class MusicBrainzQueryExtensionsTests
+{
+ [Fact]
+ public static async Task RetryOnTransientErrorAsync_ServerBusy_RetriesAndSucceeds()
+ {
+ var attempts = 0;
+
+ var result = await MusicBrainzQueryExtensions.RetryOnTransientErrorAsync(
+ async _ =>
+ {
+ attempts++;
+ if (attempts == 1)
+ {
+ throw await CreateErrorAsync(HttpStatusCode.ServiceUnavailable, TimeSpan.Zero);
+ }
+
+ return "found";
+ },
+ "test",
+ NullLogger.Instance,
+ CancellationToken.None);
+
+ Assert.Equal("found", result);
+ Assert.Equal(2, attempts);
+ }
+
+ [Fact]
+ public static async Task RetryOnTransientErrorAsync_ServerStaysBusy_GivesUp()
+ {
+ var attempts = 0;
+
+ var error = await Assert.ThrowsAsync<HttpError>(() => MusicBrainzQueryExtensions.RetryOnTransientErrorAsync<string>(
+ async _ =>
+ {
+ attempts++;
+ throw await CreateErrorAsync(HttpStatusCode.ServiceUnavailable, TimeSpan.Zero);
+ },
+ "test",
+ NullLogger.Instance,
+ CancellationToken.None));
+
+ Assert.Equal(HttpStatusCode.ServiceUnavailable, error.Status);
+ Assert.Equal(3, attempts);
+ }
+
+ [Fact]
+ public static async Task RetryOnTransientErrorAsync_NotFound_DoesNotRetry()
+ {
+ var attempts = 0;
+
+ await Assert.ThrowsAsync<HttpError>(() => MusicBrainzQueryExtensions.RetryOnTransientErrorAsync<string>(
+ async _ =>
+ {
+ attempts++;
+ throw await CreateErrorAsync(HttpStatusCode.NotFound, null);
+ },
+ "test",
+ NullLogger.Instance,
+ CancellationToken.None));
+
+ Assert.Equal(1, attempts);
+ }
+
+ [Fact]
+ public static async Task GetRetryDelay_NoHint_BacksOffExponentially()
+ {
+ var error = await CreateErrorAsync(HttpStatusCode.ServiceUnavailable, null);
+
+ Assert.Equal(TimeSpan.FromSeconds(1), MusicBrainzQueryExtensions.GetRetryDelay(error, 1));
+ Assert.Equal(TimeSpan.FromSeconds(2), MusicBrainzQueryExtensions.GetRetryDelay(error, 2));
+ }
+
+ [Fact]
+ public static async Task GetRetryDelay_RetryAfterZero_WaitsMinimum()
+ {
+ var error = await CreateErrorAsync(HttpStatusCode.ServiceUnavailable, TimeSpan.Zero);
+
+ Assert.Equal(TimeSpan.FromSeconds(1), MusicBrainzQueryExtensions.GetRetryDelay(error, 1));
+ }
+
+ [Fact]
+ public static async Task GetRetryDelay_RetryAfterLongerThanBackoff_UsesRetryAfter()
+ {
+ var error = await CreateErrorAsync(HttpStatusCode.ServiceUnavailable, TimeSpan.FromSeconds(10));
+
+ Assert.Equal(TimeSpan.FromSeconds(10), MusicBrainzQueryExtensions.GetRetryDelay(error, 1));
+ }
+
+ [Fact]
+ public static async Task GetRetryDelay_LongRetryAfter_IsCapped()
+ {
+ var error = await CreateErrorAsync(HttpStatusCode.ServiceUnavailable, TimeSpan.FromHours(1));
+
+ Assert.Equal(TimeSpan.FromSeconds(15), MusicBrainzQueryExtensions.GetRetryDelay(error, 1));
+ }
+
+ [Fact]
+ public static async Task GetRetryDelay_RateLimitWindow_WaitsForReset()
+ {
+ var error = await CreateErrorAsync(
+ HttpStatusCode.ServiceUnavailable,
+ null,
+ headers => headers.TryAddWithoutValidation("X-RateLimit-Reset", DateTimeOffset.UtcNow.AddSeconds(8).ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture)));
+
+ var delay = MusicBrainzQueryExtensions.GetRetryDelay(error, 1);
+
+ Assert.InRange(delay, TimeSpan.FromSeconds(6), TimeSpan.FromSeconds(8));
+ }
+
+ private static async Task<HttpError> CreateErrorAsync(HttpStatusCode status, TimeSpan? retryAfter, Action<HttpResponseHeaders>? configureHeaders = null)
+ {
+ using var response = new HttpResponseMessage(status);
+ if (retryAfter is not null)
+ {
+ response.Headers.RetryAfter = new RetryConditionHeaderValue(retryAfter.Value);
+ }
+
+ configureHeaders?.Invoke(response.Headers);
+
+ return await HttpError.FromResponseAsync(response);
+ }
+}