ec9011429c
Review of Task 3: the sink invoke sat before WriteAsync/ForwardAsync inside the best-effort try, so a throwing sink would have silently dropped the durable row. GuardTraceSink wraps the delegate in every helper ctor (and Collect guards EmitTrace); pinned by a throwing-sink test. ScriptRunLauncher no longer allocates a collector for a traced run with no reply target (Nobody) — the list would never be read. FlowTraceCollector.Complete pinned at unit level; Snapshot documented as a non-closing peek. Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
85 lines
3.5 KiB
C#
85 lines
3.5 KiB
C#
using ZB.MOM.WW.Audit;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
|
|
|
/// <summary>
|
|
/// Per-run sink for <c>ScriptTrace</c> audit events so a traced routed call can return them
|
|
/// inline on <c>ScriptCallResult.TraceEvents</c> (Flow Debugger design 2026-09-05 §5.4).
|
|
/// Lock-based: a run emits tens of events, not thousands. Beyond <see cref="DefaultMaxEvents"/>
|
|
/// the collector keeps the first N, sets <see cref="Truncated"/>, and the audit rows written
|
|
/// through <c>IAuditWriter</c> remain complete — Replay is never truncated, only the inline copy.
|
|
/// An untraced run holds <c>null</c> rather than an inert collector; callers null-check.
|
|
/// Outbound-calls addendum (plan 2026-09-06 D5): the <c>ApiOutbound</c> / <c>DbOutbound</c> /
|
|
/// <c>Notification</c> rows a run emits ride the same sink, so <see cref="Complete"/> closes it
|
|
/// when the launcher takes the reply snapshot — a lifecycle row the S&F forwarder writes
|
|
/// minutes later, or an un-awaited call finishing after the run returned, must not mutate a
|
|
/// list the reply has already been built from. <see cref="Add"/> is a no-op once completed.
|
|
/// </summary>
|
|
public sealed class FlowTraceCollector
|
|
{
|
|
/// <summary>Default inline cap per run (design §5.4 <c>MaxTraceEventsPerRun</c>).</summary>
|
|
public const int DefaultMaxEvents = 500;
|
|
|
|
private readonly List<AuditEvent> _events = new();
|
|
private readonly object _gate = new();
|
|
private readonly int _max;
|
|
|
|
/// <summary>True once an <see cref="Add"/> was dropped because the cap was reached.</summary>
|
|
public bool Truncated { get; private set; }
|
|
|
|
/// <summary>
|
|
/// True once <see cref="Complete"/> ran — the run's reply snapshot has been taken and
|
|
/// later <see cref="Add"/> calls are ignored (they never count as <see cref="Truncated"/>).
|
|
/// </summary>
|
|
public bool Completed { get; private set; }
|
|
|
|
/// <summary>Creates a collector with the given inline cap (must be positive).</summary>
|
|
public FlowTraceCollector(int maxEvents = DefaultMaxEvents)
|
|
{
|
|
if (maxEvents <= 0) throw new ArgumentOutOfRangeException(nameof(maxEvents), "must be positive");
|
|
_max = maxEvents;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Appends an event, or marks the collector truncated once the cap is reached. A no-op
|
|
/// after <see cref="Complete"/>.
|
|
/// </summary>
|
|
public void Add(AuditEvent evt)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(evt);
|
|
lock (_gate)
|
|
{
|
|
if (Completed) return;
|
|
if (_events.Count >= _max) { Truncated = true; return; }
|
|
_events.Add(evt);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Closes the collector and returns the final snapshot — the launcher calls this exactly
|
|
/// where it builds <c>ScriptCallResult.TraceEvents</c>. Idempotent: a second call returns
|
|
/// the same contents.
|
|
/// </summary>
|
|
public IReadOnlyList<AuditEvent> Complete()
|
|
{
|
|
lock (_gate)
|
|
{
|
|
Completed = true;
|
|
return _events.ToArray();
|
|
}
|
|
}
|
|
|
|
/// <summary>Current event count.</summary>
|
|
public int Count { get { lock (_gate) return _events.Count; } }
|
|
|
|
/// <summary>
|
|
/// Copies the collected events in emission order WITHOUT closing the collector — a diagnostic /
|
|
/// test-only peek. A reply path must use <see cref="Complete"/>, otherwise an un-awaited call
|
|
/// finishing after the reply could mutate the list the reply was built from.
|
|
/// </summary>
|
|
public IReadOnlyList<AuditEvent> Snapshot()
|
|
{
|
|
lock (_gate) return _events.ToArray();
|
|
}
|
|
}
|