Files
adas-core/adas-core.Application/Services/Caching/RedisLockProvider.cs
T

81 lines
3.5 KiB
C#

using StackExchange.Redis;
using System.Collections.Concurrent;
using adas_core.Application.Services.Interfaces;
namespace adas_core.Application.Services.Caching
{
/// <summary>
/// Lock distribuido en Redis. Usa token por adquisición (owner)
/// y liberación segura con script Lua: borra la key solo si el valor coincide.
/// </summary>
/// <!-- aidoc:v1 sig=db4fe1b -->
public class RedisLockProvider(Func<IDatabase?> getDatabase) : ILockProvider
{
private readonly string _prefix = "lock:";
private readonly TimeSpan _ttl = TimeSpan.FromSeconds(5);
private readonly TimeSpan _retryDelay = TimeSpan.FromMilliseconds(50);
private static readonly string LuaReleaseScript = @"
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end";
private readonly ConcurrentDictionary<string, string> _tokens =
new(StringComparer.Ordinal);
/// <summary>
/// Attempts to acquire a distributed lock for the specified key using Redis, retrying until the timeout expires.
/// Returns <c>false</c> if the Redis database is unavailable or if the lock cannot be acquired within the given timeout.
/// </summary>
/// <param name="key">The identifier of the resource to lock.</param>
/// <param name="timeout">The maximum duration to keep retrying before giving up.</param>
/// <returns><c>true</c> if the lock was successfully acquired; otherwise, <c>false</c>.</returns>
/// <!-- aidoc:v1 sig=4b91964 body=b8dd956 -->
public async Task<bool> AcquireAsync(string key, TimeSpan timeout)
{
var redis = getDatabase();
if (redis is null) return false;
var redisKey = (RedisKey)(_prefix + key);
var token = Guid.NewGuid().ToString("N");
var end = DateTime.UtcNow.Add(timeout);
while (DateTime.UtcNow < end)
{
if (await redis.StringSetAsync(redisKey, token, _ttl, When.NotExists))
{
_tokens[key] = token;
return true;
}
await Task.Delay(_retryDelay);
}
return false;
}
/// <summary>
/// Asynchronously releases the token associated with the specified key by removing it from the in-memory token store and executing a Lua release script against Redis. If the key is not found in the local store, or the Redis database is unavailable, the method returns without performing any further action.
/// </summary>
/// <param name="key">The identifier of the token to release.</param>
/// <!-- aidoc:v1 sig=03497b0 body=db68b0e -->
public async Task ReleaseAsync(string key)
{
if (!_tokens.TryRemove(key, out var token))
return;
var redis = getDatabase();
if (redis is null) return;
var redisKey = (RedisKey)(_prefix + key);
await redis.ScriptEvaluateAsync(
LuaReleaseScript,
[redisKey],
[token]
);
}
}
}