w4c-workflows-api/Services/Nodes/NodeGraphRunner.Routing.cs

116 lines
4.3 KiB
C#
Raw Normal View History

2026-09-13 16:28:47 +00:00
using System.Text.Json.Nodes;
using w4c_workflows.Models.Credentials;
using w4c_workflows.Models.Nodes;
using w4c_workflows.Services.Nodes.Interpolation;
using w4c_workflows.Services.Security;
namespace w4c_workflows.Services.Nodes;
// Routing half of the graph runner: forwarding produced items along outgoing
// edges (readiness accounting incl. loop-back edges), emitting listener events
// and shaping an error-port payload when a failure is routed.
public sealed partial class NodeGraphRunner
{
// ------------------------------------------------------------------ routing
private static void Propagate(
NodeGraph graph,
string nodeId,
IReadOnlyList<IReadOnlyList<FlowItem>> ports,
IReadOnlyDictionary<string, List<FlowItem>[]> inputs,
IDictionary<string, int> arrived,
IReadOnlyDictionary<string, int> incoming,
Queue<string> ready,
ISet<string> queued,
ISet<string> exhausted,
bool skipEmptyPorts)
{
for (var port = 0; port < ports.Count; port++)
{
var produced = ports[port];
// On a loop node's final ("done") invocation the loop port is empty:
// skip it so the body is not re-triggered for a no-op iteration.
if (skipEmptyPorts && produced.Count == 0)
continue;
foreach (var edge in graph.EdgesFrom(nodeId, port))
{
// An exhausted loop node is not re-triggered by its loop-back edge.
if (exhausted.Contains(edge.ToNodeId) && edge.IsLoopBack)
continue;
var targetInputs = inputs[edge.ToNodeId];
if (edge.ToInput >= 0 && edge.ToInput < targetInputs.Length)
targetInputs[edge.ToInput].AddRange(produced);
arrived[edge.ToNodeId]++;
// Max(incoming, 1): a node with no non-loop-back predecessor (an
// entry loop node) must still be re-triggered by its loop-back
// edge. The queued guard prevents a double enqueue when several
// inputs arrive before the node next runs.
if (arrived[edge.ToNodeId] >= Math.Max(incoming[edge.ToNodeId], 1)
&& queued.Add(edge.ToNodeId))
ready.Enqueue(edge.ToNodeId);
}
}
}
// ------------------------------------------------------------------ listeners
private static Task NotifyStarted(
INodeRunListener? listener,
NodeGraphNode node,
IReadOnlyList<IReadOnlyList<FlowItem>> inputs,
CancellationToken ct)
=> listener == null ? Task.CompletedTask : listener.NodeStartedAsync(node, inputs, ct);
private static Task NotifyFinished(
INodeRunListener? listener,
NodeGraphNode node,
IReadOnlyList<IReadOnlyList<FlowItem>> outputs,
NodeFailure? failure,
CancellationToken ct)
=> listener == null ? Task.CompletedTask : listener.NodeFinishedAsync(node, outputs, failure, ct);
private List<IReadOnlyList<FlowItem>> BuildErrorOutputs(
NodeGraphNode node,
IReadOnlyList<IReadOnlyList<FlowItem>> inputs,
NodeFailure failure,
int errorPort)
{
var portCount = Math.Max(1, node.Blueprint.Outputs.Count);
var ports = Enumerable.Range(0, portCount).Select(_ => (IReadOnlyList<FlowItem>)new List<FlowItem>()).ToList();
if (errorPort < 0 || errorPort >= ports.Count)
return ports;
var errorJson = new JsonObject
{
["message"] = failure.Message,
["code"] = failure.Code,
["description"] = failure.Description,
["httpStatus"] = failure.HttpStatus,
};
var source = inputs.FirstOrDefault(list => list.Count > 0);
var items = new List<FlowItem>();
if (source == null || source.Count == 0)
{
items.Add(new FlowItem { Json = new JsonObject { ["error"] = errorJson } });
}
else
{
foreach (var item in source)
{
var json = (JsonObject)item.Json.DeepClone();
json["error"] = errorJson.DeepClone();
items.Add(new FlowItem { Json = json, Binary = item.Binary, Origin = item.Origin });
}
}
ports[errorPort] = items;
return ports;
}
}