using StackExchange.Redis; namespace w4c_workflows.Services; /// Distributed lease over Redis for in-flight run state. public interface ILeaseService { Task AcquireAsync(string tenantId, string runId, string owner, TimeSpan ttl); Task RenewAsync(string tenantId, string runId, string owner, TimeSpan ttl); Task ReleaseAsync(string tenantId, string runId, string owner); } /// /// Lease / distributed lock over Redis for in-flight run state: /// wf:{tenant}:leases:{runId} → owner id, with a TTL. /// Acquire is atomic (SET NX PX); release compares the owner before deleting so /// a stale owner can never release someone else's lease. /// public class LeaseService : ILeaseService { // Compare-and-delete: only delete if the value still matches the owner. private const string ReleaseScript = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end"; private readonly IDatabase _db; public LeaseService(IConnectionMultiplexer redis) { _db = redis.GetDatabase(); } private static RedisKey KeyFor(string tenantId, string runId) => $"wf:{tenantId}:leases:{runId}"; /// Atomically acquires the lease; returns false if already held. public Task AcquireAsync(string tenantId, string runId, string owner, TimeSpan ttl) => _db.StringSetAsync(KeyFor(tenantId, runId), owner, ttl, When.NotExists); /// Extends the TTL if (and only if) the caller still owns the lease. public async Task RenewAsync(string tenantId, string runId, string owner, TimeSpan ttl) { var current = await _db.StringGetAsync(KeyFor(tenantId, runId)); if (current != owner) return false; return await _db.KeyExpireAsync(KeyFor(tenantId, runId), ttl); } /// Releases the lease only if the caller owns it. public async Task ReleaseAsync(string tenantId, string runId, string owner) { var result = await _db.ScriptEvaluateAsync( ReleaseScript, new RedisKey[] { KeyFor(tenantId, runId) }, new RedisValue[] { owner }); return (long)result == 1; } }