using System.Threading.Channels; namespace Cleanuparr.Infrastructure.Features.DryRun; /// /// Counts dry runs in flight. /// public sealed class DryRunActivity { private readonly SemaphoreSlim _gate = new(1, 1); private readonly Channel _purgeRequests = Channel.CreateBounded( new BoundedChannelOptions(1) { FullMode = BoundedChannelFullMode.DropWrite }); private int _active; /// /// Whether any dry run is in flight. /// public bool IsActive => Volatile.Read(ref _active) > 0; /// /// Marks the start of a dry run. /// Waits for any in-progress purge to finish first. /// public async Task EnterAsync() { await _gate.WaitAsync(); try { Interlocked.Increment(ref _active); } finally { _gate.Release(); } } /// /// Marks the end of a dry run. /// Requests a purge once no dry run is left active. /// public void Exit() { if (Interlocked.Decrement(ref _active) == 0) { RequestPurge(); } } /// /// Requests a purge. /// Buffers at most one pending request. /// public void RequestPurge() { _purgeRequests.Writer.TryWrite(true); } /// /// Waits for a pending purge request. /// public ValueTask WaitForPurgeRequestAsync(CancellationToken ct) { return _purgeRequests.Reader.ReadAsync(ct); } /// /// Runs the given action only if no dry run is in flight. /// Blocks new dry runs while the action runs. /// public async Task RunIfIdleAsync(Func action) { await _gate.WaitAsync(); try { if (Volatile.Read(ref _active) > 0) { return false; } await action(); return true; } finally { _gate.Release(); } } }