Skip to main content

wombatkv_core/
lib.rs

1#![forbid(unsafe_code)]
2
3use std::cmp::Ordering;
4
5pub mod metadata;
6pub mod reuse;
7
8/// Returns crate identity for smoke tests.
9#[must_use]
10pub fn crate_id() -> &'static str {
11    "wombatkv-core"
12}
13
14/// Write id used as deterministic tie-breaker within a timestamp.
15pub type WriteId = [u8; 16];
16
17/// Minimal fact identity used by conflict-resolution contracts.
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct FactRef {
20    /// Millisecond unix epoch associated with the publish.
21    pub epoch_ms: u64,
22    /// `UUIDv7` bytes (or equivalent monotonic id) used to break epoch ties.
23    pub write_id: WriteId,
24}
25
26impl FactRef {
27    /// Stable total order key for manifest winner selection.
28    #[must_use]
29    pub const fn ordering_key(&self) -> (u64, WriteId) {
30        (self.epoch_ms, self.write_id)
31    }
32}
33
34/// Deterministic winner selection for same `(tenant, F, Hi)` facts.
35#[must_use]
36pub fn pick_winner(facts: &[FactRef]) -> Option<&FactRef> {
37    facts.iter().min_by(|lhs, rhs| compare_facts(lhs, rhs))
38}
39
40/// Compare two facts using canonical order (lowest epoch, then lowest `write_id`).
41#[must_use]
42pub fn compare_facts(lhs: &FactRef, rhs: &FactRef) -> Ordering {
43    lhs.ordering_key().cmp(&rhs.ordering_key())
44}
45
46/// Canonical WAL object key shape for globally unique chunk names.
47#[must_use]
48pub fn wal_chunk_key(
49    tenant: &str,
50    fingerprint: &str,
51    epoch_ms: u64,
52    node_id: &str,
53    seq: u64,
54) -> String {
55    format!("wal/{tenant}/{fingerprint}/{epoch_ms}/chunk-{node_id}-{seq}.bin")
56}
57
58/// Counts the deepest contiguous resident prefix.
59#[must_use]
60pub fn contiguous_resident_prefix(residency: &[bool]) -> usize {
61    residency.iter().take_while(|is_resident| **is_resident).count()
62}
63
64/// Hash function options for chained prefix hashing.
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum HashFunction {
67    Blake3,
68    XxHash3,
69}
70
71/// Wire format options for adapter transport payloads.
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73pub enum WireFormat {
74    MsgPack,
75    FlatBuffers,
76    Capnp,
77}
78
79/// Frozen embedded-mode defaults used until superseded by a versioned config RFC.
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81pub struct M0Defaults {
82    pub block_size_tokens: u16,
83    pub hash_function: HashFunction,
84    pub wal_chunk_target_mb: u16,
85    pub wire_format: WireFormat,
86}
87
88impl Default for M0Defaults {
89    fn default() -> Self {
90        Self {
91            block_size_tokens: 128,
92            hash_function: HashFunction::Blake3,
93            wal_chunk_target_mb: 32,
94            wire_format: WireFormat::MsgPack,
95        }
96    }
97}
98
99impl M0Defaults {
100    /// Validate embedded-mode defaults and overrides against frozen bounds.
101    pub fn validate(self) -> Result<(), &'static str> {
102        const VALID_BLOCK_SIZES: [u16; 3] = [64, 128, 256];
103        const VALID_WAL_CHUNK_MB: [u16; 3] = [8, 32, 64];
104
105        if !VALID_BLOCK_SIZES.contains(&self.block_size_tokens) {
106            return Err("block_size_tokens must be one of 64, 128, 256");
107        }
108
109        if !VALID_WAL_CHUNK_MB.contains(&self.wal_chunk_target_mb) {
110            return Err("wal_chunk_target_mb must be one of 8, 32, 64");
111        }
112
113        Ok(())
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::{compare_facts, contiguous_resident_prefix, pick_winner, wal_chunk_key, FactRef};
120
121    #[test]
122    fn crate_id_is_stable() {
123        assert_eq!(super::crate_id(), "wombatkv-core");
124    }
125
126    #[test]
127    fn epoch_and_write_id_define_total_order() {
128        let earlier = FactRef { epoch_ms: 1_700_000_000_001, write_id: [2; 16] };
129        let later = FactRef { epoch_ms: 1_700_000_000_002, write_id: [1; 16] };
130        let same_epoch_lower_write = FactRef { epoch_ms: 1_700_000_000_001, write_id: [1; 16] };
131
132        assert!(compare_facts(&earlier, &later).is_lt());
133        assert!(compare_facts(&same_epoch_lower_write, &earlier).is_lt());
134    }
135
136    #[test]
137    fn winner_is_stable_independent_of_arrival_order() {
138        let winner = FactRef { epoch_ms: 1_700_000_000_100, write_id: [1; 16] };
139        let loser = FactRef { epoch_ms: 1_700_000_000_100, write_id: [9; 16] };
140
141        let first_order = vec![loser.clone(), winner.clone()];
142        let second_order = vec![winner.clone(), loser];
143
144        assert_eq!(pick_winner(&first_order), Some(&winner));
145        assert_eq!(pick_winner(&second_order), Some(&winner));
146    }
147
148    #[test]
149    fn wal_keys_are_unique_across_nodes_for_same_epoch_and_seq() {
150        let key_a = wal_chunk_key("t1", "f1", 1000, "node-a", 7);
151        let key_b = wal_chunk_key("t1", "f1", 1000, "node-b", 7);
152        assert_ne!(key_a, key_b);
153    }
154
155    #[test]
156    fn contiguous_prefix_stops_at_first_missing_block() {
157        let residency = [true, true, false, true];
158        assert_eq!(contiguous_resident_prefix(&residency), 2);
159    }
160
161    #[test]
162    fn m0_defaults_are_stable_and_valid() {
163        let defaults = super::M0Defaults::default();
164        assert_eq!(defaults.block_size_tokens, 128);
165        assert_eq!(defaults.hash_function, super::HashFunction::Blake3);
166        assert_eq!(defaults.wal_chunk_target_mb, 32);
167        assert_eq!(defaults.wire_format, super::WireFormat::MsgPack);
168        assert_eq!(defaults.validate(), Ok(()));
169    }
170
171    #[test]
172    fn invalid_block_size_is_rejected() {
173        let invalid = super::M0Defaults { block_size_tokens: 96, ..super::M0Defaults::default() };
174        assert_eq!(invalid.validate(), Err("block_size_tokens must be one of 64, 128, 256"));
175    }
176
177    #[test]
178    fn invalid_wal_chunk_size_is_rejected() {
179        let invalid = super::M0Defaults { wal_chunk_target_mb: 16, ..super::M0Defaults::default() };
180        assert_eq!(invalid.validate(), Err("wal_chunk_target_mb must be one of 8, 32, 64"));
181    }
182}