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