8.2 FrameLockProvider

Keyed per-frame SemaphoreSlim(1,1) over a ConcurrentDictionary with a
disposable releaser, so the next increment can serialize the upload
pipeline per FrameName.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 16:03:11 +02:00
parent 475e8988b5
commit e1c35d7423
2 changed files with 34 additions and 1 deletions

View File

@@ -0,0 +1,33 @@
using System.Collections.Concurrent;
using FrameProcessor.Domain;
namespace FrameProcessor.Concurrency;
/// <summary>
/// Hands out a per-frame <see cref="SemaphoreSlim"/> so concurrent uploads to the same
/// frame are serialized while different frames remain independent
/// (see SPEC.md §3.4, CLAUDE.md "Per-frame serialization").
/// </summary>
public sealed class FrameLockProvider
{
private readonly ConcurrentDictionary<FrameName, SemaphoreSlim> _locks = new();
public async Task<IDisposable> AcquireAsync(FrameName frame, CancellationToken cancellationToken)
{
var semaphore = _locks.GetOrAdd(frame, _ => new SemaphoreSlim(1, 1));
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
return new Releaser(semaphore);
}
private sealed class Releaser : IDisposable
{
private SemaphoreSlim? _semaphore;
public Releaser(SemaphoreSlim semaphore) => _semaphore = semaphore;
public void Dispose()
{
Interlocked.Exchange(ref _semaphore, null)?.Release();
}
}
}