using Common.Distributed; using Connected; using Connected.Interop; using Connected.ServiceModel; using Connected.Threading; namespace Common.Documents; internal sealed class DocumentLocker : IDocumentLocker where TDocument : IDocument where TPrimaryKey : notnull { public event EventHandler? Expired; public DocumentLocker(IDistributedLockService locking) { Locking = locking; Cancel = new(); } private bool IsDisposed { get; set; } public Guid Key { get; private set; } private IDistributedLockService Locking { get; } public TimeSpan LockTimeout { get; set; } = TimeSpan.FromSeconds(2); public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(1500); public TimeSpan Lifetime { get; set; } = TimeSpan.FromSeconds(30); private CancellationTokenSource Cancel { get; } public async Task Lock(TDocument document) { var id = TypeConversion.Convert(document.Id); if (string.IsNullOrEmpty(id)) throw new NullReferenceException(nameof(document.Id)); /* * Obtain the lock on the document to ensure no other process * will change it while we are updating the stock. If the lock * could not be obtained the exception will be thrown. */ Key = await Locking.Lock(new DistributedLockArgs { Duration = LockTimeout, Entity = typeof(TDocument).Name, EntityId = id }); /* * Make sure we don't get outdated by using the scheduled * task which will ping the lock before it expires. */ using var timeout = new ScheduledTask(async () => { await Locking.Ping(new DistributedLockPingArgs { Id = Key, Duration = LockTimeout }); }, async () => { await Unlock(); Expired?.Invoke(this, EventArgs.Empty); }, Timeout, Lifetime, Cancel.Token); timeout.Start(); } public async Task Unlock() { /* * Finally release the lock on the document so it becomes * updatable again. */ await Locking.Unlock(new PrimaryKeyArgs { Id = Key }); } private void Dispose(bool disposing) { if (!IsDisposed) { if (disposing) { AsyncUtils.RunSync(Unlock); Cancel.Cancel(); Cancel.Dispose(); } IsDisposed = true; } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } }