using Microsoft.EntityFrameworkCore; using w4c_workflows.Data; using w4c_workflows.Models; namespace w4c_workflows.Services.Execution; /// /// Writes a run's terminal status under a non-terminal precondition. /// /// /// The control-plane timeout sweep (RunLifecycleEngine.TimeoutStaleRunsAsync) /// fails a run that has been running past Workflows:RunTimeoutSeconds /// while a worker may still be executing it. A plain tracked write from the worker /// would then resurrect the failed run as succeeded. Applying the update /// with ExecuteUpdate plus a Status IN (pending, running, compensating) /// guard makes the write atomic: if the sweep won the race the update affects zero /// rows and the worker leaves the terminal status alone. /// /// public static class RunTerminalWriter { /// /// Atomically sets the run's terminal status/error/output when it is still /// non-terminal. Returns false when another writer (e.g. the timeout /// sweep) already terminalized the run, so the caller must not report success. /// public static async Task TrySetTerminalAsync( WorkflowsDbContext db, WorkflowRun run, string status, string? error, string? output, CancellationToken ct) { var now = DateTime.UtcNow; var affected = await db.WorkflowRuns .Where(r => r.Id == run.Id && (r.Status == RunStatus.Pending || r.Status == RunStatus.Running || r.Status == RunStatus.Compensating)) .ExecuteUpdateAsync(setters => setters .SetProperty(r => r.Status, status) .SetProperty(r => r.Error, error) .SetProperty(r => r.OutputJson, output) .SetProperty(r => r.FinishedAt, now), ct); // Refresh the tracked instance from the row we just wrote (or from the // terminal state another writer already set), so in-memory callers see the // truth and a later SaveChanges cannot flush the status unguarded. var entry = db.Entry(run); if (entry.State != EntityState.Detached) await entry.ReloadAsync(ct); return affected > 0; } }