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