The proxy's shared download is started once

ConcurrentDictionary.GetOrAdd may run its factory more than once when asked at the same time, and each run started a
download: five clients asking at once for a new file made three upstream requests in one run of
Clients_asking_at_once_share_one_download. The factory now makes a Lazy, of which only the one kept is ever started,
and a download that begins just after another cached the file uses it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-07 11:00:04 +02:00
1 parent 8fecf7de2d
commit a4ced664cf
1 file changed
+12 -5
+12 -5
View File
@@ -55,7 +55,8 @@ namespace PrivaPub.Domain.Media
readonly IMediaService _media; readonly IMediaService _media;
readonly IOptionsMonitor<MediaOptions> _options; readonly IOptionsMonitor<MediaOptions> _options;
readonly Federation.Moderation.IDomainBlocks _domainBlocks; readonly Federation.Moderation.IDomainBlocks _domainBlocks;
readonly ConcurrentDictionary<string, Task<(ProxyOutcome, string, string)>> _inFlight = new(); // Lazy: GetOrAdd may run its factory more than once when asked at the same time, but only one Lazy is ever evaluated
readonly ConcurrentDictionary<string, Lazy<Task<(ProxyOutcome, string, string)>>> _inFlight = new();
readonly ConcurrentDictionary<string, (ProxyOutcome Outcome, DateTime Until)> _refused = new(); readonly ConcurrentDictionary<string, (ProxyOutcome Outcome, DateTime Until)> _refused = new();
readonly SemaphoreSlim _downloads = new(Downloads); readonly SemaphoreSlim _downloads = new(Downloads);
readonly object _keyLock = new(); readonly object _keyLock = new();
@@ -153,15 +154,15 @@ namespace PrivaPub.Domain.Media
_refused.TryRemove(url, out _); _refused.TryRemove(url, out _);
} }
// shared by everyone asking for it now; the download itself isn't cancelled when one of them leaves // shared by everyone asking for it now; the download itself isn't cancelled when one of them leaves
var download = _inFlight.GetOrAdd(url, key => DownloadOnce(key)); var shared = _inFlight.GetOrAdd(url, key => new Lazy<Task<(ProxyOutcome, string, string)>>(() => DownloadOnce(key)));
try try
{ {
return await download.WaitAsync(token); return await shared.Value.WaitAsync(token);
} }
finally finally
{ {
if (download.IsCompleted) if (shared.Value.IsCompleted)
_inFlight.TryRemove(new KeyValuePair<string, Task<(ProxyOutcome, string, string)>>(url, download)); _inFlight.TryRemove(new KeyValuePair<string, Lazy<Task<(ProxyOutcome, string, string)>>>(url, shared));
} }
} }
@@ -170,6 +171,12 @@ namespace PrivaPub.Domain.Media
await Task.Yield(); await Task.Yield();
if (!await _downloads.WaitAsync(TimeSpan.FromSeconds(30))) if (!await _downloads.WaitAsync(TimeSpan.FromSeconds(30)))
return (ProxyOutcome.Failed, default, default);//too busy: the client tries again return (ProxyOutcome.Failed, default, default);//too busy: the client tries again
// cached by a download that ended just before this one began
if (Cached(url) is { Path: not null } done)
{
_downloads.Release();
return (ProxyOutcome.Cached, done.Path, done.ContentType);
}
var (path, typePath) = CachePaths(url); var (path, typePath) = CachePaths(url);
var part = $"{path}.{Guid.NewGuid():N}.part"; var part = $"{path}.{Guid.NewGuid():N}.part";
try try