using Kit.Helpers.Postgres; namespace Kit.Helpers; /// /// Провайдер блокировок через самописную схему (и её элементы) distributed.
/// Все активные блокировки хрянятся в таблице в явном виде.
/// У активной блокировки заполянется время начала (acquired_at_utc timestamptz) и обновляется раз в .
/// Если приложение упадёт и перестанет обновлять время начала, через (timeout_ms int default) миллисекунд блокировка будет считаться недействительной.
/// При следующей операции, связанной с любой блокировкой, запись о недействительной блокировке будет удалена. ///
public class PgTableDistributedLockService : IDistributedLockService { private readonly string _connectionString; private const int _sleepIntervalMs = 10; private const int _refreshIntervalMs = 100; private readonly object _ownersLock = new object(); private readonly IDictionary _owners = new Dictionary(); public PgTableDistributedLockService(IConfigurationDistributedPg configurationPgLock) { _connectionString = configurationPgLock.DistributedConnectionString; } private class LockOwner { public string Id { get; set; } public int ThreadId { get; set; } /// /// Уровень вложенности блокировок в рамках одного потока и объекта. /// 0 - активных блокировок нет. /// 1 - одна блокировка. /// >1 - есть вложенные блокировки. /// public IDictionary LockedObjects { get; set; } public LockOwner(int threadId) { ThreadId = threadId; Id = Guid.NewGuid().ToString(); LockedObjects = new Dictionary(); } } private IList> GetParameters(string type, string key, string ownerId) { return new List> { new KeyValuePair("_type", type), new KeyValuePair("_key", key), new KeyValuePair("_owner_id", ownerId), }; } private bool TryAcquireLock(string type, string key, string ownerId) { return _connectionString.ExecuteScalar("distributed.try_acquire_lock", isStoredProcedure: true, this.GetParameters(type, key, ownerId)); } private void Tick(string type, string key, string ownerId) { _connectionString.ExecuteNonQuery("distributed.lock_tick_for_owner_id", isStoredProcedure: true, this.GetParameters(type, key, ownerId)); } private void Free(string type, string key, string ownerId) { _connectionString.ExecuteNonQuery("distributed.lock_free_for_owner_id", isStoredProcedure: true, this.GetParameters(type, key, ownerId)); } private int Count(string type, string? key) { string sql; var parameters = new List> { new KeyValuePair("_type", type) }; if (key == null) { sql = "distributed.lock_count_by_type"; } else { sql = "distributed.lock_count_by_type_key"; parameters.Add(new KeyValuePair("_key", key)); } return _connectionString.ExecuteScalar(sql, isStoredProcedure: true, parameters); } private LockOwner GetLockOwner(string lockedObjectKey) { lock (_ownersLock) { LockOwner lockOwner; int threadId = Environment.CurrentManagedThreadId; if (_owners.ContainsKey(threadId)) { lockOwner = _owners[threadId]; } else { lockOwner = new LockOwner(threadId); _owners.Add(threadId, lockOwner); } if (lockOwner.LockedObjects.ContainsKey(lockedObjectKey)) { lockOwner.LockedObjects[lockedObjectKey]++; } else { lockOwner.LockedObjects.Add(lockedObjectKey, 1); } return lockOwner; } } public void ExecuteOnLock(string type, string key, Action action) => ExecuteOnLock(type, key, () => { action(); return true; }); public TResult ExecuteOnLock(string type, string key, Func action) { string lockedObjectKey = $"{type};;;{key}"; // Формируем Id владельца блокировки LockOwner owner = this.GetLockOwner(lockedObjectKey); try { // Ждём момента, когда наш запрос займёт блокировку while (TryAcquireLock(type, key, owner.Id) == false) { Thread.Sleep(_sleepIntervalMs); } // Периодически обновляя дату начала блокировки, выполняем операцию using (var timer = new Timer(_ => Tick(type, key, owner.Id), null, 0, _refreshIntervalMs)) { return action(); } } finally { lock (_ownersLock) { //Если это не вложенная блокировка if (owner.LockedObjects[lockedObjectKey] < 2) { // Освобождаем блокировку для владельца Free(type, key, owner.Id); owner.LockedObjects.Remove(lockedObjectKey); if (owner.LockedObjects.Count < 1) { _owners.Remove(owner.ThreadId); } } else { owner.LockedObjects[lockedObjectKey]--; } } } } public int GetLockCount(string type) => Count(type, null); public int GetLockCount(string type, string key) => Count(type, key); }