feat(host): register SiteCallAuditActor + CachedCallTelemetry forwarder/bridge (#22, #23 M3)

M3 Bundle F (Task F1) wires the cached-call audit pipeline through the
composition roots:

- Central: register SiteCallAuditActor as a cluster singleton + proxy
  (mirrors AuditLogIngestActor and NotificationOutboxActor). Program.cs
  calls .AddSiteCallAudit() on the central role.
- Site: register ICachedCallTelemetryForwarder + CachedCallLifecycleBridge
  in AddAuditLog (lazy factory — Central nodes degrade to audit-only
  emission because IOperationTrackingStore is site-only).
- Site: bind CachedCallLifecycleBridge to ICachedCallLifecycleObserver so
  StoreAndForwardService picks it up via DI.
- Site: introduce IStoreAndForwardSiteContext + Host adapter to surface the
  site id to StoreAndForwardService without creating a
  StoreAndForward -> HealthMonitoring project-reference cycle.
- ScriptExecutionActor resolves ICachedCallTelemetryForwarder per script
  scope and threads it into ScriptRuntimeContext.

CachedCallTelemetryForwarder's IOperationTrackingStore dependency is now
nullable so Central DI validation succeeds with the lazy registration; the
forwarder's tracking-half emission is a no-op when the store is absent.

Tests:
- AkkaHostedServiceAuditWiringTests: Central host builds with
  AddSiteCallAudit and resolves ICachedCallTelemetryForwarder; Site
  resolves the forwarder + bridge + observer + IStoreAndForwardSiteContext.
- Full solution: 194 Host tests green, 241 SiteRuntime tests green, every
  other suite unchanged.
This commit is contained in:
Joseph Doherty
2026-05-20 15:10:47 -04:00
parent 047988e4c8
commit 6fe23a4d9b
11 changed files with 291 additions and 5 deletions

View File

@@ -0,0 +1,27 @@
namespace ScadaLink.StoreAndForward;
/// <summary>
/// Optional ambient site context the Store-and-Forward service consults at
/// construction time. Carries the site identifier the S&amp;F retry loop
/// stamps onto cached-call audit telemetry (Audit Log #23 / M3 Bundle F).
/// </summary>
/// <remarks>
/// <para>
/// Defined here (not in <c>HealthMonitoring</c> alongside the existing
/// <c>ISiteIdentityProvider</c>) so the dependency arrow does not flip:
/// <c>HealthMonitoring</c> already references <c>StoreAndForward</c>, and
/// having S&amp;F take a dependency on <c>HealthMonitoring</c> would create a
/// project-reference cycle.
/// </para>
/// <para>
/// The Host registers a trivial adapter that forwards to the same
/// <c>NodeOptions.SiteId</c> the existing <c>ISiteIdentityProvider</c> reads.
/// Resolution is optional: when no binding is registered the S&amp;F service
/// stamps an empty site id, preserving the legacy pre-M3 behaviour exactly.
/// </para>
/// </remarks>
public interface IStoreAndForwardSiteContext
{
/// <summary>The site id stamped onto cached-call audit telemetry.</summary>
string SiteId { get; }
}

View File

@@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ScadaLink.Commons.Interfaces.Services;
namespace ScadaLink.StoreAndForward;
@@ -23,7 +24,28 @@ public static class ServiceCollectionExtensions
var options = sp.GetRequiredService<IOptions<StoreAndForwardOptions>>().Value;
var logger = sp.GetRequiredService<ILogger<StoreAndForwardService>>();
var replication = sp.GetRequiredService<ReplicationService>();
return new StoreAndForwardService(storage, options, logger, replication);
// Audit Log #23 (M3 Bundle F): Wire the cached-call lifecycle
// observer + site identity through DI so the S&F retry loop emits
// per-attempt + terminal telemetry under the same TrackedOperationId
// the script-thread CachedSubmit row used. Both bindings are
// optional — when null the legacy pre-M3 retry behaviour is
// preserved exactly (tests, central nodes without sites, hosts
// that haven't called AddAuditLog).
//
// Site identity is resolved through the optional
// IStoreAndForwardSiteContext binding (registered by the Host) to
// avoid a project-reference cycle with HealthMonitoring's
// ISiteIdentityProvider — HealthMonitoring already references S&F.
var cachedCallObserver = sp.GetService<ICachedCallLifecycleObserver>();
var siteContext = sp.GetService<IStoreAndForwardSiteContext>();
var siteId = siteContext?.SiteId ?? string.Empty;
return new StoreAndForwardService(
storage,
options,
logger,
replication,
cachedCallObserver,
siteId);
});
services.AddSingleton<ReplicationService>(sp =>