Skip to content

khepri_cluster: Handle a Ra server restart in parallel of a local reset - #440

Merged
dumbbell merged 2 commits into
v0.17.xfrom
fix-race-condition-during-cluster-join
Sep 22, 2026
Merged

dumbbell merged 2 commits into
v0.17.xfrom
fix-race-condition-during-cluster-join

Conversation

@dumbbell

Copy link
Copy Markdown
Collaborator

Why

We had one occurence in the RabbitMQ CI of the following race condition:

  • The caller restarts the local Khepri store and asks it to rejoin a cluster.
  • As part of the cluster join, the local node is removed from the remote cluster and is reset locally.
  • It was already part of the target cluster and it receives an "Append entries" RPC upon restart which it does not expect because it was just reset. That's the race condition: the cluster removal and reset crossed the RPC.
  • Because the RPC was unexpected, the store crashes and it is restarted by its supervisor.

At that point, Khepri did not handle the situation because it assumed the Ra server was terminated and it discarded the store properties stored in a persistent_term.

So, this first join attempt returned an error near the end because the Ra server was restarted behind its back. Subsequent attempts by the caller to rejoin the cluster led to a case_clause exception in check_status_and_join_locked/3 because the function does not expect the Ra server to run without store properties available.

How

To fix this, reset_locally_and_join_locked/6 is split and modified to handle the fact that restarting the Ra server after a reset might return the already_started error.

If this happens, the Ra server is forced to stop again and the start+join is tried again.

The added testcase simulates the race condition by using meck and by sending the "Append entries" RPC to the Ra server. Therefore, it knows about Ra internals which is not great.

[Why]
This is the variant taking a timeout as its third argument. It should
have been exported from day 1. The documentation assumes that.
@dumbbell dumbbell added this to the v0.17.10 milestone Sep 21, 2026
@dumbbell dumbbell self-assigned this Sep 21, 2026
@dumbbell dumbbell added the bug Something isn't working label Sep 21, 2026
@codecov

codecov Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.92308% with 3 lines in your changes missing coverage. Please review.
βœ… Project coverage is 90.13%. Comparing base (42bb381) to head (ad1330e).

Files with missing lines Patch % Lines
src/khepri_cluster.erl 76.92% 3 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           v0.17.x     #440      +/-   ##
===========================================
+ Coverage    90.08%   90.13%   +0.05%     
===========================================
  Files           22       22              
  Lines         3559     3569      +10     
===========================================
+ Hits          3206     3217      +11     
+ Misses         353      352       -1     
Flag Coverage Ξ”
erlang-26 90.10% <76.92%> (+0.05%) ⬆️
erlang-27 90.02% <76.92%> (-0.03%) ⬇️
erlang-28 89.99% <76.92%> (-0.06%) ⬇️
erlang-29 90.02% <76.92%> (+0.19%) ⬆️
os-ubuntu-latest 90.13% <76.92%> (+0.05%) ⬆️
os-windows-latest 90.10% <76.92%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

β˜” View full report in Codecov by Harness.
πŸ“’ Have feedback on the report? Share it here.

πŸš€ New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@dumbbell
dumbbell force-pushed the fix-race-condition-during-cluster-join branch from b61b4a4 to c37d6fb Compare September 21, 2026 17:54
[Why]
We had one occurence in the RabbitMQ CI of the following race condition:
* The caller restarts the local Khepri store and asks it to rejoin a
  cluster.
* As part of the cluster join, the local node is removed from the remote
  cluster and is reset locally.
* It was already part of the target cluster and it receives an "Append
  entries" RPC upon restart which it does not expect because it was just
  reset. That's the race condition: the cluster removal and reset
  crossed the RPC.
* Because the RPC was unexpected, the store crashes and it is restarted
  by its supervisor.

At that point, Khepri did not handle the situation because it assumed
the Ra server was terminated and it discarded the store properties
stored in a `persistent_term`.

So, this first join attempt returned an error near the end because the
Ra server was restarted behind its back. Subsequent attempts by the
caller to rejoin the cluster led to a `case_clause` exception in
`check_status_and_join_locked/3` because the function does not expect
the Ra server to run without store properties available.

[How]
To fix this, `reset_locally_and_join_locked/6` is split and modified to
handle the fact that restarting the Ra server after a reset might return
the `already_started` error.

If this happens, the Ra server is forced to stop again and the
start+join is tried again.

The added testcase simulates the race condition by using meck and by
sending the "Append entries" RPC to the Ra server. Therefore, it knows
about Ra internals which is not great.
@dumbbell
dumbbell force-pushed the fix-race-condition-during-cluster-join branch from c37d6fb to ad1330e Compare September 21, 2026 18:24
@github-actions

Copy link
Copy Markdown
Contributor

@dumbbell
dumbbell marked this pull request as ready for review September 22, 2026 07:58
@dumbbell
dumbbell merged commit 53f0f1d into v0.17.x Sep 22, 2026
32 checks passed
@dumbbell
dumbbell deleted the fix-race-condition-during-cluster-join branch September 22, 2026 08:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant