You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Connected.Common/Common/Distributed/DistributedLockProtector.cs

74 lines
2.7 KiB

2 years ago
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})");
}
}
}
}