Merge branch 'worktree-agent-aa79b4c585b494d98' into fix/flow-debugger-arch-review
This commit is contained in:
@@ -16,14 +16,17 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging;
|
||||
/// takes the first row's identity, time, input and title, the latest row's outcome, subtitle, output and error;
|
||||
/// <c>DurationMs</c> is the sync row's own, or submit→terminal wall time for a cached operation (null while pending);
|
||||
/// <c>Lifecycle</c> lists every row in time order.</item>
|
||||
/// <item>Steps are ordered by <c>OccurredAtUtc</c>, then a kind rank (inbound before route call
|
||||
/// before script run before writes/waits before outbound), then <c>EventId</c>.</item>
|
||||
/// <item><b>Order</b> = emission order between executions, timestamp within one (arch-review F5: central and
|
||||
/// site clocks differ). Executions are ordered by the input index of their earliest-given row; inside one
|
||||
/// execution — one clock — steps are ordered by <c>OccurredAtUtc</c>, then a kind rank (inbound before route
|
||||
/// call before script run before writes/waits before outbound), then the input index. Replay input arrives
|
||||
/// timestamp-sorted from the repository, so its order is unchanged; the Live path's collector order wins.</item>
|
||||
/// <item>Within one <c>ExecutionId</c> consecutive steps are chained in that order, <b>skipping async steps</b> — a
|
||||
/// <c>Notify</c>, or a cached operation whose first row is <c>CachedSubmit</c>. Each skipped step instead gets an
|
||||
/// <c>Async</c> (dashed) edge from the chain step immediately before it in time.</item>
|
||||
/// <item>The first step of an execution is linked from its spawner: a <c>RouteCall</c> whose
|
||||
/// <c>childExecutionId</c> matches, else the latest step of the parent execution that occurred
|
||||
/// no later than it — that second case is a <c>Spawn</c> edge (trigger cascade). The edge
|
||||
/// <c>childExecutionId</c> matches, else the last step of the parent execution that precedes it in
|
||||
/// the order above — that second case is a <c>Spawn</c> edge (trigger cascade). The edge
|
||||
/// <c>CrossesBoundary</c> when exactly one end is in the Central lane.</item>
|
||||
/// <item>An <c>AttributeWait</c> that was satisfied gets an <c>External</c> (dashed) edge from the
|
||||
/// preceding <c>AttributeWrite</c> in the same execution — the device echo the system inferred.</item>
|
||||
@@ -41,12 +44,47 @@ public static class FlowGraphBuilder
|
||||
if (events.Count == 0)
|
||||
return new FlowGraph(Array.Empty<FlowStep>(), Array.Empty<FlowEdge>(), Predicted: false, Truncated: truncated);
|
||||
|
||||
var steps = Fold(events)
|
||||
.OrderBy(s => s.OccurredAtUtc)
|
||||
// Arch-review F5: a step's position in the input is the only cross-node ordering we can trust —
|
||||
// central rows carry central's clock, site rows the site's, and a 1-2 s skew used to sort a whole
|
||||
// site execution ahead of the inbound row that started it.
|
||||
var seq = new Dictionary<Guid, int>(events.Count);
|
||||
for (var i = 0; i < events.Count; i++)
|
||||
seq.TryAdd(events[i].EventId, i);
|
||||
|
||||
// A folded step keeps the identity of its earliest-by-TIME row, which need not be the row given
|
||||
// first — so a folded node's index is the minimum over the rows folded into it.
|
||||
int InputIndex(FlowStep step)
|
||||
{
|
||||
var index = seq.TryGetValue(step.EventId, out var own) ? own : int.MaxValue;
|
||||
if (step.Lifecycle is { Count: > 1 } rows)
|
||||
{
|
||||
foreach (var row in rows)
|
||||
{
|
||||
if (seq.TryGetValue(row.EventId, out var i) && i < index)
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
var folded = Fold(events).ToList();
|
||||
var firstSeqByExecution = folded
|
||||
.GroupBy(s => s.ExecutionId ?? Guid.Empty)
|
||||
.ToDictionary(g => g.Key, g => g.Min(InputIndex));
|
||||
var steps = folded
|
||||
.OrderBy(s => firstSeqByExecution[s.ExecutionId ?? Guid.Empty]) // executions in emission order
|
||||
.ThenBy(s => s.OccurredAtUtc) // one clock inside an execution
|
||||
.ThenBy(s => KindRank(s.Kind))
|
||||
.ThenBy(s => s.EventId)
|
||||
.ThenBy(InputIndex)
|
||||
.ToList();
|
||||
|
||||
// Position in the ordered list — the "before" relation the spawner search uses, so a skewed
|
||||
// clock can no longer push every candidate parent step "after" the child.
|
||||
var position = new Dictionary<Guid, int>(steps.Count);
|
||||
for (var i = 0; i < steps.Count; i++)
|
||||
position[steps[i].EventId] = i;
|
||||
|
||||
var edges = new List<FlowEdge>();
|
||||
var byExecution = steps
|
||||
.Where(s => s.ExecutionId is not null)
|
||||
@@ -93,8 +131,12 @@ public static class FlowGraphBuilder
|
||||
if (routeCallsByChild.TryGetValue(executionId, out var routeCall))
|
||||
spawner = routeCall;
|
||||
else if (byExecution.TryGetValue(parentId, out var parentChain))
|
||||
spawner = parentChain.LastOrDefault(p => p.OccurredAtUtc <= first.OccurredAtUtc && p.Kind != FlowStepKind.Response && !IsAsync(p))
|
||||
{
|
||||
// The parent step that precedes the child in DISPLAY order, not by raw timestamp (F5).
|
||||
var childPosition = position[first.EventId];
|
||||
spawner = parentChain.LastOrDefault(p => position[p.EventId] < childPosition && p.Kind != FlowStepKind.Response && !IsAsync(p))
|
||||
?? parentChain[0];
|
||||
}
|
||||
|
||||
if (spawner is null) continue;
|
||||
var spawn = spawner.Kind != FlowStepKind.RouteCall;
|
||||
|
||||
@@ -173,8 +173,12 @@ public sealed class FlowDebugRunner
|
||||
result.ErrorMessage ?? "Access denied: routed call outside your site scope.");
|
||||
}
|
||||
|
||||
// Arch-review F5: the collector's emission order is authoritative — central rows carry central's
|
||||
// clock and site rows the site's, so re-sorting by OccurredAtUtc lets a skewed site clock float
|
||||
// its steps ahead of the RouteCall that started them. (The value-change path below keeps its
|
||||
// OrderBy: every row it merges was stamped by the same site.)
|
||||
var ordered = new List<AuditEvent>(events.Count + 1) { inboundRow };
|
||||
ordered.AddRange(events.OrderBy(e => e.OccurredAtUtc).ThenBy(e => e.EventId));
|
||||
ordered.AddRange(events);
|
||||
|
||||
return new FlowDebugRunResult(
|
||||
root, result.Success, result.ErrorMessage, result.Success ? null : result.ErrorCode,
|
||||
|
||||
Reference in New Issue
Block a user