116 lines
4.3 KiB
C#
116 lines
4.3 KiB
C#
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;
|
|
}
|
|
}
|