Repository navigation
Replies: 2 comments
TriageVerdict: A reproducible bug in Is this a misuse of humus?No. The order of the cancel and the GOAWAY is The docs promise where that stream goes: "Reconnect: the new stream lands on another pod." The end user can't defend against the window. Nothing tells them when their client has read the GOAWAY, and a stream opened again too soon ends with the same status as the one before, "server is shutting down". On a handler that sends before it looks at its context, the new stream even hears from the old pod first, and then ends again. Opening it again at once only lands on the old pod again, until the GOAWAY arrives. The strongest argument for misuse is that the wallet treats a watch that ends before its first message as a ledger that is down. Is it a feature request?No. It contradicts these promises:
A fix restores promised behaviour, and changes no exported name, signature, Is it reproducible?Yes. It reproduced at 6351c86 with Go 1.27.0, grpc-go v1.84.0, pgx v5.11.0, testcontainers-go v0.44.0 and There are two kinds of reproduction. The first kind extends
The wallet against PostgreSQL, with a connection to each replica in its handshake: every watch ends. Each held connection closes 200ms after its replica's first watch ends. That does from a test what the report's 200ms sleep did inside
8 server streams on one Client, with nothing held: some land on the old pod. From synctest's run 3:
One stream never lost this race in synctest or over TCP. It takes several streams ending at once, as the case's 8 watches do. With each service's telemetry recorded, the case passed 4 runs of 4, while the case as it is failed 3 of 4. I didn't look into why recording changes the timing. The report has the mechanism right. It needs these corrections and additions:
Why
Other ways into the same code:
#1258 is filed to fix it:
|
|
Fixed by #1258, in #1272, merged as e59c40c.
The fix also cures the triage's side finding: a connection in its handshake no longer holds |
Uh oh!
There was an error while loading. Please reload this page.
What happens
The shared
ARolloutOfTheLedgercase of the gRPC walkthrough fails intermittently under Wallet walkthrough (testcontainers), onmain, in the merge queue and on PRs, e.g. run 39689285771 onmain:It has failed this way 8 times since the case landed in #1242 on 2026-10-21, out of about 33 CI runs, about 1 in 4:
mainA watch on the wallet ended during a rollout of the ledger with
LEDGER_UNAVAILABLE. #1200 has every watch stay open through a rollout of the ledger. The same case against the in-memory ledger,TestInit_passes_the_shared_cases, hasn't failed.Why
This hasn't been triaged. What follows comes from the code and a reproduction.
Shutdowncancels the context of every stream, and of every stream that starts after, beforeGracefulStopsends the GOAWAY (Server.Shutdown, Streams at shutdown). So the ledger pod being replaced ends each watch with Unavailable "server is shutting down" while the wallet's connection to it is still open.WatchAccountopens the ledger's watch again at once. Until the wallet's client has read the GOAWAY, the new stream goes on the same connection, to the same pod, and gets a context that is already cancelled.WatchAccountreads the account before it sends its first message. pgx fails the query on the cancelled context, so the stream ends with Unavailable before any message.received && ...). This one received none, so the wallet ends its client's watch with "the ledger can't be reached".The in-memory store (
memoryStore.Account) and the fake ledger of the wallet's "relays the watch through the ledger's shutdown" test ignore a cancelled context and send the first message, so neither sees this.grpcserver's doc tells a client that gets Unavailable to reconnect, since "the new stream lands on another pod", and grpcclient's says the streams the end user's code opens again "land on the next pod". Before the GOAWAY reaches the client, they land on the same pod.
Reproduction. At 6351c86, this change to
grpc/internal/server/server.gowidens the window between the cancel and the GOAWAY:s.streams.stop() + time.Sleep(200 * time.Millisecond)With it, the case fails every time (
-count=3), with the error CI sees:Without the change, it failed this way 10 times out of 15 locally (
-count=15, with another test suite running alongside).Expected
Every watch on the wallet stays open through a rollout of the ledger, as #1200 has it, whenever the wallet's watch of the ledger is opened again. The
ARolloutOfTheLedgercase passes whenever the wallet works.References
ARolloutOfTheLedgercase, from story(example): keep every watch open through a rollout of the ledger #1200All reactions