using Connected.Caching; using Connected.Entities; using Connected.Entities.Storage; using Connected.ServiceModel; using Connected.Services; namespace Common.Distributed; internal sealed class DistributedLockOps { public class Lock : ServiceFunction { static Lock() { SynchronizationState = new(); } private static HashSet SynchronizationState { get; } public Lock(IStorageProvider storage, ICacheContext cache) { Cache = cache; Storage = storage; } private ICacheContext Cache { get; } private IStorageProvider Storage { get; } private IDistributedLock Entity { get; set; } protected override async Task OnInvoke() { /* * We must ensure that only one lock request is performed at a time for each entity. This is * because we must guarantee that one and only one entry exist for each entity and its primary key. */ var key = $"{Arguments.Entity}.{Arguments.EntityId}".ToLowerInvariant(); /* * If the hashset already holds the key it means someone else was faster than we and we are not able to * perform a lock. */ if (!SynchronizationState.Add(key)) throw new SynchronizationLockException($"{SR.ValLock} ({key})"); try { Entity = await Storage.Open().Update(Arguments.AsEntity(State.New, new { Id = Guid.NewGuid(), Expiration = DateTime.UtcNow.Add(Arguments.Duration ?? TimeSpan.FromSeconds(5)) })); return Entity.Id; } finally { /* * Free to remove the hash lock. Any subsequent lock requests will fail at data protection level. */ SynchronizationState.Remove(key); } } protected override async Task OnCommitted() { /* * Put the lock it the local cache. */ Cache.Set(DistributedLock.EntityKey, Entity.Id, Entity, TimeSpan.Zero); await Task.CompletedTask; } } public sealed class Unlock : ServiceAction> { public Unlock(IStorageProvider storage, ICacheContext cache) { Storage = storage; Cache = cache; } private IStorageProvider Storage { get; } private ICacheContext Cache { get; } protected override async Task OnInvoke() { await Storage.Open().Update(Arguments.AsEntity(State.Deleted)); } protected override async Task OnCommitted() { await Cache.Remove(DistributedLock.EntityKey, Arguments.Id); } } public sealed class Ping : ServiceAction { public Ping(IStorageProvider storage, ICacheContext cache) { Storage = storage; Cache = cache; } private IStorageProvider Storage { get; } private ICacheContext Cache { get; } protected override async Task OnInvoke() { var entity = await Cache.Get(DistributedLock.EntityKey, Arguments.Id, async (f) => { return await (from dc in Storage.Open() where dc.Id == Arguments.Id select dc).AsEntity(); }); if (entity is not null) { await Storage.Open().Update(Arguments.AsEntity(State.Default, new { Expiration = DateTime.UtcNow.Add(Arguments.Duration ?? TimeSpan.FromSeconds(5)) })); } } protected override async Task OnCommitted() { await Cache.Remove(DistributedLock.EntityKey, Arguments.Id); } } }