57 lines
2.3 KiB
C#
57 lines
2.3 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using w4c_workflows.Data;
|
|
using w4c_workflows.Models;
|
|
|
|
namespace w4c_workflows.Services.Execution;
|
|
|
|
/// <summary>
|
|
/// Writes a run's terminal status under a non-terminal precondition.
|
|
///
|
|
/// <para>
|
|
/// The control-plane timeout sweep (<c>RunLifecycleEngine.TimeoutStaleRunsAsync</c>)
|
|
/// fails a run that has been <c>running</c> past <c>Workflows:RunTimeoutSeconds</c>
|
|
/// while a worker may still be executing it. A plain tracked write from the worker
|
|
/// would then resurrect the failed run as <c>succeeded</c>. Applying the update
|
|
/// with <c>ExecuteUpdate</c> plus a <c>Status IN (pending, running, compensating)</c>
|
|
/// guard makes the write atomic: if the sweep won the race the update affects zero
|
|
/// rows and the worker leaves the terminal status alone.
|
|
/// </para>
|
|
/// </summary>
|
|
public static class RunTerminalWriter
|
|
{
|
|
/// <summary>
|
|
/// Atomically sets the run's terminal status/error/output when it is still
|
|
/// non-terminal. Returns <c>false</c> when another writer (e.g. the timeout
|
|
/// sweep) already terminalized the run, so the caller must not report success.
|
|
/// </summary>
|
|
public static async Task<bool> 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;
|
|
}
|
|
}
|