Repository navigation
Fix livelock when all TaskGroup runqueues are full (#1038) - #3633
two-headBoy wants to merge 9 commits into
Conversation
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
4 open issues
_rq.push(...)is called without checking whether it succeeded. In this function we’re explicitly… · New_rq.push(...)is called without checking whether it succeeded. In this function we’re explicitly… · New This enqueuescurrent_tid()onto_rqbefore the current task has actually been scheduled out… · New This enqueuescurrent_tid()onto_rqbefore the current task has actually been scheduled out… · New
What changed in this PR
Addresses a scheduler livelock when all TaskGroup local runqueues are full by making progress (running a queued task inline) instead of sleeping/retrying forever under full-queue conditions.
Changes:
- Replace the “sleep and retry” behavior on
_rqfull with “pop one task, run it in place, then retry push”. - Add special handling for pthread-mode tasks to avoid illegal switches and prevent accidentally dropping popped pthread tasks.
| File | Description |
|---|---|
| src/bthread/task_group_inl.h | Changes TaskGroup::push_rq() to drain work when _rq is full, preventing livelock and handling pthread-mode edge cases. |
Review effort: Lite
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 issues
4 resolved since last review
This enqueuescurrent_tid()onto_rqbefore the current task has actually been scheduled out… This enqueuescurrent_tid()onto_rqbefore the current task has actually been scheduled out…_rq.push(...)is called without checking whether it succeeded. In this function we’re explicitly…_rq.push(...)is called without checking whether it succeeded. In this function we’re explicitly…
Review effort: Lite
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
4 open issues
The scan for a non-pthread task is capped at 16 entries. If there are >16 consecutive pthread-mode… · New The scan for a non-pthread task is capped at 16 entries. If there are >16 consecutive pthread-mode… · New UnderBTHREAD_FAIR_WSQ,pop_task()uses_rq.steal()rather than an owner-side pop. If… · New This code path is explicitly a resource-exhaustion/overload path (full runqueue). ACHECK(pushed)… · New
2 resolved since last review
Review effort: Lite
wwbmmm
left a comment
There was a problem hiding this comment.
[replied by brpc-oncall robot] Deferring the publication of the outgoing task through set_remained() fixes the publish-before-switch race, but the remaining implementation still has two blockers: sched_to() can be re-entered from remained callbacks (where _sched_recursive_guard is 1, so non-NDEBUG builds abort), and after the switch the current task may have been stolen, so this is no longer the running TaskGroup. Please also add a regression test under test/.
wwbmmm
left a comment
There was a problem hiding this comment.
[replied by brpc-oncall robot] The new pthread-task scanning and the CHECK on the re-queue are fine, but the two blockers from the previous review are untouched: sched_to() can still be re-entered from within a remained callback (debug builds abort), and after the switch the current task may have been stolen, so this is no longer the running TaskGroup. A regression test is still missing.
|
Thanks for the detailed reviews. The latest five commits address the identified issues:
cc @wwbmmm |
|
I think we should be very careful to merge this PR. It change the core behavior of bthread. This issue exists for a long time but no one succeed to fix it before. |
|
@chenBright What do you think about this PR? |
Agree. Making push_rq() a potential context-switching point changes a fundamental assumption across its callers, including bthread_start_background() and internal wakeup paths. |
| // push_rq() may switch context and the current task may be resumed by | ||
| // another group. Use the current group for all subsequent accounting. | ||
| TaskGroup* g = BAIDU_GET_VOLATILE_THREAD_LOCAL(tls_task_group); |
There was a problem hiding this comment.
push_rq() can now suspend the caller and resume it on another worker. Updating the local pointer in ready_to_run() does not update its callers: after butex_wake_except() wakes many joiners, task_runner() still uses its old g to install _release_last_context and call ending_sched().
| args.meta = g->_cur_meta; | ||
| g->set_remained(ready_to_run_in_worker_ignoresignal, &args); | ||
| } | ||
| // The main task is never queued; ending_sched() resumes it via | ||
| // _main_tid when the queue drains. Jumping to a pthread-mode task | ||
| // from a normal bthread resumes the dispatch loop on _main_stack | ||
| // which runs it inline, same as ending_sched() does. | ||
| sched_to(&g, run_tid); |
There was a problem hiding this comment.
This switch makes bthread_start_background() an implicit scheduling point. Existing callers, including ExecutionQueueBase::start_execute(), deliberately use background submission because they may hold a pthread mutex.
|
Thanks for the reviews. I agree the root problem is the design: making I’ll abandon this implementation and close this PR. |


What problem does this PR solve?
Issue Number: Fixes #1038
Problem Summary:
When
_rqis full,push_rq()sleeps the worker thread and retries. Theowner worker is the only consumer of
_rq(other workers only steal), soonce every worker spins there, no one drains any queue and the whole
scheduler livelocks.
What is changed and the side effects?
Changed:
When
_rqis full, pop one pending task and run it in place, then retrythe push. The pop frees one slot, which guarantees the push of the task
being scheduled out succeeds, since
_rqhas a single producer andstealers never push; the original push is then retried until the queue
has room. The task being scheduled out is saved with
current_tid()(remained-callbacks re-enter
push_rq()on the next task's stack).Pthread-mode tasks cannot be switched out, so they fall back to waiting,
and a pthread-mode task popped by the main task is put back instead of
being silently dropped.
Verified with a recursive spawner repro (every task spawns more tasks until stopped):
before the fix all workers froze in the usleep loop with CPU ~0%;
after the fix workers kept draining the queues (CPU ~400%) and the echo_c++ smoke test passes.
Side effects:
Note: this fix guarantees scheduler liveness, not per-task completion
deadlines; rate limiting under sustained overload remains the
application's responsibility. The window where every worker is inside a
pthread-mode task when its queue fills is left as-is, consistent with
the existing stance for pthread-mode tasks in
exchange().Check List:
https://github.com/apache/brpc/blob/master/CODE_OF_CONDUCT.md.