using System.Globalization; using w4c_workflows.Models; namespace w4c_workflows.Services.Nodes; /// /// Wire contract for a graph.run job. This is the node-kernel analogue of /// : instead of one script task, the job /// asks a worker to run the whole persisted node graph of a run through /// . /// /// It exists so node-mode workflows execute on the same worker/queue path as /// script tasks (S2): the control plane enqueues one graph.run 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. /// public sealed record GraphRunInvocation( string Type, string RunId, string TenantId, string? WorkingDir, int Attempt = 1) { public const string TypeValue = "graph.run"; /// Parses a stream message into an invocation, throwing if a required field is missing. public static GraphRunInvocation FromFields(IReadOnlyDictionary fields) { static string Get(IReadOnlyDictionary 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 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)); } } /// /// Serializes the control-plane side of a graph.run 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. /// public static class GraphRunMessage { public static IReadOnlyDictionary ToFields(WorkflowRun run, string? workingDir, int attempt) { var fields = new Dictionary { ["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; } }