using Connected.Caching; using Connected.Data.DataProtection; using Connected.Data.EntityProtection; using Connected.Entities; using Connected.Entities.Storage; using Connected.Middleware; namespace Common.Distributed; internal sealed class DistributedLockProtector : MiddlewareComponent, IEntityProtector { public DistributedLockProtector(IStorageProvider storage, ICacheContext cache) { Storage = storage; Cache = cache; } private IStorageProvider Storage { get; } private ICacheContext Cache { get; } public async Task Invoke(EntityProtectionArgs args) { /* * We don't care for deleted entities. */ if (args.State == State.Deleted) return; /* * The most important thing is to prevent duplicate inserts. If the lock * already exists we will reject the transaction. Each entity record can have * only one entry in the distributed lock table. */ if (args.State == State.New) { /* * First we'll look into the memory cache. */ var existing = Cache.Get(DistributedLock.EntityKey, f => string.Equals(f.Entity, args.Entity.Entity, StringComparison.OrdinalIgnoreCase) && string.Equals(f.EntityId, args.Entity.EntityId, StringComparison.OrdinalIgnoreCase)); /* * If the cache entry exists and holds a valid lock, we simply reject the transaction. */ if (existing is not null && existing.Expiration > DateTime.UtcNow) throw new InvalidOperationException($"{SR.ValLock} ({args.Entity.Entity}, {args.Entity.EntityId})"); /* * Entry doesn't exist in the cache. Let's look in the storage. Note that this scenarios is unusual because * locks tend to be short and there are only two possible schenarios that lock exists in the storage but not * in the cache: * - the process has rebooted * - the master has changed. It this case all distributed services are redirected to the new master which must * load record by record from the cache. In this case it's most probably that all locks already expired but we * must check that anyway */ var entry = await (from dc in Storage.Open() where string.Equals(dc.Entity, args.Entity.Entity, StringComparison.OrdinalIgnoreCase) && string.Equals(dc.EntityId, args.Entity.EntityId, StringComparison.OrdinalIgnoreCase) select dc).AsEntity(); /* * It exists, we must perform additional checks. */ if (entry is not null) { /* * Set it in the cache so it gets deleted by the recycling service. */ Cache.Set(DistributedLock.EntityKey, entry.Id, entry, TimeSpan.Zero); if (entry.Expiration > DateTime.UtcNow) throw new InvalidOperationException($"{SR.ValLock} ({args.Entity.Entity}, {args.Entity.EntityId})"); } } } }