[ https://issues.apache.org/jira/browse/RATIS-2661 ]


    Xinyu Tan deleted comment on RATIS-2661:
    ----------------------------------

was (Author: tanxinyu):
## 1. First-principles analysis

  Raft cannot derive its initial membership from an empty state. Leader 
election, quorum calculation, log commitment, and
  configuration changes all require a known voting configuration. A 
configuration-change protocol can safely transform an existing
  configuration into another configuration, but it cannot establish the first 
configuration when no electorate exists.

  Therefore, the initial membership is necessarily an external bootstrap fact. 
It must be supplied consistently by the embedding
  application, static configuration, or another control-plane service. This 
part cannot be solved by Raft consensus alone because
  consensus has not yet been established.

  ## 2. Responsibility of the external coordinator

  For a newly created multi-replica group, the external coordinator should 
choose:

  1. A unique group ID.
  2. The complete initial membership.
  3. The same peer information for every intended replica.

  It should send the same group ID and membership to all intended peers and 
retry partial failures. Group creation is not an atomic
  distributed operation provided by Raft itself, so the coordinator must expect 
that some peers may accept the request while others
  are temporarily unavailable.

  An empty peer list is useful when adding a new server to an already active 
group: the existing leader can later transfer the
  committed configuration to that server. However, an empty peer list is not a 
valid bootstrap configuration for a brand-new group
  because no leader or quorum exists from which the membership could be learned.

  ## 3. The liveness gap in the current implementation

  Ratis should reliably hand the externally supplied bootstrap membership over 
to Raft. Currently, groupAdd may succeed while the
  initial membership still exists only in memory. The group directory may 
already exist, but the first formal configuration entry
  may not yet have been persisted.

  If every peer restarts during this window, directory recovery retains only 
the group ID and reconstructs the group with an empty
  membership. The recovered group cannot define a quorum, elect a leader, or 
transition to RUNNING.

  At the same time, SET_UNCONDITIONALLY is not a bootstrap escape hatch. It 
requires the division to be RUNNING and ultimately
  relies on a leader-driven configuration-change path. Consequently, membership 
is required to reach RUNNING, while RUNNING and a
  leader are required to restore membership. This creates a genuine liveness 
cycle.

  I was also able to reproduce this behavior with a standalone test against the 
current 3.3.0-SNAPSHOT code, so the problem is not
  limited to the reported 3.2.2 deployment.

  ## 4. Proposed minimal fix

  The simplest fix is to persist the bootstrap membership atomically before 
acknowledging a successful groupAdd.

  Recovery should follow this precedence:

  1. Use the formal configuration reconstructed from the Raft log or persisted 
Raft metadata, if available.
  2. Otherwise, use the persisted bootstrap membership.
  3. Use an empty configuration only when neither source exists.

  The bootstrap record is not a second Raft configuration history. It is only a 
durable creation fact used before Raft has
  successfully recorded its initial configuration. Once the initial 
configuration has been persisted through Raft, the formal Raft
  state must take precedence, and all later membership changes must continue 
through the normal configuration-change protocol.

  ## 5. Required retry and conflict semantics

  To make partial creation safely recoverable, group creation should be 
idempotent:

  1. The group does not exist: persist the bootstrap membership and create it.
  2. The same group ID and the same bootstrap membership are supplied again: 
return success or an idempotent “already created”
     result.

  3. The same group ID is supplied with different bootstrap membership: reject 
it as a conflict.
  4. The group is already active: membership changes must use setConfiguration, 
not another bootstrap request.

  For compatibility with existing empty recovered directories, Ratis could also 
support a narrowly guarded repair path. It should
  only be allowed when the group is demonstrably pristine: STARTING, empty 
membership, empty log, and no snapshot or persisted
  formal configuration. Supplying the original membership in that state 
completes an interrupted bootstrap; it does not overwrite an
  established Raft configuration.

  ## 6. Scope of the change

  This does not require redesigning Raft, leader election, or joint consensus. 
The consensus implementation can remain unchanged.
  The missing piece is the lifecycle boundary between externally coordinated 
group creation and the point at which Raft durably owns
  the configuration.

  If the application wants to abandon a partially created, never-activated 
group and use a different initial membership, it should
  remove the pristine partial copies and preferably create a new group ID. 
Reusing the old group ID with different membership could
  conflict with delayed or retried creation requests and should not be treated 
as an ordinary recovery operation.

> 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
>            Assignee: Xinyu Tan
>            Priority: Major
>              Labels: liveness
>         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)

Reply via email to