45 lines
1.7 KiB
C#
45 lines
1.7 KiB
C#
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;
|
|
}
|
|
}
|