feat(deploy): wire periodic PendingDeployment purge + SQL Server same-id re-stage test
Notify-and-fetch follow-ups: - PendingDeploymentPurgeActor: a central cluster singleton (not readiness-gated, best-effort) that sweeps expired PendingDeployment staging rows on CommunicationOptions.PendingDeploymentPurgeInterval (default 1h). Modeled on the kpi-history-recorder pattern: self-scheduling timer, per-tick DI scope -> IDeploymentManagerRepository, continue-on-error. Wired in AkkaHostedService.RegisterCentralActors (manager + proxy + drain); resolves the deferred TODO in DeploymentService. Correctness never depends on it (supersession bounds rows to <=1/instance; the fetch endpoint enforces the TTL), so it is deliberately absent from RequiredSingletonsHealthCheck. - SQL Server integration test for StagePendingIfAbsentAsync re-staging an instance's OWN DeploymentId over an expired row against the real UNIQUE index on DeploymentId — confirms EF orders DELETE before INSERT in one SaveChanges (SQLite's constraint timing differs from SQL Server's). Plus a same-instance supersession variant on real SQL Server. Tests: 2 TestKit actor tests + 2 SQL Server integration tests (both ran green against the infra MSSQL container); 235 Communication + 15 PendingDeployment tests pass; Host builds 0 warnings.
This commit is contained in:
@@ -756,6 +756,60 @@ akka {{
|
||||
_actorSystem.ActorOf(kpiHistoryProxyProps, "kpi-history-recorder-proxy");
|
||||
_logger.LogInformation("KpiHistoryRecorderActor singleton created (not readiness-gated)");
|
||||
|
||||
// Notify-and-fetch deploy transport — central singleton that periodically
|
||||
// reclaims expired (TTL-elapsed) PendingDeployment staging rows. Mirrors the
|
||||
// kpi-history-recorder singleton pattern above: a ClusterSingletonManager pins
|
||||
// it to the active central node, a ClusterSingletonProxy gives a stable address,
|
||||
// and a PhaseClusterLeave graceful-stop task drains the in-flight tick before
|
||||
// handover. The purge timer self-schedules in PreStart. NOT readiness-gated by
|
||||
// design: the purge is best-effort hygiene — supersession bounds the table to ≤1
|
||||
// pending row per instance and the config-fetch endpoint enforces the TTL on read,
|
||||
// so correctness never depends on it; pending-deployment-purge is therefore
|
||||
// deliberately absent from RequiredSingletonsHealthCheck. The actor takes the root
|
||||
// IServiceProvider and opens its own per-tick DI scope (IDeploymentManagerRepository
|
||||
// is a scoped EF Core service).
|
||||
var pendingPurgeLogger = _serviceProvider.GetRequiredService<ILoggerFactory>()
|
||||
.CreateLogger<PendingDeploymentPurgeActor>();
|
||||
var pendingPurgeCommunicationOptions =
|
||||
_serviceProvider.GetRequiredService<IOptions<CommunicationOptions>>();
|
||||
|
||||
var pendingPurgeSingletonProps = ClusterSingletonManager.Props(
|
||||
singletonProps: Props.Create(() => new PendingDeploymentPurgeActor(
|
||||
_serviceProvider,
|
||||
pendingPurgeCommunicationOptions,
|
||||
pendingPurgeLogger)),
|
||||
terminationMessage: PoisonPill.Instance,
|
||||
settings: ClusterSingletonManagerSettings.Create(_actorSystem!)
|
||||
.WithSingletonName("pending-deployment-purge"));
|
||||
var pendingPurgeSingletonManager =
|
||||
_actorSystem!.ActorOf(pendingPurgeSingletonProps, "pending-deployment-purge-singleton");
|
||||
|
||||
var pendingPurgeShutdown = Akka.Actor.CoordinatedShutdown.Get(_actorSystem);
|
||||
pendingPurgeShutdown.AddTask(
|
||||
Akka.Actor.CoordinatedShutdown.PhaseClusterLeave,
|
||||
"drain-pending-deployment-purge-singleton",
|
||||
async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await pendingPurgeSingletonManager.GracefulStop(TimeSpan.FromSeconds(10));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex,
|
||||
"PendingDeploymentPurge singleton did not drain within the graceful-stop "
|
||||
+ "timeout; falling through to PoisonPill handover");
|
||||
}
|
||||
return Akka.Done.Instance;
|
||||
});
|
||||
|
||||
var pendingPurgeProxyProps = ClusterSingletonProxy.Props(
|
||||
singletonManagerPath: "/user/pending-deployment-purge-singleton",
|
||||
settings: ClusterSingletonProxySettings.Create(_actorSystem)
|
||||
.WithSingletonName("pending-deployment-purge"));
|
||||
_actorSystem.ActorOf(pendingPurgeProxyProps, "pending-deployment-purge-proxy");
|
||||
_logger.LogInformation("PendingDeploymentPurgeActor singleton created (not readiness-gated)");
|
||||
|
||||
_logger.LogInformation("Central actors registered. CentralCommunicationActor created.");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user