feat(siteruntime): ExternalSystem.CachedCall emits CachedSubmit telemetry (#23 M3)

Rework ScriptRuntimeContext.ExternalSystem.CachedCall to fit the M3
combined-telemetry model:

* Mints a fresh TrackedOperationId and emits one CachedSubmit packet
  via ICachedCallTelemetryForwarder BEFORE handing the call off — the
  SiteCalls row is materialised before the first delivery attempt so
  Tracking.Status(id) can observe a Submitted row even if immediate
  delivery resolves before the helper returns.
* Threads the TrackedOperationId into IExternalSystemClient.CachedCallAsync
  as a new optional parameter (and into IDatabaseGateway.CachedWriteAsync
  for the Database mirror set up here for E6). The gateway uses the id
  as the StoreAndForward messageId so the retry loop (Tasks E4/E5) can
  recover it from StoreAndForwardMessage.Id.
* Returns the TrackedOperationId rather than ExternalCallResult — the
  script's contract is now "get a tracking handle, observe outcome via
  Tracking.Status". Best-effort emission: a thrown forwarder is logged
  + swallowed; the original call still runs and the id is still returned.

DatabaseHelper gets the matching siteId / sourceScript / forwarder
fields and a parallel CachedSubmit emitter (Channel=DbOutbound) so Task
E6's Database.CachedWrite mirror plugs in without further runtime
wiring.

New ICachedCallTelemetryForwarder seam in Commons.Interfaces.Services
so SiteRuntime depends on Commons (existing arrow) rather than
ScadaLink.AuditLog (would have introduced a new dependency).

Bundle E task E3 (and helper-shape work for E6).
This commit is contained in:
Joseph Doherty
2026-05-20 14:48:05 -04:00
parent 2145b29d4d
commit 42430dd10a
8 changed files with 587 additions and 16 deletions

View File

@@ -0,0 +1,34 @@
using ScadaLink.Commons.Messages.Integration;
namespace ScadaLink.Commons.Interfaces.Services;
/// <summary>
/// Site-side fan-out abstraction for cached-call lifecycle telemetry
/// (Audit Log #23 / M3). One <see cref="CachedCallTelemetry"/> packet carries
/// both an audit row and an operational <c>SiteCalls</c> upsert; the
/// implementation routes the audit half through <see cref="IAuditWriter"/>
/// and the operational half through the site-local tracking SQLite store.
/// </summary>
/// <remarks>
/// <para>
/// Defined in Commons so the script runtime (and the StoreAndForward retry
/// loop, Bundle E4) can take a dependency on the abstraction rather than on
/// the concrete forwarder living inside <c>ScadaLink.AuditLog</c> — the
/// existing dependency arrow runs from <c>SiteRuntime</c> to Commons, not to
/// AuditLog.
/// </para>
/// <para>
/// <b>Best-effort contract (alog.md §7):</b> implementations MUST swallow
/// internal failures rather than propagating to the calling script.
/// </para>
/// </remarks>
public interface ICachedCallTelemetryForwarder
{
/// <summary>
/// Fan one combined-telemetry packet out to the audit writer and the
/// tracking store. Best-effort — failures on either half are logged and
/// swallowed; the returned Task completes when both halves have been
/// attempted.
/// </summary>
Task ForwardAsync(CachedCallTelemetry telemetry, CancellationToken ct = default);
}