w4c-workflows-api/Services/Runs/RunLifecycleService.cs
Vitali sharp8n 42ffcb9adc workflows
2026-09-13 19:28:47 +03:00

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