nodedb_cluster/metadata_group/compensation.rs
1// SPDX-License-Identifier: BUSL-1.1
2
3//! Compensation actions applied when a migration is aborted.
4//!
5//! A `MigrationAbort` entry carries an ordered list of these
6//! compensations. The applier executes them in order; any failure
7//! is fatal (no warn-and-continue — a partial abort is as broken
8//! as a partial commit).
9
10use serde::{Deserialize, Serialize};
11
12/// A single compensation step emitted as part of a `MigrationAbort` entry.
13///
14/// Applied by `CacheApplier` in the order listed; all must succeed or
15/// the abort itself is fatal.
16#[derive(
17 Debug,
18 Clone,
19 PartialEq,
20 Eq,
21 Serialize,
22 Deserialize,
23 zerompk::ToMessagePack,
24 zerompk::FromMessagePack,
25)]
26pub enum Compensation {
27 /// Remove a learner that was added by Phase 1 but never promoted.
28 ///
29 /// Safe to apply idempotently: if the learner is already absent,
30 /// the routing-table `remove_group_member` call is a no-op.
31 RemoveLearner { group_id: u64, peer_id: u64 },
32
33 /// Remove a voter that was promoted in Phase 2 but the cut-over
34 /// never committed.
35 ///
36 /// Safe to apply idempotently for the same reason as `RemoveLearner`.
37 RemoveVoter { group_id: u64, peer_id: u64 },
38
39 /// Restore the routing hint for a group leader.
40 ///
41 /// Applied when the source-leader was changed by a Phase 2
42 /// `PromoteLearner` but the `LeadershipTransfer` never committed.
43 /// Tells the routing table to point back to the original leader.
44 RestoreLeaderHint { group_id: u64, peer_id: u64 },
45
46 /// Remove a ghost stub that was speculatively inserted before the
47 /// cut-over commit, and the commit subsequently failed.
48 RemoveGhostStub { vshard_id: u32 },
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54
55 #[test]
56 fn compensation_zerompk_roundtrip() {
57 let cases = [
58 Compensation::RemoveLearner {
59 group_id: 1,
60 peer_id: 42,
61 },
62 Compensation::RemoveVoter {
63 group_id: 2,
64 peer_id: 7,
65 },
66 Compensation::RestoreLeaderHint {
67 group_id: 3,
68 peer_id: 99,
69 },
70 Compensation::RemoveGhostStub { vshard_id: 10 },
71 ];
72 for c in &cases {
73 let bytes = zerompk::to_msgpack_vec(c).expect("encode");
74 let decoded: Compensation = zerompk::from_msgpack(&bytes).expect("decode");
75 assert_eq!(*c, decoded);
76 }
77 }
78}