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; }