using w4c_workflows.Data;
namespace w4c_workflows.Services.Runs;
///
/// Hosts as a control-plane background service.
/// Each tick it dispatches pending runs and consumes task results across every
/// tenant with a running run. All state-machine logic lives in the engine
/// so it is unit-testable without a running host.
///
public class RunLifecycleService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly RunLifecycleEngine _engine;
private readonly ILogger _logger;
private readonly int _pollDelayMs;
private readonly int _minTickDelayMs;
public RunLifecycleService(
IServiceScopeFactory scopeFactory,
RunLifecycleEngine engine,
IConfiguration config,
ILogger logger)
{
_scopeFactory = scopeFactory;
_engine = engine;
_logger = logger;
_pollDelayMs = ParseInt(config["Workflows:LifecyclePollDelayMs"], 500);
_minTickDelayMs = ParseInt(config["Workflows:LifecycleMinTickDelayMs"], 25);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("RunLifecycleService started");
while (!stoppingToken.IsCancellationRequested)
{
var processed = 0;
try
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService();
var dispatcher = scope.ServiceProvider.GetRequiredService();
var store = scope.ServiceProvider.GetRequiredService();
processed += await _engine.RetryDueAsync(db, dispatcher, stoppingToken);
processed += await _engine.DispatchPendingAsync(db, dispatcher, stoppingToken);
processed += await _engine.ProcessResultsAsync(db, dispatcher, store, stoppingToken);
processed += await _engine.TimeoutStaleRunsAsync(db, stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Run lifecycle loop error");
}
// Always yield: work found means a short breather rather than an
// immediate re-loop, so a busy queue cannot starve the thread pool.
if (!stoppingToken.IsCancellationRequested)
{
var delay = processed == 0 ? _pollDelayMs : _minTickDelayMs;
try { await Task.Delay(delay, stoppingToken); }
catch (OperationCanceledException) { break; }
}
}
_logger.LogInformation("RunLifecycleService stopped");
}
private static int ParseInt(string? text, int fallback)
=> int.TryParse(text, out var value) && value > 0 ? value : fallback;
}