Repository navigation
adapter,raftengine: read the real status off a reset client stream - #1252
Conversation
gRPC reports a stream the server has already terminated to the sender as a bare io.EOF; the status it terminated with is only readable from the receive side. Four client-stream senders returned that EOF instead of falling through to CloseAndRecv, so the peer's actual reason never reached the caller. PushChunkBlob is the one with a visible consequence: the replicator decides whether a push is worth retrying from this error, and an Unauthenticated from a peer missing its token looked identical to a transport hiccup. It is also a real CI failure -- TestGRPCS3BlobCluster- AuthenticatedCapabilityPushAndFetch asserted Unauthenticated and got Unknown whenever the reset won the race, which is exactly this. The three snapshot senders lose diagnosability rather than behaviour: a follower that rejected a snapshot for a nameable reason appeared in the sender's log as "EOF". The tests push more than the flow-control window so the sender is still writing when the reset lands, and reverting each of the four guards fails exactly the one test that covers it. They stay under gRPC's 4 MiB send cap: an oversized message makes the client reject it locally with a status of its own, which satisfied the assertion without the stream ever being reset -- the first version of this test passed against the bug for that reason. The snapshot test also shrinks the chunk size, because the 16 MiB default turns the payload into a single write that can win the race, which left the in-memory path uncovered. Claude-Session: https://claude.ai/code/session_013rNHooj7NF3giihWVba8QE
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
@codex review |
|
@claude review |
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 3 minutes. View limit details
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to The updated senders preserve server termination statuses after stream resets, with coverage for all four paths and no remaining actionable risk identified. 🚥 Pre-merge checks | ✅ 4 | ❌ 1
✨ Finishing Touches 💡 1
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Claude finished @bootjp's task in 2m 14s —— View job Review complete
Note: the sandbox blocked Correctness of the fixThe core change is sound.
A few things I checked specifically because they're easy to get wrong with this pattern, and they all hold up:
The doc comment on TestsBoth new tests are well constructed for what they're proving:
Minor observations (non-blocking)
Overall this is a precise, well-scoped fix with solid regression coverage matching the PR's own self-review. I don't see correctness issues in the diff. |
|
Codex Review: Didn't find any major issues. Swish! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
There was a problem hiding this comment.
🟢 Approval recommended
The EOF handling follows the gRPC streaming contract and is covered by targeted regression tests.
0 open findings
What changed in this PR
Fixes client-stream error handling so callers receive the server’s actual gRPC status instead of an opaque EOF.
Changes:
- Falls through to
CloseAndRecvwhen snapshot or S3 sends return EOF. - Adds regression coverage for all four affected send paths.
| File | Description |
|---|---|
adapter/s3_blob_cluster.go |
Recovers server status after early stream termination. |
adapter/s3_blob_push_status_test.go |
Tests early S3 push rejection. |
internal/raftengine/etcd/grpc_transport.go |
Fixes status propagation across snapshot senders. |
internal/raftengine/etcd/grpc_transport_snapshot_status_test.go |
Tests all snapshot sender paths. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Codex Review: Didn't find any major issues. Bravo. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
The finding
test (ubuntu-latest)failed on #1249 withTestGRPCS3BlobClusterAuthenticatedCapabilityPushAndFetchexpecting0x10(codes.Unauthenticated) and getting0x2(codes.Unknown). The failure is unrelated to #1249 — it is a pre-existing bug onmain, and it is a production bug, not a test flake.gRPC reports a stream the server has already terminated to the sender as a bare
io.EOF; the status it terminated with is only readable from the receive side (CloseAndRecv/RecvMsg). Four client-stream senders in the tree returned thatio.EOFstraight to the caller instead of falling through, so the peer's actual reason was discarded and replaced by an opaque EOF.adapter/s3_blob_cluster.goPushChunkBlobs3_blob_replicator.go:136decides whether a push is worth retrying from this error. AnUnauthenticatedfrom a peer missing its token was indistinguishable from a transport hiccup.internal/raftengine/etcd/grpc_transport.gosendSnapshotEOF.sendSnapshotSpoolstreamFSMSnapshotWhether a given call hits the bug is a race — small payloads usually get the Send in before the reset lands, which is why this surfaced as an intermittent CI failure rather than a constant one.
The fix
Treat
io.EOFfrom the send loop as gRPC's "the status is on the receive side" signal and fall through toCloseAndRecv, which already runs immediately after in all four cases. Any other send error still returns as before. The contract is documented once onsendSnapshotChunk, the leaf both snapshot helpers go through.Behavior change / risk
Callers that previously saw
io.EOFfrom these four paths now see the peer's real status. Nothing that previously succeeded can now fail: the fix only widens which errors are reported accurately, and theCloseAndRecvit falls through to was already the next statement on the success path.Test evidence
Two new tests, both reproducing before the fix:
adapter/s3_blob_push_status_test.go— a server that refuses the push before reading a frame; reproduced the exact CI signature (expected0x10, actual0x2) 3/3 before the fix.internal/raftengine/etcd/grpc_transport_snapshot_status_test.go— three subtests, one per snapshot sender.Revert-check, one guard at a time — each reverted guard fails exactly the test that covers it, and the file restores byte-exact (
diff -q):streamFSMSnapshotTestSnapshotSendersSurfaceTheReceiversStatus/streamed_FSM_snapshotsendSnapshot…/in-memory_payloadsendSnapshotSpool…/spooled_payloadPushChunkBlobTestPushChunkBlobSurfacesTheServersStatusWhenItRejectsEarlyTwo things had to be tuned before the tests measured anything, both recorded in comments so the next reader does not undo them:
ResourceExhausted— which the client produces locally for an oversized message. It passed against the buggy code. Now 1 MiB, assertingFailedPrecondition, a status the client cannot manufacture.snapshotChunkSizeto 32 KiB. At the 16 MiB default the payload is a single write, which can win the race against the reset; that left the in-memory path passing when reverted.go test ./adapter/ ./internal/raftengine/... -race -count=1 -timeout 40m→ok adapter 662.431s,ok etcd 26.387s,ok transportsoak 1.700sgolangci-lint --config=.golangci.yaml run ./adapter/... ./internal/raftengine/...→ 0 issuesSelf-review
errors.Ison an error path that was already returning. No hot-path or allocation change.Note for #1249
#1249's red
test (ubuntu-latest)is this bug, not its own change. It should go green once this merges and #1249 picks upmain.https://claude.ai/code/session_013rNHooj7NF3giihWVba8QE
Summary by CodeRabbit