Skip to content

Stream Coordinator can leave a stream in stopped in a 2/3 node loss disaster #16822

Description

@the-mikedavis

Describe the bug

When a stream has its writer member on node A and replicas on nodes B and C, when nodes B and C are removed from the cluster in sequence (by forget_cluster_node), the writer on node A is terminated as a side effect of the first delete_replica and is not re-elected. The writer can't be healed by anything in the current stream coordinator API:

  1. evaluate_stream/3 has no branch that re-acts on a writer already in {stopped, _, _}
  2. the only path to {ready, NextEpoch} is the select_leader/2 call inside member_stopped's is_quorum branch, and once the first member_stopped event has fired for the writer, no later event re-evaluates the quorum predicate
  3. the filter_command/3 clause for delete_replica rejects any further delete as last_stream_member

The log is still intact on A, but the writer can't be recovered. This sequence is actually OK as long as the writer was originally on B or C during the disaster event: then the stream can be recovered with a sequence of force-shrinks of Khepri, the Stream Coordinator (and any QQs), and then normal stream membership change commands.

Reproduction steps

I found this behavior by simulating a disaster scenario where 2/3 nodes of a cluster are lost.

  1. 3-node cluster with a stream declared.
  2. Identify the node running the stream writer, call it A.
  3. Kill nodes B and C (SIGTERM, simultaneous or staggered). Wait for rabbit on node B/C down on A. (Treat B and C as permanently lost now, all commands are against A from here on.)
  4. Force-shrink Khepri: rabbitmqctl force_standalone_khepri_boot
  5. Force-shrink the stream coordinator: rabbitmqctl eval 'ra_server_proc:force_shrink_members_to_current_member({rabbit_stream_coordinator, node()}).'
  6. Issue delete_replica for B and then C. (forget_cluster_node also does this.) Note that the writer terminates on A (osiris_writer:terminate/2 ... reason:shutdown) when the first delete_replica is applied, and no started writer ... in 2 is logged.

The condition for triggering the bug is precisely 'the stream's writer was on the surviving node at the time delete_replica is first issued'. Streams whose writer was on one of the dead nodes recover normally via the {down, Pid, Reason} -> {down, Epoch} path in update_stream0/3.

add_replica invoked on the stuck stream fails with:

[warning] queue '<Q>' ... failed to add replica on 'rabbit@<D>':
   {noproc,{gen_batch_server,call,[<OLD_WRITER_PID>,query_replication_state,5000]}}

where <OLD_WRITER_PID> is the PID that terminated at the osiris_writer:terminate line above.

rabbitmq-streams restart_stream <Q> hangs indefinitely and never returns. The command is applied to the Ra state machine, sets target = stopped on the single existing member (a no-op since it is already stopped), but evaluate_stream/3 finds no actionable state transition and never emits a reply effect, so the reply_to Pid waits forever.

Expected behavior

The stream coordinator should either not leave the writer member in a stopped state, or have an API to recover it. I would lean towards the former since that's how it behaves if the writer is on one of the lost nodes.

Additional context

This was reproduced against main from a few weeks ago, 2bd3449. Like QQ/Khepri/Coordinator force-shrinking, we can't guarantee that there won't be data loss in the disaster-recovery, but we can provide tools to recover availability. I think we might be able to fix it simply by re-evaluating is_quorum in member_deleted.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions