79 lines
2.9 KiB
C#
79 lines
2.9 KiB
C#
using w4c_workflows.Models.Nodes;
|
|
|
|
namespace w4c_workflows.Services.Nodes;
|
|
|
|
/// <summary>
|
|
/// Loop support for the node graph. An edge that closes a cycle is a
|
|
/// <b>loop-back</b> edge: it must target a node whose blueprint declares
|
|
/// <see cref="NodeBlueprint.LoopBack"/> (currently <c>core.splitInBatches</c>),
|
|
/// and at run time it re-triggers that node for the next iteration instead of
|
|
/// counting toward its readiness.
|
|
///
|
|
/// The classification is derived from the edge set (a DFS back edge), so it is
|
|
/// recomputed identically by the compiler and when a persisted graph is rebuilt.
|
|
/// </summary>
|
|
public static class NodeGraphLinks
|
|
{
|
|
/// <summary>
|
|
/// Returns the edges with <see cref="NodeGraphEdge.IsLoopBack"/> set, or an
|
|
/// error when a cycle passes through a node that is not loop-capable.
|
|
/// </summary>
|
|
public static (List<NodeGraphEdge> Edges, string? Error) MarkLoopBackEdges(
|
|
IReadOnlyList<NodeGraphNode> nodes, IReadOnlyList<NodeGraphEdge> edges)
|
|
{
|
|
var byId = nodes.ToDictionary(n => n.Id, StringComparer.Ordinal);
|
|
|
|
var adjacency = new Dictionary<string, List<int>>(StringComparer.Ordinal);
|
|
for (var i = 0; i < edges.Count; i++)
|
|
{
|
|
if (!adjacency.TryGetValue(edges[i].FromNodeId, out var outgoing))
|
|
adjacency[edges[i].FromNodeId] = outgoing = new List<int>();
|
|
outgoing.Add(i);
|
|
}
|
|
|
|
var color = new Dictionary<string, int>(StringComparer.Ordinal); // 0 = white, 1 = on stack, 2 = done
|
|
var backEdges = new HashSet<int>();
|
|
|
|
void Visit(string nodeId)
|
|
{
|
|
color[nodeId] = 1;
|
|
if (adjacency.TryGetValue(nodeId, out var outgoing))
|
|
{
|
|
foreach (var index in outgoing)
|
|
{
|
|
var target = edges[index].ToNodeId;
|
|
var targetColor = color.GetValueOrDefault(target, 0);
|
|
if (targetColor == 1)
|
|
backEdges.Add(index);
|
|
else if (targetColor == 0)
|
|
Visit(target);
|
|
}
|
|
}
|
|
color[nodeId] = 2;
|
|
}
|
|
|
|
foreach (var node in nodes)
|
|
{
|
|
if (color.GetValueOrDefault(node.Id, 0) == 0)
|
|
Visit(node.Id);
|
|
}
|
|
|
|
var marked = new List<NodeGraphEdge>(edges.Count);
|
|
string? error = null;
|
|
for (var i = 0; i < edges.Count; i++)
|
|
{
|
|
var edge = edges[i];
|
|
var isLoopBack = backEdges.Contains(i);
|
|
if (isLoopBack && !byId[edge.ToNodeId].Blueprint.LoopBack)
|
|
{
|
|
error ??=
|
|
$"a cycle through '{edge.FromNodeId}' → '{edge.ToNodeId}' is not allowed: " +
|
|
$"'{byId[edge.ToNodeId].Blueprint.Type}' is not a loop node";
|
|
}
|
|
marked.Add(edge with { IsLoopBack = isLoopBack });
|
|
}
|
|
|
|
return (marked, error);
|
|
}
|
|
}
|