Repository navigation
Fix SelectiveChannel handling of late subcall callbacks - #3600
Merged
Merged
Conversation
Discard subcall results after the main RPC has finished while still returning their resources. Release the main call-id lock for non-final callbacks and let the final callback complete cleanup. Add deterministic coverage for late timeout/cancel callbacks, callback ordering, controller destruction, and error propagation and retries before completion. Tests: brpc_channel_unittest (87 passed).
Contributor
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
2 open findings
What changed in this PR
Fixes SelectiveChannel behavior so late subcall callbacks cannot continue modifying the main RPC result after the main RPC has already finished, while still allowing late callbacks to safely return subcall resources.
Changes:
- Update
Sender::PushFree()to immediately stop result processing once_finishedis true (but still allow resource return/cleanup). - Add deterministic unit tests covering late callback ordering, retryable/non-retryable/success results, and controller destruction inside user done callbacks.
- Add tests ensuring retries and error propagation still behave normally before the main RPC completes.
| File | Description |
|---|---|
| test/brpc_channel_unittest.cpp | Adds regression and behavior-preservation tests for late subcall callbacks and pre-completion retry/error handling. |
| src/brpc/selective_channel.cpp | Changes Sender::PushFree() logic to stop processing late results after finish while still draining subcall resources safely. |
🧠 Review effort: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+3501
to
+3502
| ASSERT_EQ(nullptr, cntl->_lb.get()); | ||
| ASSERT_EQ(0, done_count); |
Comment on lines
+128
to
+132
| ++calls; | ||
| return cntl->ErrorCode() == 5013; | ||
| } | ||
|
|
||
| mutable int calls = 0; |
Contributor
|
LGTM |
Copilot stopped reviewing on behalf of
wwbmmm due to an error
October 13, 2026 10:19
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


What problem does this PR solve?
Issue Number: N/A
Problem Summary:
Late SelectiveChannel subcall callbacks can continue processing results after the main RPC has finished, as long as other subcalls remain outstanding. This can modify the completed RPC result and, in the older version identified by the core dump, trigger a retry that dereferences a cleared load balancer.
The issue was identified from
core-mtable-stablete-270416-1790555285, generated bymtable-stabletestwhile callingmtable.tableserver.Sdk.ReadTable.The main RPC used a
1000mstimeout, a996msbackup request delay, and a maximum of three retries. Its customMTableRetryPolicyallowed retries for error5013(PARTITION_NOT_READY).The failure sequence was:
1000msdeadline.ERPCTIMEDOUT(1008). In the affected older version,Controller::EndRPC()cleared the main controller’s_lbbefore invokingSender::Run().Sender::Run()set_finished=true. Since subcalls were still outstanding, it released the main call-id lock, signaled cancellation to the subcalls, and waited for their resources to be returned.saved_errorwas1008, confirming that the main RPC had already timed out.PushFree()returned the subcall resource, leaving_nfree=1and_nalloc=2. The previous implementation returnedfalseonly when_finished && _nfree == _nalloc. Consequently, this callback was allowed to continue processing its result even though_finishedwas alreadytrue.SubDone::Run()wrote E5013 into the main controller and calledOnVersionedRPCReturned(). The subcall’s correlation ID matched the current call, so the stale-call filtering did not reject it.MTableRetryPolicy::DoRetry()accepted E5013. The retry count increased from two to three, and execution enteredSender::IssueRPC().Sender::IssueRPC()accessed the main controller’s already-cleared_lb. The faulting instruction wasmov 0x18(%rbx), %rdi, withRBX=0, causing a SIGSEGV at address0x18.The core contained the following state:
_finishedtrue_nfree / _nalloc1 / 2_lbnullptr1008(ERPCTIMEDOUT)5013(PARTITION_NOT_READY)3The main controller’s error text also recorded E5013 after the timeout:
The retry start timestamp was
3μsafter the main RPC’s end timestamp and75μsafter its deadline. These timestamps support the failure sequence, although they do not constitute a complete thread execution trace. The main controller’s_error_code=0at the crash resulted from clearing the error for the new retry; it did not indicate a successful RPC.The current repository already has an ending-state guard and a separate load balancer reference in Sender, which protect against the same retry/crash path. However, late callbacks can still modify the main controller’s error text, response, and attachment before reaching that guard. This PR fixes the underlying callback handling issue.
What is changed and the side effects?
Changed:
Sender::PushFree()stop result processing whenever_finishedistrue, regardless of whether all subcall resources have been returned.Clear().Validation:
brpc_channel_unittestpassed.Side effects:
Check List: