Skip to content

Fix livelock when all TaskGroup runqueues are full (#1038) - #3633

Open
two-headBoy wants to merge 2 commits into
apache:masterfrom
two-headBoy:fix-rq-full-livelock
Open

two-headBoy wants to merge 2 commits into
apache:masterfrom
two-headBoy:fix-rq-full-livelock

Conversation

@two-headBoy

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: Fixes #1038

Problem Summary:
When _rq is full, push_rq() sleeps the worker thread and retries. The
owner worker is the only consumer of _rq (other workers only steal), so
once every worker spins there, no one drains any queue and the whole
scheduler livelocks.

What is changed and the side effects?

Changed:
When _rq is full, pop one pending task and run it in place, then retry
the push. The pop frees one slot, which guarantees the push of the task
being scheduled out succeeds, since _rq has a single producer and
stealers 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.

// Repro for brpc issue #1038: all local run queues full -> livelock.
#include <bthread/bthread.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>

static void* spawner(void*) {
    while (true) {
        bthread_t tid;
        // The spawned task is itself a spawner: a worker that steals it
        // is "converted" into a producer.
        bthread_start_background(&tid, nullptr, spawner, nullptr);
    }
    return nullptr;
}

int main() {
    // The concurrency floor is BTHREAD_MIN_CONCURRENCY (=3+epoll, 4 on
    // Linux); setting 1 fails with EINVAL. Use the floor and seed every
    // worker with one spawner.
    const int n = BTHREAD_MIN_CONCURRENCY;
    bthread_setconcurrency(n);
    for (int i = 0; i < n; ++i) {
        bthread_t tid;
        bthread_start_background(&tid, nullptr, spawner, nullptr);
    }
    fprintf(stderr, "spawner x%d started, waiting for livelock...\n", n);
    time_t start = time(nullptr);
    while (true) {
        sleep(1);
        fprintf(stderr, "main alive, %lds elapsed\n", time(nullptr) - start);
    }
    return 0;
}

Side effects:

  • Performance effects: none on the hot path; only runs when the queue is full.
  • Breaking backward compatibility: none.

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:

  • Please make sure your changes are compilable.
  • When providing us with a new feature, it is best to add related tests.
  • Please follow https://github.com/apache/brpc/blob/master/CODE_OF_CONDUCT.md.

@chenBright
chenBright requested a balanced review from Copilot October 20, 2026 14:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
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 _rq full 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

Comment thread src/bthread/task_group_inl.h
Comment thread src/bthread/task_group_inl.h Outdated
Comment thread src/bthread/task_group_inl.h
Comment thread src/bthread/task_group_inl.h
@chenBright
chenBright requested a balanced review from Copilot October 21, 2026 12:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +113 to +123
if (is_current_main_task() &&
address_meta(run_tid)->attr.stack_type == BTHREAD_STACKTYPE_PTHREAD) {
// pthread-mode tasks are run inline on _main_stack by the
// dispatch loops (run_main_task/task_runner). Switching to one
// from the main task itself causes no stack jump and would
// silently drop it, put it back and wait; stealers may run it
// on other workers.
_rq.push(run_tid);
::usleep(1000);
continue;
}
// from the main task itself causes no stack jump and would
// silently drop it, put it back and wait; stealers may run it
// on other workers.
_rq.push(run_tid);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

task control 中所有的 local queue is full 导致死锁&解决方案.

2 participants