Yongzao Dan created RATIS-2661:
----------------------------------
Summary: Recovered Raft group with no persisted configuration has
no path out of STARTING
Key: RATIS-2661
URL: https://issues.apache.org/jira/browse/RATIS-2661
Project: Ratis
Issue Type: Bug
Components: raft-group, server
Affects Versions: 3.2.2
Environment: Apache Ratis 3.2.2, revision
288c032064ce3d168b8a763e248a326459a4a9b7. gRPC transport, three voting peers,
dynamic multi-group usage. Observed in an IoTDB-based 2.0.10.2 deployment. Not
yet verified against newer Ratis releases.
Reporter: Yongzao Dan
Attachments: ratis-3.2.2-uninitialized-group-recovery-sanitized.log
h2. Summary
A dynamically managed Raft group can become permanently unrecoverable if all
peers restart before the initial configuration entry is persisted.
During automatic directory recovery, Ratis reconstructs the group using only
its group ID. If no configuration exists in storage, the recovered group has an
empty peer list. The division remains in STARTING with reason NOT_IN_CONF.
The embedding application still knows the original membership and invokes
setConfiguration with SET_UNCONDITIONALLY, but RaftServerImpl rejects the
request because the lifecycle is not RUNNING. Since a group with an empty
configuration cannot elect a leader, there is no apparent supported path to
restore the membership.
h2. Observed failure sequence
# A new three-peer group was created with the correct initial membership.
# Peer RPC failures prevented the group from forming a majority.
# The group remained at term 0 and reached PRE_VOTE round 7808.
# No Raft log or configuration entry was created; shutdown reported stopIndex =
-1.
# All servers were restarted while preserving their storage directories.
# Each server recovered the group with peers:[] and entered STARTING / FOLLOWER
/ NOT_IN_CONF.
# The application invoked SET_UNCONDITIONALLY with the original three peers.
# Every request failed with ServerNotReadyException because the group was still
STARTING.
# After peer communication recovered, newly created groups elected leaders
normally, but this recovered group remained unavailable.
A sanitized log excerpt containing this sequence is attached.
h2. Suspected root cause
In Ratis 3.2.2, RaftServerProxy.initGroupDir() recovers a directory as:
{code:java}
addGroup(RaftGroup.valueOf(groupId), StartupOption.RECOVER);
{code}
This RaftGroup contains no peers. ServerState.initialize() only replaces that
empty configuration if readRaftConfiguration() returns a persisted
configuration.
RaftServerImpl.start() does not call startAsPeer() when the local peer is
absent from the configuration. It sets the role to FOLLOWER with NOT_IN_CONF,
while the lifecycle remains STARTING. startAsPeer() is the path that
transitions the lifecycle to RUNNING.
RaftServerImpl.setConfigurationAsync() then rejects the recovery request before
inspecting its mode:
{code:java}
assertLifeCycleState(LifeCycle.States.RUNNING);
{code}
The normal configuration-change path also checks for a ready leader. This
creates an unbreakable liveness cycle:
* Membership is required to elect a leader and reach RUNNING.
* RUNNING and a leader are required to restore membership.
Simply relaxing the lifecycle assertion may therefore be insufficient; a
guarded bootstrap path would also need to handle the absence of a leader.
h2. Proposed reproduction
This reproduction is inferred from the production incident and has not yet been
reduced to a standalone Ratis test.
# Start three Ratis servers and add a new group containing all three peers.
# Block peer RPC before the first configuration entry is written or committed.
# Verify that the group remains at term 0 with last log index -1.
# Stop all servers while preserving their group directories.
# Restart them through the StartupOption.RECOVER directory-scanning path.
# Call setConfiguration with SET_UNCONDITIONALLY and the original peer list.
# Verify that every division remains in STARTING and rejects the request.
h2. Expected behavior
Recovery should not leave an uninitialized group in a state with no supported
transition to RUNNING. Ratis should provide a non-destructive way to restore
bootstrap membership when no configuration or Raft log has ever been persisted.
h2. Impact
A transient bootstrap communication failure followed by a restart becomes a
permanent outage for that group. Repeated restarts and configuration retries do
not help. Recovery currently requires out-of-band storage intervention or
application-specific group recreation.
h2. Possible fix directions
* Persist enough bootstrap membership information when the group directory is
created.
* Allow callers to provide membership for recovered group IDs that have no
stored configuration.
* Provide a guarded re-bootstrap operation for STARTING + empty configuration +
empty log. If SET_UNCONDITIONALLY is reused, it would also need a safe
bootstrap path that does not depend on an existing leader.
Is there an existing supported recovery procedure for this state? If so,
documenting that procedure would also help dynamic multi-group applications.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)