Deploy to a site pair with one node down: first attempt returns OUTCOME_UNKNOWN (connection failed), retry succeeds #136
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 twice while verifying #135 on the primary docker rig (2026-11-13,
d7fa31f0; details in the #135 closing comment).What happened. With
site-a-astopped andsite-a-bthe sole Up node of site-a, the first instance deploy to site-a returnedOUTCOME_UNKNOWN: connection failed…. An immediate retry succeeded againstsite-a-b. Both times, it was the first deploy after the node went down.Why it matters. A deploy to a site with one node down is exactly the failover case. Central reports an unknown outcome even though the site is healthy, so an operator or CLI script may treat it as a failure or re-send it.
To do.
Fixed in
fd80aeab(main).Root cause. Central keeps one sticky gRPC channel per site node (
SitePairChannelProvider), with NodeA preferred. The failback loop only probes while central is off A, so nothing notices that A has stopped until a real call fails. The first deploy after the stop therefore always goes to A. When it failed withUnavailable,SitePairChannelProvider.ExecuteAsync(previouslySitePairChannelProvider.cs:326-328) flipped stickiness to B. Because a deploy is not retry-safe and the error had no server-sentscadabridge-dispatch: not-startedtrailer, it then threwOUTCOME_UNKNOWNinstead of trying B. The next call went to B and succeeded, which is the "retry works" symptom.The rule was too blunt. For a stopped node, the client failed while opening the connection, so the request provably never left central. Grpc.Net.Client reports this as
Unavailable/"Error connecting to subchannel."with aSocketExceptioncause (refused, or the stopped container's hostname no longer resolving). Its balancer fails the call at pick time, before any HTTP request exists.Fix.
GrpcRetrySafety.WasNeverSent(GrpcRetrySafety.cs:48) returns true only for two shapes:SocketExceptioncause.HttpRequestExceptionclassedNameResolutionError/ConnectionErrorwhose socket error can only happen before a connection exists (ConnectionRefused,HostNotFound,TryAgain,NoData).ProvablyNotApplied=WasNotDispatched || WasNeverSent.ExecuteAsync(SitePairChannelProvider.cs:332,:341) now uses it, so a mutation fails over to the peer within the same call.OUTCOME_UNKNOWNand is not replayed: a reset or premature end on an established connection, GOAWAY, a keepalive timeout, or anUnavailablesent by the server. Deadlines are still never replayed. The request reaches exactly one node, so the site-side dedup (deployment id + query-before-redeploy) is never relied on for this path.GrpcCentralTransport) uses the sameProvablyNotAppliedrule.Tests.
GrpcSiteTransportFailoverTests(real Grpc.Net.Client +SocketsHttpHandler, NodeB a TestServer):ConnectionRefused_OnNodeA_MutationFailsOverToNodeB_WithinTheCallUnresolvableNodeA_MutationFailsOverToNodeB_WithinTheCall(the docker shape)ConnectionLostAfterDelivery_StaysOutcomeUnknown_AndIsNotReplayed: A's stub runs the command, then the transport fails with a premature end. Result isAborted/OUTCOME_UNKNOWN, and B gets 0 calls.GrpcRetrySafetyNeverSentTests: 9 classifier cases, positive and negative.WasNotDispatchedin the provider fails both failover tests.WasNeverSenttrue for anyUnavailablefailsConnectionLostAfterDelivery…and the existingAmbiguousUnavailable_DoesNotReplayLifecycleOnPeer.Rig (primary, site-a pair, CLI :9000,
instance deploy --id 1492rig-probe-1).docker stop scadabridge-site-a-a, then deploy →Deployment failed: Status(StatusCode="Aborted", Detail="OUTCOME_UNKNOWN: connection failed…"). Immediate retry → status 2 (success).scadabridge:latestfromfd80aeab; centrals and the site-a pair recreated together): a deploy with both nodes up succeeds.docker stop scadabridge-site-a-aat 05:25:03, then the first deploy → status 2, deployment48af9dee…. central-a logs05:25:06 WRN … node A unavailable; failing over to node B. site-a-b logs a singleDeploying instance rig-probe-1, deploymentId=48af9dee…, so it was applied once.Side note, not changed here: that warning renders as
Site central node A …. The node's enrichedSiteIdproperty appears to win over the message-template argument, so the log line names the wrong site.