The store-and-forward retry loop emits the per-attempt and terminal cached audit rows (ApiCallCached/DbWriteCached Attempted, CachedResolve) via CachedCallLifecycleBridge from a CachedCallAttemptContext, not from the script context. The ExecutionId rollout (Task 4) already threaded ExecutionId and SourceScript through this path; ParentExecutionId — the spawning inbound-API request's ExecutionId — was not, so those retry-loop rows had ParentExecutionId = null even for an inbound-API-routed run. Thread it additively as a sibling at every carry point ExecutionId passes through: - StoreAndForwardMessage gains ParentExecutionId (Guid?). - StoreAndForwardStorage adds a nullable parent_execution_id column via the same idempotent PRAGMA-probed ALTER TABLE migration; rows persisted by an older build read back null (back-compat). The defensive Guid.TryParse read helper (ParseExecutionId) is renamed ParseGuidColumn and reused for both columns so a corrupt value cannot abort the retry sweep. - StoreAndForwardService.EnqueueAsync gains an optional parentExecutionId param, stamped onto the buffered message and surfaced on the CachedCallAttemptContext built in the retry loop. - CachedCallAttemptContext gains ParentExecutionId. - CachedCallLifecycleBridge.BuildPacket sets AuditEvent.ParentExecutionId from the context, beside the existing ExecutionId. - IExternalSystemClient.CachedCallAsync / IDatabaseGateway.CachedWriteAsync gain an optional parentExecutionId param; ScriptRuntimeContext's CachedCall / CachedWrite helpers pass _parentExecutionId. All threading is additive — ParentExecutionId is Guid? everywhere, null for non-routed runs, and old buffered S&F rows still deserialize with the new field null.
121 lines
5.4 KiB
C#
121 lines
5.4 KiB
C#
using ScadaLink.Commons.Types;
|
|
|
|
namespace ScadaLink.Commons.Interfaces.Services;
|
|
|
|
/// <summary>
|
|
/// Audit Log #23 (M3 Bundle E — Tasks E4/E5): site-side hook the
|
|
/// store-and-forward retry loop invokes after every cached-call attempt and
|
|
/// at terminal-state transitions, so the audit pipeline can emit
|
|
/// <c>ApiCallCached</c>/<c>DbWriteCached</c> per-attempt rows and the
|
|
/// <c>CachedResolve</c> terminal row under the original
|
|
/// <see cref="TrackedOperationId"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// The interface deliberately uses <see cref="CachedCallAttemptOutcome"/>
|
|
/// rather than <see cref="ScadaLink.Commons.Types.Enums.AuditStatus"/> so the
|
|
/// S&F project does not need to depend on the audit vocabulary — the
|
|
/// bridge living in <c>ScadaLink.AuditLog</c> maps the outcome to the right
|
|
/// audit kind + status when materialising the <c>CachedCallTelemetry</c>
|
|
/// packet.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Best-effort contract (alog.md §7):</b> implementations MUST swallow
|
|
/// internal failures rather than propagating to the S&F service — a
|
|
/// thrown observer must not be misclassified as a transient delivery
|
|
/// failure and must not corrupt the retry-count bookkeeping.
|
|
/// </para>
|
|
/// </remarks>
|
|
public interface ICachedCallLifecycleObserver
|
|
{
|
|
/// <summary>
|
|
/// Called by the store-and-forward retry loop after every cached-call
|
|
/// delivery attempt. Receives the message's TrackedOperationId-bearing id,
|
|
/// the per-category channel discriminator, retry-count + last-error
|
|
/// context, and whether the outcome reached a terminal state.
|
|
/// </summary>
|
|
Task OnAttemptCompletedAsync(CachedCallAttemptContext context, CancellationToken ct = default);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Per-attempt context handed to <see cref="ICachedCallLifecycleObserver"/>.
|
|
/// </summary>
|
|
/// <param name="TrackedOperationId">
|
|
/// Tracking id parsed from the underlying <c>StoreAndForwardMessage.Id</c>.
|
|
/// </param>
|
|
/// <param name="Channel">
|
|
/// Trust-boundary channel string — <c>"ApiOutbound"</c> for ExternalSystem
|
|
/// cached calls, <c>"DbOutbound"</c> for cached DB writes.
|
|
/// </param>
|
|
/// <param name="Target">Human-readable target (system name / DB connection).</param>
|
|
/// <param name="SourceSite">Site id that submitted the cached call.</param>
|
|
/// <param name="Outcome">Per-attempt outcome.</param>
|
|
/// <param name="RetryCount">Number of retries performed so far (S&F bookkeeping).</param>
|
|
/// <param name="LastError">Most recent error message (null on success).</param>
|
|
/// <param name="HttpStatus">Most recent HTTP status (null when not applicable).</param>
|
|
/// <param name="CreatedAtUtc">When the underlying S&F message was first enqueued.</param>
|
|
/// <param name="OccurredAtUtc">When this attempt completed.</param>
|
|
/// <param name="DurationMs">Duration of the attempt in milliseconds (null when not measured).</param>
|
|
/// <param name="SourceInstanceId">Originating instance, when known.</param>
|
|
/// <param name="ExecutionId">
|
|
/// Audit Log #23 (ExecutionId Task 4): the originating script execution's
|
|
/// per-run correlation id, threaded through the store-and-forward buffer from
|
|
/// the cached-call enqueue path. The audit bridge stamps it onto the
|
|
/// retry-loop <c>ApiCallCached</c>/<c>DbWriteCached</c> Attempted and
|
|
/// <c>CachedResolve</c> rows so they correlate with the rest of the run.
|
|
/// <c>null</c> for rows buffered before Task 4 (back-compat).
|
|
/// </param>
|
|
/// <param name="SourceScript">
|
|
/// Audit Log #23 (ExecutionId Task 4): the originating script identifier,
|
|
/// threaded alongside <paramref name="ExecutionId"/> so the retry-loop audit
|
|
/// rows carry the same <c>SourceScript</c> provenance the script-side cached
|
|
/// rows already do. <c>null</c> when not known.
|
|
/// </param>
|
|
/// <param name="ParentExecutionId">
|
|
/// Audit Log #23 (ParentExecutionId Task 6): the <c>ExecutionId</c> of the
|
|
/// inbound-API request that spawned the originating script execution,
|
|
/// threaded through the store-and-forward buffer alongside
|
|
/// <paramref name="ExecutionId"/>. The audit bridge stamps it onto the
|
|
/// retry-loop <c>ApiCallCached</c>/<c>DbWriteCached</c> Attempted and
|
|
/// <c>CachedResolve</c> rows so they correlate back to the spawning run.
|
|
/// <c>null</c> for a non-routed run and for rows buffered before Task 6
|
|
/// (back-compat).
|
|
/// </param>
|
|
public sealed record CachedCallAttemptContext(
|
|
TrackedOperationId TrackedOperationId,
|
|
string Channel,
|
|
string Target,
|
|
string SourceSite,
|
|
CachedCallAttemptOutcome Outcome,
|
|
int RetryCount,
|
|
string? LastError,
|
|
int? HttpStatus,
|
|
DateTime CreatedAtUtc,
|
|
DateTime OccurredAtUtc,
|
|
int? DurationMs,
|
|
string? SourceInstanceId,
|
|
Guid? ExecutionId = null,
|
|
string? SourceScript = null,
|
|
Guid? ParentExecutionId = null);
|
|
|
|
/// <summary>
|
|
/// Coarse outcome of one cached-call delivery attempt, observed from inside
|
|
/// the store-and-forward retry loop. The audit bridge maps this to the
|
|
/// <c>ApiCallCached</c>/<c>DbWriteCached</c> Attempted row and, when terminal,
|
|
/// the corresponding <c>CachedResolve</c> row.
|
|
/// </summary>
|
|
public enum CachedCallAttemptOutcome
|
|
{
|
|
/// <summary>Attempt delivered successfully — terminal Delivered state.</summary>
|
|
Delivered,
|
|
|
|
/// <summary>Attempt failed transiently; another retry will follow.</summary>
|
|
TransientFailure,
|
|
|
|
/// <summary>Attempt returned permanent failure — terminal Parked state (S&F semantics).</summary>
|
|
PermanentFailure,
|
|
|
|
/// <summary>Retry budget exhausted — terminal Parked state.</summary>
|
|
ParkedMaxRetries,
|
|
}
|