@@ -219,12 +219,40 @@ AsyncReplResult<> RaftReplDev::start_replace_member(std::string& task_id, const
219219 boost::uuids::to_string (member_out.id ), boost::uuids::to_string (member_in.id ));
220220
221221 // Step1, validate request
222+ bool is_leader = m_my_repl_id == get_leader_id ();
223+ // Check if leader itself is requested to move out.
224+ if (m_my_repl_id == member_out.id ) {
225+ // immediate=false successor=-1, nuraft will choose an alive peer with highest priority as successor, and wait
226+ // until the successor finishes the catch-up of the latest log, and then resign. Return NOT_LEADER and let
227+ // client retry.
228+ if (is_leader) {
229+ RD_LOGI (trace_id, " Step1. Replace member, leader is the member_out so yield leadership, task_id={}" , task_id);
230+ raft_server ()->yield_leadership (false /* immediate */ , -1 /* successor */ );
231+ }
232+ RD_LOGE (trace_id, " Step1. Replace member, I am not leader, can not handle the request, task_id={}" , task_id);
233+ return make_async_error<>(ReplServiceError::NOT_LEADER);
234+ }
235+ if (commit_quorum >= 1 ) {
236+ // Reduce the quorum size BEFORE checking leadership. When two members are down, the raft leader will
237+ // eventually yield its leadership after leadership_expiry (default: 20x heartbeat interval) because it
238+ // cannot reach majority. Once leadership is lost, the remaining single node cannot elect itself without a
239+ // reduced election quorum. By calling reset_quorum_size here first (which sets both custom_commit_quorum
240+ // and custom_election_quorum to 1), the current leader is able to maintain leadership, and if leadership
241+ // was already lost, the node will self-elect on the next election timeout. The caller should retry on
242+ // NOT_LEADER to allow time for self-election to complete.
243+ reset_quorum_size (commit_quorum, trace_id);
244+ }
245+
246+ if (!is_leader) { return make_async_error<>(ReplServiceError::NOT_LEADER); }
247+
248+ // I am leader and not out_member
222249 // TODO support rollback, this could happen when the first task failed, and we want to launch a new task to
223250 // remediate it. Need to rollback the first task. And for the same task, it's reentrant and idempotent.
224251 auto existing_task_id = get_replace_member_task_id ();
225252 if (!existing_task_id.empty () && existing_task_id != task_id) {
226253 RD_LOGE (trace_id, " Step1. Replace member, task_id={} is not the same as existing task_id={}" , task_id,
227254 existing_task_id);
255+ reset_quorum_size (0 , trace_id);
228256 return make_async_error<>(ReplServiceError::REPLACE_MEMBER_TASK_MISMATCH);
229257 }
230258
@@ -239,18 +267,10 @@ AsyncReplResult<> RaftReplDev::start_replace_member(std::string& task_id, const
239267 return make_async_success<>();
240268 }
241269 RD_LOGE (trace_id, " Step1. Replace member invalid parameter, out member is not found, task_id={}" , task_id);
270+ reset_quorum_size (0 , trace_id);
242271 return make_async_error<>(ReplServiceError::SERVER_NOT_FOUND);
243272 }
244- if (m_my_repl_id != get_leader_id ()) { return make_async_error<>(ReplServiceError::NOT_LEADER); }
245- // Check if leader itself is requested to move out.
246- if (m_my_repl_id == member_out.id ) {
247- // immediate=false successor=-1, nuraft will choose an alive peer with highest priority as successor, and wait
248- // until the successor finishes the catch-up of the latest log, and then resign. Return NOT_LEADER and let
249- // client retry.
250- raft_server ()->yield_leadership (false /* immediate */ , -1 /* successor */ );
251- RD_LOGI (trace_id, " Step1. Replace member, leader is the member_out so yield leadership, task_id={}" , task_id);
252- return make_async_error<>(ReplServiceError::NOT_LEADER);
253- }
273+
254274 // quorum safety check. TODO currently only consider lsn, need to check last response time.
255275 auto active_peers = get_active_peers ();
256276 // active_peers doesn't include leader itself.
@@ -272,18 +292,15 @@ AsyncReplResult<> RaftReplDev::start_replace_member(std::string& task_id, const
272292 " Step1. Replace member, quorum safety check failed, active_peers={}, "
273293 " active_peers_exclude_out/in_member={}, required_quorum={}, commit_quorum={}, task_id={}" ,
274294 active_peers.size (), active_num, quorum, commit_quorum, task_id);
295+ reset_quorum_size (0 , trace_id);
275296 return make_async_error<>(ReplServiceError::QUORUM_NOT_MET);
276297 }
277298
278- if (commit_quorum >= 1 ) {
279- // Two members are down and leader cant form the quorum. Reduce the quorum size.
280- reset_quorum_size (commit_quorum, trace_id);
281- }
282-
283299 // Step 2: Handle out member.
284300#ifdef _PRERELEASE
285301 if (iomgr_flip::instance ()->test_flip (" replace_member_set_learner_failure" )) {
286302 RD_LOGE (trace_id, " Simulating set member to learner failure" );
303+ reset_quorum_size (0 , trace_id);
287304 return make_async_error (ReplServiceError::FAILED);
288305 }
289306#endif
0 commit comments