Initial curated itworx-knowledge-hub source from 3b1751d0d9dee551aa6994eee0093704dc334817
This commit is contained in:
commit
ad8ad252a9
825 files changed
+124216
No files matched your search
@@ -0,0 +1,91 @@
|
||||
namespace ItWorx.KnowledgeHub.Application.Attachments;
|
||||
|
||||
/// <summary>
|
||||
/// One process-wide, per-content-hash mutual-exclusion lock, shared by every writer of
|
||||
/// the content-addressed blob store — <c>UploadAttachmentUseCase</c> (commit a new blob
|
||||
/// or discover an existing one) and <c>OrphanedBlobGarbageCollector</c> (delete a blob
|
||||
/// no reference points to any more). Registered as a singleton so both use cases,
|
||||
/// resolved from different per-request scopes, serialize against the very same lock
|
||||
/// table.
|
||||
///
|
||||
/// This is what makes blob garbage collection safe against a concurrent upload of
|
||||
/// byte-identical content: without it, a GC pass could delete a blob's database row and
|
||||
/// physical file in between an upload's own "does this hash already exist?" check and
|
||||
/// its own write, corrupting the upload's result. With both critical sections holding
|
||||
/// the same per-hash lock, one of two outcomes is always consistent — either GC
|
||||
/// completes first (the upload then behaves exactly like any fresh upload of new
|
||||
/// content), or the upload completes first (GC's own re-check-under-lock then correctly
|
||||
/// finds a live reference and skips deletion).
|
||||
/// </summary>
|
||||
public sealed class AttachmentContentLock
|
||||
{
|
||||
private readonly object gate = new();
|
||||
private readonly Dictionary<string, Entry> locks = [];
|
||||
|
||||
public async Task<IDisposable> AcquireAsync(string sha256, CancellationToken cancellationToken)
|
||||
{
|
||||
Entry entry;
|
||||
lock (gate)
|
||||
{
|
||||
if (!locks.TryGetValue(sha256, out entry!))
|
||||
{
|
||||
entry = new Entry();
|
||||
locks.Add(sha256, entry);
|
||||
}
|
||||
|
||||
entry.ReferenceCount += 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await entry.Semaphore.WaitAsync(cancellationToken);
|
||||
return new Lease(sha256, entry, this);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Release(sha256, entry, acquired: false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void Release(string sha256, Entry entry, bool acquired)
|
||||
{
|
||||
if (acquired)
|
||||
{
|
||||
entry.Semaphore.Release();
|
||||
}
|
||||
|
||||
lock (gate)
|
||||
{
|
||||
entry.ReferenceCount -= 1;
|
||||
if (entry.ReferenceCount == 0 && locks.TryGetValue(sha256, out var current) && ReferenceEquals(current, entry))
|
||||
{
|
||||
locks.Remove(sha256);
|
||||
entry.Semaphore.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class Entry
|
||||
{
|
||||
public SemaphoreSlim Semaphore { get; } = new(1, 1);
|
||||
|
||||
public int ReferenceCount { get; set; }
|
||||
}
|
||||
|
||||
private sealed class Lease(string sha256, Entry entry, AttachmentContentLock owner) : IDisposable
|
||||
{
|
||||
private bool disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
disposed = true;
|
||||
owner.Release(sha256, entry, acquired: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user