feat(site-runtime): outbound API/DB/notification audit rows ride the flow-trace collector
ScriptRuntimeContext caches a trace sink (null on an untraced run, so the production path is byte-identical) and threads it into the ExternalSystem / Database / Notify helpers and AuditingDbConnection→AuditingDbCommand (onAudit). All eight outbound emit sites hand the SAME AuditEvent instance to the sink first, then to the writer/forwarder. FlowTraceCollector gains Completed + Complete(): ScriptRunLauncher closes the collector where it builds the reply snapshot, so un-awaited calls finishing after the run returned never mutate a list the reply was built from. Cap (500) unchanged. Plan 2026-09-06 D5. Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
@@ -9,6 +9,11 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
/// 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
|
||||
{
|
||||
@@ -22,6 +27,12 @@ public sealed class FlowTraceCollector
|
||||
/// <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)
|
||||
{
|
||||
@@ -29,17 +40,35 @@ public sealed class FlowTraceCollector
|
||||
_max = maxEvents;
|
||||
}
|
||||
|
||||
/// <summary>Appends an event, or marks the collector truncated once the cap is reached.</summary>
|
||||
/// <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; } }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user