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