76 lines
3 KiB
C#
76 lines
3 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;
|
|
private readonly int _minTickDelayMs;
|
|
|
|
public RunLifecycleService(
|
|
IServiceScopeFactory scopeFactory,
|
|
RunLifecycleEngine engine,
|
|
IConfiguration config,
|
|
ILogger<RunLifecycleService> 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<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");
|
|
}
|
|
|
|
// 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;
|
|
}
|