Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

khepri_machine: Add command deduplication mechanism #250

Merged
merged 2 commits into from
May 15, 2024

Conversation

dumbbell
Copy link
Member

@dumbbell dumbbell commented Feb 20, 2024

Why

The "client" side of khepri_machine implemented in process_sync_command/3 have a retry mechanism if ra:process_command/3 returns an error such as noproc, nodedown or shutdown.

However, this retry mechanism can't tell if the state machine already received the command and just couldn't reply, for instance because there is a node stopping or a change of leadership.

Therefore, it's possible that the same command is submitted twice and thus processed twice.

That's ok for idempotent commands, but it may not be alright for all transactions for example. That's why we need a deduplication mechanism that ensures the same command is not applied multiple times.

How

Two new commands are introduced to implement the deduplication system:

  • #dedup{} which is used to wrap the command to protect and assign a unique reference to it
  • #dedup_ack{} which is used at the end of the retry loop to let the state machine know that the "client" side received the reply

When the state machine receives a command wrapped into a #dedup{} command, it will remember the reply for the initial processing of that command. For any subsequent copies of the same #dedup{} (based on the unique reference), the state machine will not apply the wrapped command and will simply returned the reply it remembered from the first application.

Later when the state machine receives a #dedup_ack{}, it will drop the cached reply for that reference.

Just in case the client never sends a #dedup_ack{}, the state machine will drop any expired cached entries. The expiration time is based on the command timeout. If it's infinity, it defaults to 15 minutes.

This whole deduplication mechanism can be enabled or disabled through the new protect_against_dups command option which takes a boolean. This option is off by default, except for R/W transactions.

Thus if the caller knows the transation is idempotent, it can decide to turn the dedup mechanism off.

V2: We now use the effective_machine_version counter provided by ra_counters:counters/2 if it is available as it is faster than querying the Ra server. If the counter is unavailable, we fall back to the query. The new counter is added by rabbitmq/ra#426 and will be used once a Ra release contains this change.

@dumbbell dumbbell added the bug Something isn't working label Feb 20, 2024
@dumbbell dumbbell added this to the v0.13.0 milestone Feb 20, 2024
@dumbbell dumbbell self-assigned this Feb 20, 2024
Copy link

codecov bot commented Feb 20, 2024

Codecov Report

Attention: Patch coverage is 90.98361% with 11 lines in your changes are missing coverage. Please review.

Project coverage is 89.64%. Comparing base (a478a9e) to head (c6465c2).

Files Patch % Lines
src/khepri_machine.erl 92.30% 8 Missing ⚠️
src/khepri_machine_v0.erl 82.35% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #250      +/-   ##
==========================================
- Coverage   89.70%   89.64%   -0.06%     
==========================================
  Files          20       21       +1     
  Lines        3002     3091      +89     
==========================================
+ Hits         2693     2771      +78     
- Misses        309      320      +11     
Flag Coverage Δ
erlang-24 88.67% <90.16%> (+<0.01%) ⬆️
erlang-25 88.64% <90.98%> (-0.03%) ⬇️
erlang-26 89.29% <90.98%> (-0.05%) ⬇️
os-ubuntu-latest 89.58% <90.98%> (+<0.01%) ⬆️
os-windows-latest 89.38% <90.98%> (-0.06%) ⬇️

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

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@dumbbell
Copy link
Member Author

I need to bump the state machine version and convert the state.

@dumbbell dumbbell force-pushed the test-duplicated-commands branch 2 times, most recently from d1ee73c to 16394e5 Compare February 23, 2024 17:44
@dumbbell
Copy link
Member Author

dumbbell commented Mar 12, 2024

The pull request is ready for review. The machine version is taken into account, so if the machine is too old, the public API won’t use the dedup mechanism.

I tested this patch successfully with rolling_upgrade_SUITE in the shovel plugin (the testsuite was failing quite easily without the deduplication).

@dumbbell dumbbell force-pushed the test-duplicated-commands branch 2 times, most recently from 5d1bb39 to e76d572 Compare March 28, 2024 14:37
[Why]
When `ra:process_command()` returns an error like `{error, shutdown}`,
it doesn’t mean the command was nod appended to the Ra log. It’s to be
expected with networks, but something I forgot in Khepri.

The retry mechanism we have will call `ra:process_command()` again after
specific errors. Therefore, a command may be appended and thus applied
twice. We observed exactly this in a testcase of the rabbitmq-shovel
plugin.

To help us work on a solution, we need a smaller testcase.

[How]
This new testcase reproduces the problem by spamming a Khepri store with
transactions that bump a counter, while the cluster is changed to
trigger an election.

At the end, the counter should be equal to the number of times the
transaction was executed.
[Why]
The "client" side of `khepri_machine` implemented in
`process_sync_command/3` have a retry mechanism if
`ra:process_command/3` returns an error such as `noproc`, `nodedown` or
`shutdown`.

However, this retry mechanism can't tell if the state machine already
received the command and just couldn't reply, for instance because there
is a node stopping or a change of leadership.

Therefore, it's possible that the same command is submitted twice and
thus processed twice.

That's ok for idempotent commands, but it may not be alright for all
transactions for example. That's why we need a deduplication mechanism
that ensures the same command is not applied multiple times.

[How]
Two new commands are introduced to implement the deduplication system:
* #dedup{} which is used to wrap the command to protect and assign a
  unique reference to it
* #dedup_ack{} which is used at the end of the retry loop to let the
  state machine know that the "client" side received the reply

When the state machine receives a command wrapped into a #dedup{}
command, it will remember the reply for the initial processing of that
command. For any subsequent copies of the same #dedup{} (based on the
unique reference), the state machine will not apply the wrapped command
and will simply returned the reply it remembered from the first
application.

Later when the state machine receives a #dedup_ack{}, it will drop the
cached reply for that reference.

Just in case the client never sends a #dedup_ack{}, the state machine
will drop any expired cached entries. The expiration time is based on
the command timeout. If it's infinity, it defaults to 15 minutes.

This whole deduplication mechanism can be enabled or disabled through
the new `protect_against_dups` command option which takes a boolean.
This option is off by default, except for R/W transactions.

Thus if the caller knows the transation is idempotent, it can decide to
turn the dedup mechanism off.

Because the state machine's state grows with a new field and handles two
new commandes, we bump the machine version from 0 to 1.

V2: We now use the `effective_machine_version` counter provided by
    `ra_counters:counters/2` if it is available as it is faster than
    querying the Ra server. If the counter is unavailable, we fall back
    to the query. The new counter is added by rabbitmq/ra#426 and will
    be used once a Ra release contains this change.
@dumbbell dumbbell merged commit 5f6f3e6 into main May 15, 2024
12 checks passed
@dumbbell dumbbell deleted the test-duplicated-commands branch May 15, 2024 20:22
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.

None yet

2 participants