|
|
|
|
using Common.Distributed;
|
|
|
|
|
using Connected;
|
|
|
|
|
using Connected.Interop;
|
|
|
|
|
using Connected.ServiceModel;
|
|
|
|
|
using Connected.Threading;
|
|
|
|
|
|
|
|
|
|
namespace Common.Documents;
|
|
|
|
|
internal sealed class DocumentLocker<TDocument, TPrimaryKey> : IDocumentLocker<TDocument, TPrimaryKey>
|
|
|
|
|
where TDocument : IDocument<TPrimaryKey>
|
|
|
|
|
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<string>(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<Guid> { 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);
|
|
|
|
|
}
|
|
|
|
|
}
|