w4c-workflows-api/Services/Nodes/GraphRunMessage.cs
2026-09-13 11:35:17 +03:00

76 lines
3 KiB
C#

using System.Globalization;
using w4c_workflows.Models;
namespace w4c_workflows.Services.Nodes;
/// <summary>
/// Wire contract for a <c>graph.run</c> job. This is the node-kernel analogue of
/// <see cref="Execution.TaskInvocation"/>: instead of one script task, the job
/// asks a worker to run the whole persisted node graph of a run through
/// <see cref="NodeWorkflowRunner"/>.
///
/// It exists so node-mode workflows execute on the same worker/queue path as
/// script tasks (S2): the control plane enqueues one <c>graph.run</c> message per
/// run, a worker claims it, reconstructs the graph from the compiled entities and
/// owns the run's terminal status. That gives node runs the same leases,
/// timeouts and scale-out as script runs, instead of blocking the lifecycle
/// dispatch loop for the whole graph.
///
/// Field names are flat stream entries so the contract is transport-agnostic.
/// </summary>
public sealed record GraphRunInvocation(
string Type,
string RunId,
string TenantId,
string? WorkingDir,
int Attempt = 1)
{
public const string TypeValue = "graph.run";
/// <summary>Parses a stream message into an invocation, throwing if a required field is missing.</summary>
public static GraphRunInvocation FromFields(IReadOnlyDictionary<string, string> fields)
{
static string Get(IReadOnlyDictionary<string, string> fields, string key) =>
fields.TryGetValue(key, out var value) && !string.IsNullOrEmpty(value)
? value
: throw new InvalidOperationException($"missing required graph job field '{key}'");
static int GetInt(IReadOnlyDictionary<string, string> fields, string key, int fallback) =>
fields.TryGetValue(key, out var raw) && int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n)
? n
: fallback;
return new GraphRunInvocation(
Get(fields, "type"),
Get(fields, "run_id"),
Get(fields, "tenant_id"),
fields.GetValueOrDefault("working_dir"),
GetInt(fields, "attempt", 1));
}
}
/// <summary>
/// Serializes the control-plane side of a <c>graph.run</c> job. Only the run
/// reference and its working directory travel on the wire: the worker reloads
/// the compiled workflow, its task graph and input from the database, so the
/// message cannot drift from persisted state.
/// </summary>
public static class GraphRunMessage
{
public static IReadOnlyDictionary<string, string> ToFields(WorkflowRun run, string? workingDir, int attempt)
{
var fields = new Dictionary<string, string>
{
["type"] = GraphRunInvocation.TypeValue,
["run_id"] = run.Id.ToString(),
["tenant_id"] = run.TenantId,
["attempt"] = attempt.ToString(CultureInfo.InvariantCulture),
};
if (!string.IsNullOrWhiteSpace(workingDir))
fields["working_dir"] = workingDir;
return fields;
}
}