Skip to content

Commit c09290f

Browse files
committed
docs: Add FAQ explaining the interleaved change-membership in parallel
1 parent ef090e1 commit c09290f

File tree

2 files changed

+74
-11
lines changed

2 files changed

+74
-11
lines changed

openraft/src/docs/faq/faq-toc.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
* [How to initialize a cluster?](#how-to-initialize-a-cluster)
1111
* [Are there any issues with running a single node service?](#are-there-any-issues-with-running-a-single-node-service)
1212
* [How do I store additional information about nodes in Openraft?](#how-do-i-store-additional-information-about-nodes-in-openraft)
13-
* [How to remove node-2 safely from a cluster `{1, 2, 3}`?](#how-to-remove-node-2-safely-from-a-cluster-1-2-3)
1413
* [What actions are required when a node restarts?](#what-actions-are-required-when-a-node-restarts)
1514
* [What will happen when data gets lost?](#what-will-happen-when-data-gets-lost)
1615
* [Can I wipe out the data of ONE node and wait for the leader to replicate all data to it again?](#can-i-wipe-out-the-data-of-one-node-and-wait-for-the-leader-to-replicate-all-data-to-it-again)
17-
* [Is Openraft resilient to incorrectly configured clusters?](#is-openraft-resilient-to-incorrectly-configured-clusters)
16+
* [Is Openraft resilient to incorrectly configured clusters?](#is-openraft-resilient-to-incorrectly-configured-clusters)
17+
- [Membership config](#membership-config)
18+
* [How to remove node-2 safely from a cluster `{1, 2, 3}`?](#how-to-remove-node-2-safely-from-a-cluster-1-2-3)
19+
* [Does OpenRaft support calling `change_membership` in parallel?](#does-openraft-support-calling-change_membership-in-parallel)

openraft/src/docs/faq/faq.md

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ struct MyNode {
142142

143143
```rust,ignore
144144
openraft::declare_raft_types!(
145-
pub MyRaftConfig:
146-
// ...
145+
pub MyRaftConfig:
146+
// ...
147147
NodeId = u64, // Use the appropriate type for NodeId
148148
Node = MyNode, // Replace BasicNode with your custom node type
149149
// ... other associated types
@@ -153,13 +153,6 @@ openraft::declare_raft_types!(
153153
Use `MyRaftConfig` in your Raft setup to utilize the custom node structure.
154154

155155

156-
### How to remove node-2 safely from a cluster `{1, 2, 3}`?
157-
158-
Call `Raft::change_membership(btreeset!{1, 3})` to exclude node-2 from
159-
the cluster. Then wipe out node-2 data.
160-
**NEVER** modify/erase the data of any node that is still in a raft cluster, unless you know what you are doing.
161-
162-
163156
### What actions are required when a node restarts?
164157

165158
None. No calls, e.g., to either [`add_learner()`][] or [`change_membership()`][]
@@ -237,6 +230,74 @@ pub(crate) fn following_handler(&mut self) -> FollowingHandler<C> {
237230
```
238231

239232

233+
## Membership config
234+
235+
236+
### How to remove node-2 safely from a cluster `{1, 2, 3}`?
237+
238+
Call `Raft::change_membership(btreeset!{1, 3})` to exclude node-2 from
239+
the cluster. Then wipe out node-2 data.
240+
**NEVER** modify/erase the data of any node that is still in a raft cluster, unless you know what you are doing.
241+
242+
243+
### Does OpenRaft support calling `change_membership` in parallel?
244+
245+
Yes, OpenRaft does support this scenario, but with some important caveats.
246+
`change_membership` is a two-step process—first transitioning to a joint
247+
configuration and then to a final uniform configuration. When multiple
248+
`change_membership` calls occur concurrently, their steps can interleave,
249+
potentially leaving the cluster in a joint config state. This state is valid in
250+
OpenRaft.
251+
252+
Here's an example of how such interleaving might play out:
253+
254+
1. **Initial State**: `{y}`
255+
2. **Task 1** calls `change_membership(AddVoters(x))`
256+
→ Transitions to joint config `[{y}, {y, x}]`
257+
3. **Task 2** calls `change_membership(RemoveVoters(x))`
258+
→ Transitions from `[{y}, {y, x}]` to `[{y, x}, {y}]`
259+
4. **Task 2 (Step 2)** finalizes config to `{y}` and reports success to its client
260+
5. **Task 1 (Step 2)** proceeds unaware, adding `x` again
261+
→ Transitions from `{y}` to `[{y}, {y, x}]`
262+
263+
At this point, both tasks report success to their respective clients, but the
264+
cluster is left in a **joint configuration** state: `[{y}, {y, x}]`.
265+
266+
This illustrates how concurrent changes can lead to a final configuration that
267+
may not reflect the full intent of either request. However, this does **not**
268+
violate OpenRaft’s consistency model, and joint configs are treated as valid
269+
operating states.
270+
271+
If your system ensures only one process issues `change_membership` at a time,
272+
you're safe. If not, always validate the final config after changes to avoid
273+
surprises.
274+
For example, if two requests attempt to change the config to `{a,b,c}` and
275+
`{x,y,z}` respectively, the result may end up being one or the
276+
other—unpredictable from each individual caller’s perspective.
277+
278+
This behavior is by design, as it provides several advantages:
279+
280+
- **Simplifies recovery**: If the leader crashes after the first step, a new
281+
leader can continue operating from the joint state without requiring special
282+
recovery procedures.
283+
284+
- **Supports dynamic flexibility**: The cluster can adapt to changing conditions
285+
without being locked into a specific membership transition.
286+
287+
OpenRaft intentionally supports this behavior because:
288+
289+
- When a leader crashes after establishing a joint configuration, the new leader
290+
can seamlessly resume from this state and process new membership changes as
291+
needed.
292+
293+
- The system can adapt if nodes in the target configuration become unstable. For
294+
example, if transitioning from `{a,b,c}` to `{b,c,d}` but node `d` becomes
295+
unreliable when it finished phase-1 and the config is `[{a,b,c}, {b,c,d}]`,
296+
the leader can pivot to include a different node (e.g., change membership
297+
config to `[{a,b,c}, {b,c,x}]` then to `{b,c,x}`) or revert to the original
298+
configuration `{a,b,c}`.
299+
300+
240301
[`Linearizable Read`]: `crate::docs::protocol::read`
241302
[`leader_id`]: `crate::docs::data::leader_id`
242303

0 commit comments

Comments
 (0)