feat(audit): start purge + reconciliation singletons; production ISiteEnumerator

This commit is contained in:
Joseph Doherty
2026-06-15 10:00:44 -04:00
parent d03c2af9a1
commit 36a08a4145
6 changed files with 337 additions and 6 deletions
@@ -588,6 +588,117 @@ akka {{
_logger.LogInformation(
"SiteCallAuditActor singleton created and registered with CentralCommunicationActor");
// Audit Log (#23) M6 Bundle B/C — start the two central-only maintenance
// singletons that were fully implemented but never instantiated: the
// daily AuditLog partition-switch purge (AuditLogPurgeActor) and the
// periodic per-site audit-event reconciliation pull
// (SiteAuditReconciliationActor). Both mirror the SiteCallAudit /
// NotificationOutbox singleton pattern above: a ClusterSingletonManager
// pins the actor to the active central node, a ClusterSingletonProxy
// gives a stable address, and a PhaseClusterLeave graceful-stop task
// drains the in-flight tick before handover. Options + the production
// ISiteEnumerator + IPullAuditEventsClient come from
// AddAuditLogCentralReconciliationClient (central composition root only).
// Both actors take the root IServiceProvider and open their own per-tick
// DI scope because IAuditLogRepository / ISiteRepository are scoped EF
// Core services.
var auditPurgeLogger = _serviceProvider.GetRequiredService<ILoggerFactory>()
.CreateLogger<ZB.MOM.WW.ScadaBridge.AuditLog.Central.AuditLogPurgeActor>();
var auditPurgeOptions = _serviceProvider
.GetRequiredService<IOptions<ZB.MOM.WW.ScadaBridge.AuditLog.Central.AuditLogPurgeOptions>>();
var auditLogOptions = _serviceProvider
.GetRequiredService<IOptions<ZB.MOM.WW.ScadaBridge.AuditLog.Configuration.AuditLogOptions>>();
var auditPurgeSingletonProps = ClusterSingletonManager.Props(
singletonProps: Props.Create(() => new ZB.MOM.WW.ScadaBridge.AuditLog.Central.AuditLogPurgeActor(
_serviceProvider,
auditPurgeOptions,
auditLogOptions,
auditPurgeLogger)),
terminationMessage: PoisonPill.Instance,
settings: ClusterSingletonManagerSettings.Create(_actorSystem!)
.WithSingletonName("audit-log-purge"));
var auditPurgeSingletonManager =
_actorSystem!.ActorOf(auditPurgeSingletonProps, "audit-log-purge-singleton");
var auditPurgeShutdown = Akka.Actor.CoordinatedShutdown.Get(_actorSystem);
auditPurgeShutdown.AddTask(
Akka.Actor.CoordinatedShutdown.PhaseClusterLeave,
"drain-audit-log-purge-singleton",
async () =>
{
try
{
await auditPurgeSingletonManager.GracefulStop(TimeSpan.FromSeconds(10));
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"AuditLogPurge singleton did not drain within the graceful-stop "
+ "timeout; falling through to PoisonPill handover");
}
return Akka.Done.Instance;
});
var auditPurgeProxyProps = ClusterSingletonProxy.Props(
singletonManagerPath: "/user/audit-log-purge-singleton",
settings: ClusterSingletonProxySettings.Create(_actorSystem)
.WithSingletonName("audit-log-purge"));
_actorSystem.ActorOf(auditPurgeProxyProps, "audit-log-purge-proxy");
_logger.LogInformation("AuditLogPurgeActor singleton created");
// SiteAuditReconciliationActor — self-healing fallback puller. Resolves
// its production ISiteEnumerator (config-DB Site projection) and
// IPullAuditEventsClient (gRPC) from the central reconciliation-client
// helper registered in Program.cs.
var auditReconLogger = _serviceProvider.GetRequiredService<ILoggerFactory>()
.CreateLogger<ZB.MOM.WW.ScadaBridge.AuditLog.Central.SiteAuditReconciliationActor>();
var auditReconOptions = _serviceProvider
.GetRequiredService<IOptions<ZB.MOM.WW.ScadaBridge.AuditLog.Central.SiteAuditReconciliationOptions>>();
var auditReconSites = _serviceProvider
.GetRequiredService<ZB.MOM.WW.ScadaBridge.AuditLog.Central.ISiteEnumerator>();
var auditReconClient = _serviceProvider
.GetRequiredService<ZB.MOM.WW.ScadaBridge.AuditLog.Central.IPullAuditEventsClient>();
var auditReconSingletonProps = ClusterSingletonManager.Props(
singletonProps: Props.Create(() => new ZB.MOM.WW.ScadaBridge.AuditLog.Central.SiteAuditReconciliationActor(
auditReconSites,
auditReconClient,
_serviceProvider,
auditReconOptions,
auditReconLogger)),
terminationMessage: PoisonPill.Instance,
settings: ClusterSingletonManagerSettings.Create(_actorSystem!)
.WithSingletonName("site-audit-reconciliation"));
var auditReconSingletonManager =
_actorSystem!.ActorOf(auditReconSingletonProps, "site-audit-reconciliation-singleton");
var auditReconShutdown = Akka.Actor.CoordinatedShutdown.Get(_actorSystem);
auditReconShutdown.AddTask(
Akka.Actor.CoordinatedShutdown.PhaseClusterLeave,
"drain-site-audit-reconciliation-singleton",
async () =>
{
try
{
await auditReconSingletonManager.GracefulStop(TimeSpan.FromSeconds(10));
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"SiteAuditReconciliation singleton did not drain within the graceful-stop "
+ "timeout; falling through to PoisonPill handover");
}
return Akka.Done.Instance;
});
var auditReconProxyProps = ClusterSingletonProxy.Props(
singletonManagerPath: "/user/site-audit-reconciliation-singleton",
settings: ClusterSingletonProxySettings.Create(_actorSystem)
.WithSingletonName("site-audit-reconciliation"));
_actorSystem.ActorOf(auditReconProxyProps, "site-audit-reconciliation-proxy");
_logger.LogInformation("SiteAuditReconciliationActor singleton created");
_logger.LogInformation("Central actors registered. CentralCommunicationActor created.");
}
@@ -97,6 +97,13 @@ try
// pf_AuditLog_Month forward monthly. Depends on IPartitionMaintenance
// (registered below by AddConfigurationDatabase).
builder.Services.AddAuditLogCentralMaintenance(builder.Configuration);
// #23 M6 Bundle B/C — central-only registration backing the two
// maintenance singletons started in AkkaHostedService: the production
// ISiteEnumerator + IPullAuditEventsClient (gRPC) used by the
// SiteAuditReconciliationActor, plus the AuditLogPurgeOptions /
// SiteAuditReconciliationOptions bindings consumed by both singletons.
// Central-only by design (it dials sites), kept out of AddAuditLog.
builder.Services.AddAuditLogCentralReconciliationClient(builder.Configuration);
// Site Call Audit (#22) — central node owns the SiteCallAuditActor
// singleton (M3 Bundle F). The extension itself currently registers
// nothing — actor Props are constructed inline in AkkaHostedService —