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 |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (4)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughS3チャンク送信とRaftスナップショット送信で、送信中の ChangesgRPC終了ステータスの伝播
Priority: ⬇️ Low Estimated code review effort: 2 (Simple) | ~12 minutes Change: Bug fix 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❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
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". |
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