Improve proxy cache behavior and diagnostics

This commit is contained in:
ajp_anton
2026-07-20 02:17:08 +00:00
parent a97e2bb37f
commit 4bd6277635
10 changed files with 503 additions and 64 deletions
@@ -68,6 +68,30 @@ public sealed class ItemsProxyRequestBuilderTests
Assert.Contains("|SortName|Descending|L|20|10|fi-FI|", result.CacheKey);
}
[Fact]
public void BuildUsesOneUpstreamKeyForDifferentLocalPagingControls()
{
var request = HttpRequest();
var first = ItemsProxyRequestBuilder.Build(
request,
"/Items?SortBy=SortName&StartIndex=0&Limit=100",
"token",
UserId,
multilangEnabled: true);
var second = ItemsProxyRequestBuilder.Build(
request,
"/Items?SortBy=SortName&StartIndex=100&Limit=100",
"token",
UserId,
multilangEnabled: true);
Assert.NotNull(first);
Assert.NotNull(second);
Assert.Equal(first.NormalizedUrlForCache, second.NormalizedUrlForCache);
Assert.NotEqual(first.CacheKey, second.CacheKey);
}
[Fact]
public void BuildLeavesSortAndPagingUpstreamWhenMultilangDisabled()
{
@@ -0,0 +1,52 @@
using Jellyfin.Plugin.Multilang.Services;
namespace Jellyfin.Plugin.Multilang.Tests;
public sealed class ItemsProxyRequestCoalescerTests
{
[Fact]
public async Task MatchingRequestsShareOneInFlightFetch()
{
var coalescer = new ItemsProxyRequestCoalescer();
var response = new TaskCompletionSource<ItemsProxyUpstreamResponse>(TaskCreationOptions.RunContinuationsAsynchronously);
var fetches = 0;
Task<ItemsProxyUpstreamResponse> Fetch(CancellationToken _)
{
fetches++;
return response.Task;
}
var first = coalescer.GetOrFetchAsync("user|/Items", Fetch, CancellationToken.None);
var second = coalescer.GetOrFetchAsync("user|/Items", Fetch, CancellationToken.None);
Assert.Equal(1, fetches);
response.SetResult(new ItemsProxyUpstreamResponse(200, "{}", "application/json"));
var firstResult = await first;
var secondResult = await second;
Assert.False(firstResult.JoinedExistingRequest);
Assert.True(secondResult.JoinedExistingRequest);
Assert.Equal("{}", firstResult.Response.Body);
Assert.Equal(firstResult.Response, secondResult.Response);
}
[Fact]
public async Task CancellingOneWaiterDoesNotCancelTheSharedFetch()
{
var coalescer = new ItemsProxyRequestCoalescer();
var response = new TaskCompletionSource<ItemsProxyUpstreamResponse>(TaskCreationOptions.RunContinuationsAsynchronously);
using var cancellation = new CancellationTokenSource();
var cancelledWaiter = coalescer.GetOrFetchAsync("user|/Items", _ => response.Task, cancellation.Token);
var activeWaiter = coalescer.GetOrFetchAsync("user|/Items", _ => response.Task, CancellationToken.None);
cancellation.Cancel();
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => cancelledWaiter);
response.SetResult(new ItemsProxyUpstreamResponse(200, "{}", "application/json"));
var result = await activeWaiter;
Assert.True(result.JoinedExistingRequest);
Assert.Equal(200, result.Response.StatusCode);
}
}