w4c-workflows-api/Services/LeaseService.cs

45 lines
1.7 KiB
C#
Raw Normal View History

using StackExchange.Redis;
namespace w4c_workflows.Services;
/// <summary>Distributed lease over Redis for in-flight run state.</summary>
public interface ILeaseService
{
Task<bool> AcquireAsync(string tenantId, string runId, string owner, TimeSpan ttl);
Task<bool> ReleaseAsync(string tenantId, string runId, string owner);
}
/// <summary>
/// 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.
/// </summary>
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}";
/// <summary>Atomically acquires the lease; returns false if already held.</summary>
public Task<bool> AcquireAsync(string tenantId, string runId, string owner, TimeSpan ttl)
=> _db.StringSetAsync(KeyFor(tenantId, runId), owner, ttl, When.NotExists);
/// <summary>Releases the lease only if the caller owns it.</summary>
public async Task<bool> 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;
}
}