using StackExchange.Redis; using System.Collections.Concurrent; using adas_core.Application.Services.Interfaces; namespace adas_core.Application.Services.Caching { /// /// 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. /// public class RedisLockProvider(Func 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 _tokens = new(StringComparer.Ordinal); /// /// Attempts to acquire a distributed lock for the specified key using Redis, retrying until the timeout expires. /// Returns false if the Redis database is unavailable or if the lock cannot be acquired within the given timeout. /// /// The identifier of the resource to lock. /// The maximum duration to keep retrying before giving up. /// true if the lock was successfully acquired; otherwise, false. public async Task 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; } /// /// 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. /// /// The identifier of the token to release. 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] ); } } }