w4c-workflows-api/Services/Runs/RunLifecycleService.cs

71 lines
2.7 KiB
C#

using w4c_workflows.Data;
namespace w4c_workflows.Services.Runs;
/// <summary>
/// Hosts <see cref="RunLifecycleEngine"/> as a control-plane background service.
/// Each tick it dispatches pending runs and consumes task results across every
/// tenant with a <c>running</c> run. All state-machine logic lives in the engine
/// so it is unit-testable without a running host.
/// </summary>
public class RunLifecycleService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly RunLifecycleEngine _engine;
private readonly ILogger<RunLifecycleService> _logger;
private readonly int _pollDelayMs;
public RunLifecycleService(
IServiceScopeFactory scopeFactory,
RunLifecycleEngine engine,
IConfiguration config,
ILogger<RunLifecycleService> logger)
{
_scopeFactory = scopeFactory;
_engine = engine;
_logger = logger;
_pollDelayMs = ParseInt(config["Workflows:LifecyclePollDelayMs"], 500);
}
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<WorkflowsDbContext>();
var dispatcher = scope.ServiceProvider.GetRequiredService<TaskDispatcher>();
var store = scope.ServiceProvider.GetRequiredService<DurableStateStore>();
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");
}
if (processed == 0 && !stoppingToken.IsCancellationRequested)
{
try { await Task.Delay(_pollDelayMs, 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;
}