mace/utils/merge_operator.rs
1//! User-provided merge operators (runtime-only, never persisted).
2
3use std::sync::Arc;
4
5/// user-provided merge operator for a bucket
6///
7/// The operator turns a base value plus a chain of merge operands into the logical value:
8///
9/// ```text
10/// apply(base, combine_operands(a, b)) == apply(apply(base, a), b)
11/// ```
12///
13/// `base = None` stands for "key absent or deleted" and belongs to the domain of `apply`.
14/// Implementations must be deterministic and must satisfy associativity, commutativity of
15/// `combine_operands`, and the base action law above. They must not read time, randomness,
16/// external I/O, or mutable process state, and must not produce observable side effects:
17/// compaction and split may retry or redo fold attempts.
18///
19/// Reads combine operands from newest to oldest; compaction combines them from oldest to newest.
20/// Commutativity and associativity make the two orders equivalent. The methods are total and
21/// must return non-empty results; an empty or oversized result is an engine-detected violation.
22/// `None` from `apply` is the only logical deletion. The engine does not invoke the operator during
23/// merge admission or catch panics.
24///
25/// The engine stores only this runtime handle and never persists operator identity. Callers own
26/// semantic compatibility across process restarts.
27pub trait MergeOperator: Send + Sync {
28 /// Combines two operands into one equivalent operand. Must return a non-empty
29 /// equivalent operand for every pair the operator accepted.
30 fn combine_operands(&self, key: &[u8], left: &[u8], right: &[u8]) -> Vec<u8>;
31
32 /// Applies one operand to a base value. `None` deletes or leaves the key absent;
33 /// `Some(value)` must be non-empty.
34 fn apply(&self, key: &[u8], base: Option<&[u8]>, operand: &[u8]) -> Option<Vec<u8>>;
35}
36
37/// Default 64-bit unsigned integer additive merge operator
38///
39/// Operands and base values are little-endian `u64` deltas: `apply` adds the operand onto the base
40/// (zero when the key is absent or deleted), and `combine_operands` adds the two operands. A payload
41/// that is not exactly 8 bytes decodes as zero. ("corruption will be treated as 0"); the operator
42/// stays total for every input and never returns an empty value.
43#[derive(Default, Clone, Copy, Debug)]
44pub struct U64AddOperator;
45
46impl U64AddOperator {
47 fn decode(raw: &[u8]) -> u64 {
48 if raw.len() == size_of::<u64>() {
49 u64::from_le_bytes(raw.try_into().expect("exact u64 slice"))
50 } else {
51 // reference behavior: a malformed payload contributes zero
52 log::error!(
53 "uint64 value corruption, size: {} > {}",
54 raw.len(),
55 size_of::<u64>()
56 );
57 0
58 }
59 }
60
61 fn add(left: u64, right: u64) -> Vec<u8> {
62 left.wrapping_add(right).to_le_bytes().to_vec()
63 }
64}
65
66impl MergeOperator for U64AddOperator {
67 fn combine_operands(&self, _key: &[u8], left: &[u8], right: &[u8]) -> Vec<u8> {
68 Self::add(Self::decode(left), Self::decode(right))
69 }
70
71 fn apply(&self, _key: &[u8], base: Option<&[u8]>, operand: &[u8]) -> Option<Vec<u8>> {
72 let base = base.map(Self::decode).unwrap_or(0);
73 Some(Self::add(base, Self::decode(operand)))
74 }
75}
76
77/// Convenience handle for bucket options that want the default counter algebra.
78pub fn u64_add_operator() -> Option<Arc<dyn MergeOperator>> {
79 Some(Arc::new(U64AddOperator))
80}
81
82#[cfg(test)]
83mod tests {
84 use super::{MergeOperator, U64AddOperator};
85
86 fn decode(raw: &[u8]) -> u64 {
87 u64::from_le_bytes(raw.try_into().unwrap())
88 }
89
90 #[test]
91 fn u64_add_operator_folds_onto_base_and_absence() {
92 let op = U64AddOperator;
93 // absent base: operand alone
94 let v = op.apply(b"k", None, &5u64.to_le_bytes()).unwrap();
95 assert_eq!(decode(&v), 5);
96 // existing base: base + operand
97 let v = op
98 .apply(b"k", Some(&10u64.to_le_bytes()), &5u64.to_le_bytes())
99 .unwrap();
100 assert_eq!(decode(&v), 15);
101 // combine: left + right
102 let v = op.combine_operands(b"k", &5u64.to_le_bytes(), &7u64.to_le_bytes());
103 assert_eq!(decode(&v), 12);
104 // wrapping arithmetic, mirroring the reference operator
105 let v = op
106 .apply(b"k", Some(&u64::MAX.to_le_bytes()), &2u64.to_le_bytes())
107 .unwrap();
108 assert_eq!(decode(&v), 1);
109 }
110
111 #[test]
112 fn u64_add_operator_treats_malformed_payload_as_zero() {
113 let op = U64AddOperator;
114 // non-8-byte operand contributes zero (reference behavior)
115 let v = op
116 .apply(b"k", Some(&10u64.to_le_bytes()), b"short")
117 .unwrap();
118 assert_eq!(decode(&v), 10);
119 // malformed base decodes as zero
120 let v = op.apply(b"k", Some(b"bad"), &5u64.to_le_bytes()).unwrap();
121 assert_eq!(decode(&v), 5);
122 // outputs are never empty
123 let v = op.apply(b"k", None, &0u64.to_le_bytes()).unwrap();
124 assert_eq!(decode(&v), 0);
125 }
126}