Planned stop of the Primary takes 30 s: host waits for the peer's open LocalDb sync stream before Akka leaves (failover delayed) #906
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Seen on QA 2026-11-13 during the lmxopcua#780 ScriptedAlarm-seam rolling restart (
c9640f5c).Restart-Service OtOpcUaHoston the Primary vq04 at 08:07:21Z took about 30 s to stop. Until it stopped, the Secondary vq03 stayed at ServiceLevel 240 with/health/active503 (sampled every ~4 s), and moved to 250 only at about 08:07:52Z.vq04 log (EST):
03:07:21.289 Application is shutting down...03:07:51.292 Error reading message. System.IO.IOException: The request stream was aborted.;03:07:51.293 HTTP POST /localdb_sync.v1.LocalDbSync/Sync responded 200 in 76909.9139 ms;03:07:51.302 CoordinatedShutdown invoked …, thenLeaving, OldestChanged → vq03, singleton hand-over done at 03:07:52.So the host waits for the peer's open LocalDb sync stream (vq03 is the sync client,
LocalDb__Replication__PeerAddress) until Kestrel's graceful-shutdown timeout (the 30 s default) aborts it. Only then does AkkaCoordinatedShutdownrun and leave the cluster. During those 30 s the stopping Primary still holds the Primary role and keeps the Secondary at 240.The sync client side stops fast: vq03 restarted at 08:06:30Z and was ready again in 10 s. That explains why the 2026-11-13 failover gate, which stopped vq03, saw a 1 s stop, while today's stop of vq04 took 30 s.
Not checked: whether vq04 still answered OPC UA reads in that window. The OPC UA endpoint may already have been stopping, which would leave the pair with no node at 250 for about 30 s on a planned stop of the Primary.
Possible fixes:
LocalDbSync/Syncstreams onApplicationStopping, through a token the service observes;HostOptions.ShutdownTimeoutfor that endpoint.The stream belongs to the
ZB.MOM.WW.LocalDb.Replicationsync service hosted in OtOpcUa (scadaproj LocalDb lib). ScadaBridge's site pairs may show the same thing.Record:
docs/deploy/2026-11-13-780-scriptedalarm-qa-and-vd03-flip.md.Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
Fixed on
masterdcff5ae9(OtOpcUa host) + library fix on scadaprojmain3ddb083(unpublished, follow-up scadaproj#57). No deploy done.Root cause. Two things combined:
WebApplicationstarts the web server after every hosted service the app registers, so it stops it first. Akka.Hosting'sStopAsync, which runsCoordinatedShutdown(leave + singleton hand-over), only runs after Kestrel has finished draining.HostOptions.ShutdownTimeout(30 s default). The peer's/localdb_sync.v1.LocalDbSync/Syncstream never ends on its own.ZB.MOM.WW.LocalDb0.3.7'sLocalDbSyncServiceobserves onlycontext.CancellationToken, and ASP.NET Core cancels that only when Kestrel aborts the request at the end of the drain. Nothing linkedApplicationStoppinginto it. That matches the vq04 log: 30 s of silence, then "request stream was aborted", then CoordinatedShutdown.From the hosted-service order, the OPC UA endpoint (
OtOpcUaServerHostedService, registered beforeAddAkka) stops after Akka, so vq04 most likely still served at 250 during the window. The real cost was a 30 s delay in failover on a planned stop.Fix (OtOpcUa host,
dcff5ae9):GracefulClusterLeaveService(IHostedLifecycleService). ItsStoppingAsyncruns before any hosted service'sStopAsync, Kestrel's included. It raisesApplicationStopping(idempotent), then runs and awaitsCoordinatedShutdown.Run(ClrExitReason), bounded by the host shutdown token. The node leaves the cluster while Kestrel is still up, so/health/activekeeps answering. It deliberately does not callCluster.Leave: that path ends inClusterLeavingReason, and Akka.Hosting then setsEnvironment.ExitCode = -1, so a supervisor would read the planned stop as a crash. Akka.Hosting's own laterRuncall returns the same completed task.ShutdownStreamCancellationInterceptor, innermost on the shared gRPC pipeline after the auth gates. It gives every client-, server- and duplex-streaming call a context whose token is linked withApplicationStopping, and refuses new streamsUnavailableonce stopping has begun.GetHttpContext()is preserved. This also covers the mesh telemetry and command streams.Library (scadaproj
3ddb083, not published):LocalDbSyncServicetakes an optionalIHostApplicationLifetime, linksApplicationStoppinginto the stream token and refuses new streamsUnavailablewhile stopping. Wire-compatible. 262/262. Both halves were mutation-killed.Evidence. These are test measurements, not a docker-dev or box measurement.
GracefulStopWithOpenSyncStreamTestsuses a realWebApplication, Kestrel h2c, the realMapZbLocalDbSyncbehindLocalDbSyncAuthInterceptor, a single-member Akka cluster and a realSyncBackgroundServicepeer holding the stream, with an 8 s drain budget. A wrappedIServertimestamps when the drain begins.With production's 30 s budget, the "without" row is the QA 30 s.
Mutations:
StoppingAsyncdoes not run CoordinatedShutdown: the order assertion fails (drain at 1 ms, removed at 893 ms).Tests. Release solution build: 0 warnings.
Host.Tests288/288.Host.IntegrationTests339/342 with 2 skipped and 1 failure:FailoverDuringDeployTests.Deployment_started_with_node_b_down_does_not_seal_without_it. It passed 3/3 in isolation, and itsTwoNodeClusterHarnessuses neither new component (no gRPC pipeline, no leave service), so I'm treating it as a load flake. After rebase: the LocalDb, MeshGrpc and #906 filter passed 114/114.ScadaBridge has the same exposure on site pairs. It maps
MapZbLocalDbSync()and cancels only its ownSiteStreamGrpcServerstreams onApplicationStopping, and Kestrel stops before itsAkkaHostedService. Not changed and not measured; tracked in scadaproj#57.Closing: the OtOpcUa fix is complete on its own. The library publish and pins are in scadaproj#57, and the fix takes effect on DEV/QA at the next OtOpcUa deploy.
Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
Deployed and proven on the QA pair, 2026-11-13. Master
76e73e7d: the host graceful leave plus LocalDb 0.3.8. Record:docs/deploy/2026-11-13-vd03-qa-to-76e73e7d.md(5baf05f6).Before vs after on the same node. vq04 is the LocalDb sync server, and vq03 holds its stream open.
c9640f5c, the window's stop of vq04, 12:35Z76e73e7d, leg 2, vq04 Primary, 12:44ZApplication is shutting down→LeavingLocalDbSync/Sync responded 20007:35:56.989 →Leaving07:35:57.020 localSync responded 200.480 →Host stopping: leaving the Akka cluster before the web server drains its open requests (lmxopcua#906)..483 →Leaving.500 →Akka CoordinatedShutdown completed in 236 msStop-Service/health/active200 on the first 0.5 s sampleLeg 1 (stop vq03, the sync client, while Primary):
#906line, CoordinatedShutdown 647 ms, vq04 at 250 in 0.36 s.Both legs:
localdb-replicationconnected with backlog 0 afterwards;Note.
Stop-Serviceis ~6 s rather than 1–3 s. After the leave, the log shows ~5 s ending inSERVER - Service Fault Occurred. Reason=BadTimeout. This is most likely the OPC UA stack closing the gate's own open subscription session. It does not delay the hand-over.Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi