fix(health): decouple AuditCentralHealthSnapshot from ActorSystem (#23 M6)
The snapshot's per-site stalled latch now lives on the snapshot itself and is fed by SiteAuditTelemetryStalledTracker via ApplyStalled, removing the chain that required ActorSystem at DI composition time. The tracker is now constructed by AkkaHostedService once ActorSystem.Create returns, with a lock-guarded auxiliary-disposable list so concurrent host start/stop in tests cannot race the enumeration.
This commit is contained in:
@@ -34,6 +34,13 @@ public class AkkaHostedService : IHostedService
|
||||
private readonly CommunicationOptions _communicationOptions;
|
||||
private readonly ILogger<AkkaHostedService> _logger;
|
||||
private ActorSystem? _actorSystem;
|
||||
/// <summary>
|
||||
/// Auxiliary IDisposables (e.g. the SiteAuditTelemetryStalledTracker)
|
||||
/// that this hosted service constructs at start time and must tear down
|
||||
/// on shutdown — they don't fit the ActorSystem lifecycle but share its
|
||||
/// process scope.
|
||||
/// </summary>
|
||||
private readonly List<IDisposable> _trackedDisposables = new();
|
||||
|
||||
public AkkaHostedService(
|
||||
IServiceProvider serviceProvider,
|
||||
@@ -201,6 +208,31 @@ akka {{
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Dispose auxiliary subscribers (e.g. SiteAuditTelemetryStalledTracker)
|
||||
// BEFORE Akka shuts down so their EventStream unsubscribe calls run
|
||||
// while the system is still alive. Per-tracker Dispose is wrapped in
|
||||
// its own try so a misbehaving subscriber can't sink the shutdown.
|
||||
// Snapshot the list inside a lock so a concurrent StartAsync (the
|
||||
// test harness sometimes triggers a second start/stop interleaving)
|
||||
// can't race the enumeration. Clearing the original list under the
|
||||
// same lock leaves the next StartAsync with a clean slate.
|
||||
IDisposable[] disposables;
|
||||
lock (_trackedDisposables)
|
||||
{
|
||||
disposables = _trackedDisposables.ToArray();
|
||||
_trackedDisposables.Clear();
|
||||
}
|
||||
foreach (var disposable in disposables)
|
||||
{
|
||||
try { disposable.Dispose(); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex,
|
||||
"Auxiliary subscriber {Type} threw during shutdown",
|
||||
disposable.GetType().Name);
|
||||
}
|
||||
}
|
||||
|
||||
if (_actorSystem != null)
|
||||
{
|
||||
_logger.LogInformation("Shutting down Akka.NET actor system via CoordinatedShutdown...");
|
||||
@@ -349,6 +381,31 @@ akka {{
|
||||
"AuditLogIngestActor singleton created (gRPC server bound: {GrpcBound})",
|
||||
grpcServer is not null);
|
||||
|
||||
// Audit Log (#23) M6 Bundle E (T7): subscribe the per-site stalled
|
||||
// telemetry tracker to the actor system EventStream NOW that the
|
||||
// system exists. The tracker mirrors every
|
||||
// SiteAuditTelemetryStalledChanged publication (from
|
||||
// SiteAuditReconciliationActor — wired in a later bundle) into the
|
||||
// AuditCentralHealthSnapshot singleton so the central health surface
|
||||
// sees per-site stalled state. The tracker is constructed here rather
|
||||
// than in AddAuditLogCentralMaintenance because its ctor needs an
|
||||
// ActorSystem, which is not a DI-resolvable singleton — it's owned
|
||||
// by this hosted service. The snapshot singleton is resolvable;
|
||||
// passing it in seeds the tracker's Apply() so both internal state
|
||||
// and the snapshot stay in lock-step.
|
||||
var auditCentralSnapshot = _serviceProvider
|
||||
.GetService<ScadaLink.AuditLog.Central.AuditCentralHealthSnapshot>();
|
||||
if (auditCentralSnapshot is not null)
|
||||
{
|
||||
var stalledTracker = new ScadaLink.AuditLog.Central.SiteAuditTelemetryStalledTracker(
|
||||
_actorSystem!, auditCentralSnapshot);
|
||||
lock (_trackedDisposables)
|
||||
{
|
||||
_trackedDisposables.Add(stalledTracker);
|
||||
}
|
||||
_logger.LogInformation("SiteAuditTelemetryStalledTracker subscribed to EventStream");
|
||||
}
|
||||
|
||||
// Site Call Audit (#22) — central singleton mirrors the AuditLogIngest
|
||||
// and NotificationOutbox patterns. M3's dual-write transaction routes
|
||||
// SiteCalls upserts through AuditLogIngestActor's own scope-per-message
|
||||
|
||||
Reference in New Issue
Block a user