Skip to main content

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}