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);
}