76 lines
3 KiB
C#
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;
|
|
}
|
|
}
|