using Microsoft.EntityFrameworkCore;
using w4c_workflows.Data;
using w4c_workflows.Models;
using w4c_workflows.Services.Quota;
namespace w4c_workflows.Services.Runs;
/// Request to start a workflow run.
public sealed record LaunchRequest(
string TenantId,
Guid WorkflowId,
string? TriggerJson,
string? InputJson,
string? CorrelationId,
Guid? StartTaskId = null);
///
/// Starts a run for a compiled workflow by creating a pending
/// row. This is the seam between the trigger engine
/// (step 8 — decides WHEN to run) and the run lifecycle engine (step 9 — owns
/// dispatch via Redis, worker result consumption, chain advancement and durable
/// checkpoint/resume). The lifecycle engine picks up pending runs and
/// drives them to completion, so the trigger engine never touches the jobs
/// stream.
///
public interface IRunLauncher
{
Task LaunchAsync(LaunchRequest request, CancellationToken ct);
}
public class RunLauncher : IRunLauncher
{
private readonly WorkflowsDbContext _db;
private readonly WorkflowQuotaService _quota;
private readonly ILogger _logger;
public RunLauncher(WorkflowsDbContext db, WorkflowQuotaService quota, ILogger logger)
{
_db = db;
_quota = quota;
_logger = logger;
}
public async Task LaunchAsync(LaunchRequest request, CancellationToken ct)
{
var workflow = await _db.Workflows
.FirstOrDefaultAsync(w =>
w.Id == request.WorkflowId
&& w.TenantId == request.TenantId
&& w.Status == WorkflowStatus.Compiled, ct);
if (workflow == null)
throw new InvalidOperationException(
$"workflow {request.WorkflowId} not found or not compiled for tenant {request.TenantId}");
// Execution quota: only top-level runs are metered (sub-workflow children
// are created directly, not through the launcher). The reservation is an
// atomic increment, so a burst of concurrent triggers can never oversell.
if (!await _quota.TryReserveAsync(request.TenantId, ct))
throw new WorkflowQuotaExceededException(await _quota.GetAsync(request.TenantId, ct));
var run = new WorkflowRun
{
Id = Guid.NewGuid(),
WorkflowId = workflow.Id,
TenantId = request.TenantId,
Status = RunStatus.Pending,
TriggerJson = request.TriggerJson,
InputJson = request.InputJson,
CorrelationId = request.CorrelationId,
StartTaskId = request.StartTaskId,
};
_db.WorkflowRuns.Add(run);
await _db.SaveChangesAsync(ct);
_logger.LogDebug(
"Launched pending run {RunId} for workflow {WorkflowId} (tenant {TenantId}, correlation {CorrelationId})",
run.Id, workflow.Id, request.TenantId, request.CorrelationId);
return run.Id;
}
}