Skip to content

[Fix] Address deadlock issue during leader stepdown#530

Open
gu0keno0 wants to merge 9 commits into
baidu:masterfrom
gu0keno0:fix-stepdown-deadlock
Open

[Fix] Address deadlock issue during leader stepdown#530
gu0keno0 wants to merge 9 commits into
baidu:masterfrom
gu0keno0:fix-stepdown-deadlock

Conversation

@gu0keno0

@gu0keno0 gu0keno0 commented May 3, 2026

Copy link
Copy Markdown

Refs:

#531
#323

We observed the following braft's deadlock issue:

th1:

#0  clock_nanosleep
#1  nanosleep
#2  usleep
#3  bthread::TaskGroup::push_rq (this=0x752c72e00000, tid=4294975251)
#4  bthread::TaskGroup::ready_to_run
#5  bthread::TaskGroup::start_background<false>
#6  braft::run_closure_in_bthread_nosig
#7  braft::ClosureQueue::clear
#8  braft::BallotBox::clear_pending_tasks
#9  braft::NodeImpl::step_down (this=0x752c722f7000, term=418)
#10 braft::NodeImpl::check_step_down
#11 braft::NodeImpl::handle_append_entries_request   ← acquired mutex here (lck._M_owns=true)
#12 braft::RaftServiceImpl::append_entries
#13 brpc::policy::ProcessRpcRequest
#14 brpc::ProcessInputMessage
#15 bthread::TaskGroup::task_runner
#16 bthread_make_fcontext

th2:

#0  __lll_lock_wait
#1  pthread_mutex_lock
#2  bthread::internal::pthread_mutex_lock_internal (mutex=0x752c722f7290)
#3  bthread::internal::pthread_mutex_lock_impl<pthread_mutex_t>
#4  bthread::pthread_mutex_lock_impl
#5  pthread_mutex_lock
#6  butil::Mutex::lock
#7  std::unique_lock<butil::Mutex>::lock
#8  std::unique_lock<butil::Mutex>::unique_lock
#9  braft::NodeImpl::apply (this=0x752c722f7000)    ← blocked waiting for mutex
#10 braft::NodeImpl::execute_applying_tasks
#11 bthread::ExecutionQueueBase::_execute
#12 bthread::ExecutionQueueBase::start_execute

...

Thread-1 was performing step-down while thread-2 was trying to apply, thread-1 was spinning on push_rq because there was a spike of other operations running on bthreads around the same time. Therefore, thread-1 was holding NodeImpl.mutex while waiting. Meanwhile, thread-2 was trying to grab NodeImpl.mutex and cannot get it because thread-1 was holding it. Because thread-2 couldn't finish the bthread, no free bthread slot can be freed up, so thread-1 had to spin further on push_rq, causing a deadlock.

When this deadlock happens, braft completely freezes.

This PR moves the scheduling of the "done" closures to be outside of the critical session of NodeImpl.mutex. The new code only drains the closures and append them into a local RAII guard when NodeImpl.mutex was being held; the guard will schedule the closures when NodeImpl.mutex is released. The ordering is guaranteed by constructing the guard before taking the mutex.

Tests:

  1. Added two UTs to exercise exactly the deadlock. When setting flush_done_closures_after_step_down=false, they both deadlocks
  2. Existing tests work fine with flush_done_closures_after_step_down=true.

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.

1 participant