Resolve all 46 low-severity architecture review findings
This commit is contained in:
@@ -168,8 +168,8 @@ public interface IAuditLogRepository
|
||||
/// <para>
|
||||
/// <b>Bounded + idempotent.</b> Each batch is a <c>DELETE TOP (@batch)</c> so the
|
||||
/// transaction log and lock footprint stay bounded regardless of backlog. Re-running
|
||||
/// the purge is a no-op once every eligible row is gone (the loop exits when a batch
|
||||
/// deletes zero rows), so a crash mid-loop is recoverable by simply running again.
|
||||
/// the purge resumes remaining backlog after reaching <paramref name="maxRows"/>;
|
||||
/// an empty batch ends it sooner. A crash mid-loop is recoverable by running again.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="channel">Canonical channel name (the <c>Category</c> column value, e.g. <c>ApiOutbound</c>).</param>
|
||||
@@ -183,13 +183,14 @@ public interface IAuditLogRepository
|
||||
/// by the ~30s ADO.NET default (arch-review 04, S2).
|
||||
/// </param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <param name="maxRows">Maximum total rows deleted in this call; remaining backlog is retained for a later tick.</param>
|
||||
/// <returns>A task that resolves to the total number of rows deleted across all batches.</returns>
|
||||
Task<long> PurgeChannelOlderThanAsync(
|
||||
string channel,
|
||||
DateTime threshold,
|
||||
int batchSize,
|
||||
TimeSpan? commandTimeout = null,
|
||||
CancellationToken ct = default);
|
||||
CancellationToken ct = default, int maxRows = 50_000);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the set of <c>pf_AuditLog_Month</c> partition lower-bound
|
||||
|
||||
@@ -250,6 +250,10 @@ public interface ITemplateEngineRepository
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A task that resolves to a read-only list of instances for the specified site.</returns>
|
||||
Task<IReadOnlyList<Instance>> GetInstancesBySiteIdAsync(int siteId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Reads one bounded, untracked page of topology instance rows for a site/area.</summary>
|
||||
Task<IReadOnlyList<Instance>> GetTopologyInstancesAsync(int siteId, int? areaId, int skip, int take, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Retrieves an instance by unique name.</summary>
|
||||
/// <param name="uniqueName">The unique instance name.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
|
||||
@@ -17,11 +17,12 @@ public interface IPullTraceEventsClient
|
||||
/// <param name="sinceUtc">Lower bound on <c>OccurredAtUtc</c> (the run's start is the natural choice).</param>
|
||||
/// <param name="maxEvents">Row cap (the site caps at 2000).</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <param name="afterEventId">Optional exclusive EventId at sinceUtc for stable keyset paging.</param>
|
||||
/// <returns>The matching rows ordered <c>(OccurredAtUtc, EventId)</c>; empty on fault or unknown site.</returns>
|
||||
Task<IReadOnlyList<AuditEvent>> PullAsync(
|
||||
string siteId,
|
||||
IReadOnlyCollection<Guid> executionIds,
|
||||
DateTime sinceUtc,
|
||||
int maxEvents,
|
||||
CancellationToken ct);
|
||||
CancellationToken ct, string? afterEventId = null);
|
||||
}
|
||||
|
||||
@@ -232,5 +232,5 @@ public interface ISiteAuditQueue
|
||||
/// forwarding state — moves no cursor and flips nothing. At most 64 ids per call.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<AuditEvent>> ReadByExecutionIdsAsync(
|
||||
IReadOnlyCollection<Guid> executionIds, DateTime sinceUtc, int limit, CancellationToken ct = default);
|
||||
IReadOnlyCollection<Guid> executionIds, DateTime sinceUtc, int limit, CancellationToken ct = default, string? afterEventId = null);
|
||||
}
|
||||
|
||||
@@ -12,13 +12,10 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
|
||||
/// <param name="Quality">Quality of the new value (<c>Good</c>/<c>Bad</c>/<c>Uncertain</c>).</param>
|
||||
/// <param name="Timestamp">UTC timestamp of the change.</param>
|
||||
/// <param name="SourceExecutionId">
|
||||
/// Audit Log #23 (ParentExecutionId tag-cascade): the <c>ExecutionId</c> of the
|
||||
/// script execution / inbound API request whose write produced this change, or
|
||||
/// <c>null</c> when the change did not originate from an audited execution —
|
||||
/// which is the case for every value that arrives from the Data Connection
|
||||
/// Layer (external device data), for deployment-time seeding, and for the
|
||||
/// confirmed value of a data-sourced write (the device echo arrives on the
|
||||
/// subscription long after the writing execution ended).
|
||||
/// Execution associated with this change. Static writes carry an observed writer id;
|
||||
/// armed device updates carry only a candidate writer id (see
|
||||
/// <paramref name="SourceExecutionInferred"/>), since subscriptions carry no write-request id.
|
||||
/// Other device updates and deployment seeding carry no id.
|
||||
///
|
||||
/// <para>
|
||||
/// The Alarm Actor records this id as the <c>ParentExecutionId</c> of the
|
||||
@@ -34,6 +31,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
|
||||
/// </para>
|
||||
/// </param>
|
||||
/// <param name="Trace">Flow Debugger trace flag — see design 2026-09-05 §5.2. Additive; defaults to untraced.</param>
|
||||
/// <param name="SourceExecutionInferred">True when the source is inferred from the first device update within the trace-arm window, regardless of value equality, clamping, or coercion; it is not evidence of a write echo.</param>
|
||||
public record AttributeValueChanged(
|
||||
string InstanceUniqueName,
|
||||
string AttributePath,
|
||||
@@ -42,4 +40,5 @@ public record AttributeValueChanged(
|
||||
string Quality,
|
||||
DateTimeOffset Timestamp,
|
||||
Guid? SourceExecutionId = null,
|
||||
bool Trace = false) : ISiteStreamEvent;
|
||||
bool Trace = false,
|
||||
bool SourceExecutionInferred = false) : ISiteStreamEvent;
|
||||
|
||||
@@ -66,16 +66,17 @@ public static class FlowGraphBuilder
|
||||
// D2: a ScriptLog row is a line, never a node. Partition BEFORE the input-order index below, so
|
||||
// `seq` indexes only real step rows and a line can never shift an execution's display position.
|
||||
var lines = new List<FlowLogLine>();
|
||||
var stepEvents = new List<AuditEvent>(events.Count);
|
||||
var stepEvents = new List<(AuditEvent Event, AuditDetails Details)>(events.Count);
|
||||
foreach (var e in events)
|
||||
{
|
||||
if (FlowStepProjector.IsLogLine(e))
|
||||
lines.Add(FlowStepProjector.ToLogLine(e));
|
||||
var details = AuditDetailsCodec.Deserialize(e.DetailsJson);
|
||||
if (FlowStepProjector.IsLogLine(details))
|
||||
lines.Add(FlowStepProjector.ToLogLine(e, details));
|
||||
else
|
||||
stepEvents.Add(e);
|
||||
stepEvents.Add((e, details));
|
||||
}
|
||||
|
||||
events = stepEvents;
|
||||
events = stepEvents.Select(e => e.Event).ToList();
|
||||
|
||||
if (events.Count == 0)
|
||||
return new FlowGraph(Array.Empty<FlowStep>(), Array.Empty<FlowEdge>(), Predicted: false, Truncated: truncated,
|
||||
@@ -105,7 +106,7 @@ public static class FlowGraphBuilder
|
||||
return index;
|
||||
}
|
||||
|
||||
var folded = Fold(events).ToList();
|
||||
var folded = Fold(stepEvents).ToList();
|
||||
var firstSeqByExecution = folded
|
||||
.GroupBy(s => s.ExecutionId ?? Guid.Empty)
|
||||
.ToDictionary(g => g.Key, g => g.Min(InputIndex));
|
||||
@@ -205,7 +206,7 @@ public static class FlowGraphBuilder
|
||||
|
||||
var firstChained = chain.FirstOrDefault(s => !IsAsync(s));
|
||||
if (firstChained is not null && spawner.EventId != firstChained.EventId)
|
||||
edges.Add(new FlowEdge(spawner.EventId, firstChained.EventId, CrossesBoundary(spawner, firstChained), External: false, Spawn: spawn));
|
||||
edges.Add(new FlowEdge(spawner.EventId, firstChained.EventId, CrossesBoundary(spawner, firstChained), External: spawn && first.Extra?.TriggerSource == "inferred-device-update", Spawn: spawn));
|
||||
}
|
||||
|
||||
// 3. Satisfied wait ⇐ preceding write in the same execution (inferred device echo).
|
||||
@@ -219,6 +220,7 @@ public static class FlowGraphBuilder
|
||||
for (var j = i - 1; j >= 0; j--)
|
||||
{
|
||||
if (chain[j].Kind != FlowStepKind.AttributeWrite) continue;
|
||||
edges.RemoveAll(e => e.FromEventId == chain[j].EventId && e.ToEventId == wait.EventId);
|
||||
edges.Add(new FlowEdge(chain[j].EventId, wait.EventId, CrossesBoundary: false, External: true));
|
||||
break;
|
||||
}
|
||||
@@ -376,14 +378,14 @@ public static class FlowGraphBuilder
|
||||
/// Projects every event and folds the outbound rows of one operation (same kind + <c>CorrelationId</c>) into one step
|
||||
/// carrying the whole lifecycle. Non-outbound rows and null-correlation outbound rows pass through one-to-one.
|
||||
/// </summary>
|
||||
private static IEnumerable<FlowStep> Fold(IReadOnlyList<AuditEvent> events)
|
||||
private static IEnumerable<FlowStep> Fold(IReadOnlyList<(AuditEvent Event, AuditDetails Details)> events)
|
||||
{
|
||||
var groups = new Dictionary<(FlowStepKind Kind, Guid Correlation), List<(FlowStep Step, AuditEvent Evt)>>();
|
||||
var groups = new Dictionary<(FlowStepKind Kind, Guid Correlation), List<(FlowStep Step, FlowLifecycleRow Row)>>();
|
||||
var order = new List<object>(events.Count); // FlowStep, or the group key — preserves first-seen order
|
||||
|
||||
foreach (var evt in events)
|
||||
foreach (var (evt, details) in events)
|
||||
{
|
||||
var step = FlowStepProjector.Project(evt);
|
||||
var step = FlowStepProjector.Project(evt, details);
|
||||
if (!FlowStepProjector.IsOutbound(step.Kind))
|
||||
{
|
||||
order.Add(step);
|
||||
@@ -392,7 +394,7 @@ public static class FlowGraphBuilder
|
||||
|
||||
if (evt.CorrelationId is not { } correlation)
|
||||
{
|
||||
order.Add(step with { Lifecycle = [FlowStepProjector.ToLifecycleRow(evt)] });
|
||||
order.Add(step with { Lifecycle = [FlowStepProjector.ToLifecycleRow(evt, details)] });
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -403,7 +405,7 @@ public static class FlowGraphBuilder
|
||||
groups[key] = group;
|
||||
order.Add(key);
|
||||
}
|
||||
group.Add((step, evt));
|
||||
group.Add((step, FlowStepProjector.ToLifecycleRow(evt, details)));
|
||||
}
|
||||
|
||||
foreach (var item in order)
|
||||
@@ -416,7 +418,6 @@ public static class FlowGraphBuilder
|
||||
|
||||
var key = ((FlowStepKind, Guid))item;
|
||||
var rows = groups[key]
|
||||
.Select(r => (r.Step, Row: FlowStepProjector.ToLifecycleRow(r.Evt)))
|
||||
.OrderBy(r => r.Row.OccurredAtUtc)
|
||||
.ThenBy(r => LifecycleRank(r.Row.Kind, r.Row.Status))
|
||||
.ThenBy(r => r.Row.EventId)
|
||||
@@ -435,6 +436,7 @@ public static class FlowGraphBuilder
|
||||
Subtitle = latest.Subtitle,
|
||||
OutputJson = latest.OutputJson,
|
||||
ErrorMessage = latest.ErrorMessage,
|
||||
ErrorDetail = latest.ErrorDetail,
|
||||
DurationMs = rows.Count == 1 && !terminal ? null : duration,
|
||||
Lifecycle = lifecycle,
|
||||
};
|
||||
|
||||
@@ -65,6 +65,7 @@ public enum FlowStepOutcome
|
||||
/// <param name="Node">Node that wrote it.</param>
|
||||
/// <param name="ErrorMessage">Error summary on a failed attempt.</param>
|
||||
/// <param name="DurationMs">The row's own duration, when it has one.</param>
|
||||
/// <param name="ErrorDetail">Exception detail from this lifecycle row.</param>
|
||||
public sealed record FlowLifecycleRow(
|
||||
Guid EventId,
|
||||
string Kind,
|
||||
@@ -72,7 +73,8 @@ public sealed record FlowLifecycleRow(
|
||||
DateTimeOffset OccurredAtUtc,
|
||||
string? Node,
|
||||
string? ErrorMessage,
|
||||
int? DurationMs);
|
||||
int? DurationMs,
|
||||
string? ErrorDetail = null);
|
||||
|
||||
/// <summary>
|
||||
/// One log line folded onto a step (<see cref="FlowStep.Logs"/>; design 2026-09-09 D1/D8) — the
|
||||
|
||||
@@ -17,7 +17,11 @@ public static class FlowStepProjector
|
||||
public static FlowStep Project(AuditEvent evt)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(evt);
|
||||
var d = AuditDetailsCodec.Deserialize(evt.DetailsJson);
|
||||
return Project(evt, AuditDetailsCodec.Deserialize(evt.DetailsJson));
|
||||
}
|
||||
|
||||
internal static FlowStep Project(AuditEvent evt, AuditDetails d)
|
||||
{
|
||||
var extra = ScriptTraceExtra.TryParse(d.Extra);
|
||||
var kind = ResolveKind(d.Channel, d.Kind);
|
||||
var lane = ResolveLane(kind, d.SourceSiteId);
|
||||
@@ -52,6 +56,11 @@ public static class FlowStepProjector
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(evt);
|
||||
var d = AuditDetailsCodec.Deserialize(evt.DetailsJson);
|
||||
return IsLogLine(d);
|
||||
}
|
||||
|
||||
internal static bool IsLogLine(AuditDetails d)
|
||||
{
|
||||
return string.Equals(d.Channel, nameof(AuditChannel.ScriptTrace), StringComparison.Ordinal)
|
||||
&& string.Equals(d.Kind, nameof(AuditKind.ScriptLog), StringComparison.Ordinal);
|
||||
}
|
||||
@@ -60,7 +69,11 @@ public static class FlowStepProjector
|
||||
public static FlowLogLine ToLogLine(AuditEvent evt)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(evt);
|
||||
var d = AuditDetailsCodec.Deserialize(evt.DetailsJson);
|
||||
return ToLogLine(evt, AuditDetailsCodec.Deserialize(evt.DetailsJson));
|
||||
}
|
||||
|
||||
internal static FlowLogLine ToLogLine(AuditEvent evt, AuditDetails d)
|
||||
{
|
||||
var x = ScriptTraceExtra.TryParse(d.Extra);
|
||||
return new FlowLogLine(
|
||||
EventId: evt.EventId,
|
||||
@@ -79,7 +92,11 @@ public static class FlowStepProjector
|
||||
public static FlowLifecycleRow ToLifecycleRow(AuditEvent evt)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(evt);
|
||||
var d = AuditDetailsCodec.Deserialize(evt.DetailsJson);
|
||||
return ToLifecycleRow(evt, AuditDetailsCodec.Deserialize(evt.DetailsJson));
|
||||
}
|
||||
|
||||
internal static FlowLifecycleRow ToLifecycleRow(AuditEvent evt, AuditDetails d)
|
||||
{
|
||||
return new FlowLifecycleRow(
|
||||
EventId: evt.EventId,
|
||||
Kind: d.Kind ?? string.Empty,
|
||||
@@ -87,7 +104,8 @@ public static class FlowStepProjector
|
||||
OccurredAtUtc: evt.OccurredAtUtc,
|
||||
Node: evt.SourceNode,
|
||||
ErrorMessage: d.ErrorMessage,
|
||||
DurationMs: d.DurationMs);
|
||||
DurationMs: d.DurationMs,
|
||||
ErrorDetail: d.ErrorDetail);
|
||||
}
|
||||
|
||||
/// <summary>Maps a channel/kind string pair onto a step kind.</summary>
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed record ScriptTraceExtra
|
||||
/// <summary>Encoded value that fired a value-change run.</summary>
|
||||
public string? TriggerValue { get; init; }
|
||||
|
||||
/// <summary>Where the triggering change came from: <c>value-change</c> (the actor cannot tell a static write from a device echo).</summary>
|
||||
/// <summary>Where the triggering change came from: <c>value-change</c> for an observed write, or <c>inferred-device-update</c> for the first device publish in a trace-arm window (not a confirmed echo).</summary>
|
||||
public string? TriggerSource { get; init; }
|
||||
|
||||
/// <summary>On a <c>RouteCall</c> row: the routed site run's <c>ExecutionId</c>.</summary>
|
||||
|
||||
Reference in New Issue
Block a user