w4c-workflows-api/Services/Triggers/ITriggerState.cs

51 lines
1.9 KiB
C#
Raw Normal View History

using System.Globalization;
using StackExchange.Redis;
namespace w4c_workflows.Services.Triggers;
/// <summary>
/// Stores the last time a scheduled trigger (cron/interval) fired, per
/// workflow. Redis-backed in v1 (key <c>wf:{tenant}:triggers:{workflowId}:lastFire</c>);
/// ephemeral by design — losing it only re-fires a workflow once, which is safe
/// because delivery is at-least-once and the run lifecycle dedups via
/// correlation id.
/// </summary>
public interface ITriggerState
{
Task<DateTimeOffset?> GetLastFireAsync(string tenantId, Guid workflowId, CancellationToken ct);
Task SetLastFireAsync(string tenantId, Guid workflowId, DateTimeOffset at, CancellationToken ct);
}
public class RedisTriggerState : ITriggerState
{
private readonly IDatabase _db;
private readonly TimeSpan _ttl;
public RedisTriggerState(IConnectionMultiplexer redis, IConfiguration config)
{
_db = redis.GetDatabase();
_ttl = TimeSpan.FromDays(ParseInt(config["Workflows:TriggerStateTtlDays"], 90));
}
public async Task<DateTimeOffset?> GetLastFireAsync(string tenantId, Guid workflowId, CancellationToken ct)
{
var value = await _db.StringGetAsync(KeyFor(tenantId, workflowId));
if (value.IsNullOrEmpty)
return null;
return DateTimeOffset.TryParse(value.ToString(), CultureInfo.InvariantCulture,
DateTimeStyles.RoundtripKind, out var parsed)
? parsed
: null;
}
public Task SetLastFireAsync(string tenantId, Guid workflowId, DateTimeOffset at, CancellationToken ct)
=> _db.StringSetAsync(KeyFor(tenantId, workflowId), at.ToString("O"), _ttl);
private static RedisKey KeyFor(string tenantId, Guid workflowId)
=> $"wf:{tenantId}:triggers:{workflowId}:lastFire";
private static int ParseInt(string? text, int fallback)
=> int.TryParse(text, out var value) && value > 0 ? value : fallback;
}