|
|
|
|
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<DistributedLock>
|
|
|
|
|
{
|
|
|
|
|
public DistributedLockProtector(IStorageProvider storage, ICacheContext cache)
|
|
|
|
|
{
|
|
|
|
|
Storage = storage;
|
|
|
|
|
Cache = cache;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private IStorageProvider Storage { get; }
|
|
|
|
|
private ICacheContext Cache { get; }
|
|
|
|
|
|
|
|
|
|
public async Task Invoke(EntityProtectionArgs<DistributedLock> 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>(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<DistributedLock>()
|
|
|
|
|
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})");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|