oxedyne/fe2o3/fe2o3_o3db_sync/src/dist/config.rs
5.2 KiB, 63 runs
created by r1870400018:11380, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! Configuration types for distributed Ozone. |
| 2 | //! |
| 3 | //! A [`DistOzoneConfig`] block is the caller's one-stop description of |
| 4 | //! distributed mode at start-up. It bundles the local peer's identity, the |
| 5 | //! initial bootstrap peer list, the OAM placement parameters and the |
| 6 | //! per-table consistency / anti-entropy cadence declarations. |
| 7 | //! |
| 8 | //! The config is *static* once a [`DistOzone`](crate::dist::DistOzone) engine |
| 9 | //! is constructed; runtime mutation (peers joining, leaving, network size |
| 10 | //! re-estimated) flows through the engine's own methods rather than through |
| 11 | //! the config. |
| 12 | //! |
| 13 | //! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\ |
| 14 | //! Anthropic Claude |
| 15 | |
| 16 | use oxedyne_fe2o3_core::prelude::*; |
| 17 | use crate::kademlia::id::NodeId; |
| 18 | use crate::oam::config::OamConfig; |
| 19 | |
| 20 | use std::time::Duration; |
| 21 | |
| 22 | |
| 23 | /// The consistency guarantee a table provides under distributed mode. |
| 24 | /// |
| 25 | /// *Eventual* tables accept concurrent writes and converge through IBLT |
| 26 | /// anti-entropy. *Cohort-backed* tables serialise writes through a HotStuff |
| 27 | /// consensus cohort and reach strict consistency after three message rounds. |
| 28 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] |
| 29 | pub enum Consistency { |
| 30 | Eventual, // writes land locally and at every OAM holder |
| 31 | // Tolerates up to floor((lambda - 1) / 3) Byzantine members. |
| 32 | Cohort { |
| 33 | lambda: u64, // 5, 7 or 9 |
| 34 | }, |
| 35 | } |
| 36 | |
| 37 | |
| 38 | /// Per-table configuration: the name, the consistency model, the |
| 39 | /// anti-entropy cadence, and the IBLT sketch dimensions used for |
| 40 | /// anti-entropy reconciliation. |
| 41 | #[derive(Clone, Debug, Eq, PartialEq)] |
| 42 | pub struct TableConfig { |
| 43 | pub name: String, // unique within a DistOzoneConfig |
| 44 | pub consistency: Consistency, |
| 45 | pub anti_entropy: Duration, // ignored for Cohort tables |
| 46 | pub iblt_cells: usize, // 1.5 x d cells decode a difference of d |
| 47 | } |
| 48 | |
| 49 | impl TableConfig { |
| 50 | // Identity directory, oxedation log, name claims. |
| 51 | pub const DEFAULT_AE: Duration = Duration::from_secs(30); |
| 52 | |
| 53 | // Small high-value tables -- the peer set, the revocation list. |
| 54 | pub const HIGH_VALUE_AE: Duration = Duration::from_secs(3); |
| 55 | |
| 56 | // Tuned for a steady-state symmetric difference of up to ~160 records |
| 57 | // (256 / 1.5). Larger differences overload the sketch; the anti-entropy |
| 58 | // handler falls back to a bulk transfer when decoding fails. |
| 59 | pub const DEFAULT_IBLT_CELLS: usize = 256; |
| 60 | |
| 61 | /// The number of hash functions the anti-entropy IBLT uses. Fixed at |
| 62 | /// three across the crate, matching the sizing rule of thumb in |
| 63 | /// `fe2o3_data::iblt`. |
| 64 | pub const IBLT_NUM_HASHES: usize = 3; |
| 65 | |
| 66 | pub fn new<S: Into<String>>( |
| 67 | name: S, |
| 68 | consistency: Consistency, |
| 69 | anti_entropy: Duration, |
| 70 | iblt_cells: usize, |
| 71 | ) |
| 72 | -> Outcome<Self> |
| 73 | { |
| 74 | let name = name.into(); |
| 75 | if name.is_empty() { |
| 76 | return Err(err!( |
| 77 | "TableConfig requires a non-empty name."; |
| 78 | Invalid, Input, Missing)); |
| 79 | } |
| 80 | if let Consistency::Cohort { lambda } = consistency { |
| 81 | if !matches!(lambda, 5 | 7 | 9) { |
| 82 | return Err(err!( |
| 83 | "Cohort lambda must be 5, 7 or 9; got {}.", lambda; |
| 84 | Invalid, Input, Size)); |
| 85 | } |
| 86 | } |
| 87 | if iblt_cells == 0 { |
| 88 | return Err(err!( |
| 89 | "TableConfig requires iblt_cells > 0."; |
| 90 | Invalid, Input, Size)); |
| 91 | } |
| 92 | Ok(Self { name, consistency, anti_entropy, iblt_cells }) |
| 93 | } |
| 94 | |
| 95 | pub fn eventual<S: Into<String>>(name: S) -> Outcome<Self> { |
| 96 | Self::new( |
| 97 | name, |
| 98 | Consistency::Eventual, |
| 99 | Self::DEFAULT_AE, |
| 100 | Self::DEFAULT_IBLT_CELLS, |
| 101 | ) |
| 102 | } |
| 103 | |
| 104 | /// Cohort size 5. |
| 105 | pub fn cohort_default<S: Into<String>>(name: S) -> Outcome<Self> { |
| 106 | Self::new( |
| 107 | name, |
| 108 | Consistency::Cohort { lambda: 5 }, |
| 109 | Self::DEFAULT_AE, |
| 110 | Self::DEFAULT_IBLT_CELLS, |
| 111 | ) |
| 112 | } |
| 113 | |
| 114 | /// The IBLT's splitmix64 salt, derived from the table name so that |
| 115 | /// different tables have different hash functions. |
| 116 | pub fn iblt_seed(&self) -> u64 { |
| 117 | let mut state: u64 = 0x9E3779B97F4A7C15; |
| 118 | for byte in self.name.as_bytes() { |
| 119 | state = state.wrapping_add(*byte as u64); |
| 120 | state = (state ^ (state >> 30)).wrapping_mul(0xBF58476D1CE4E5B9); |
| 121 | state = (state ^ (state >> 27)).wrapping_mul(0x94D049BB133111EB); |
| 122 | state ^= state >> 31; |
| 123 | } |
| 124 | state |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | |
| 129 | /// Top-level configuration for distributed Ozone mode. |
| 130 | #[derive(Clone, Debug)] |
| 131 | pub struct DistOzoneConfig { |
| 132 | pub local_peer_id: NodeId, |
| 133 | pub bootstrap_peers: Vec<NodeId>, // the local peer is filtered out |
| 134 | pub oam: OamConfig, // network_size is the initial value only |
| 135 | pub tables: Vec<TableConfig>, // unique names |
| 136 | } |
| 137 | |
| 138 | impl DistOzoneConfig { |
| 139 | pub fn new( |
| 140 | local_peer_id: NodeId, |
| 141 | bootstrap_peers: Vec<NodeId>, |
| 142 | oam: OamConfig, |
| 143 | tables: Vec<TableConfig>, |
| 144 | ) |
| 145 | -> Outcome<Self> |
| 146 | { |
| 147 | if tables.is_empty() { |
| 148 | return Err(err!( |
| 149 | "DistOzoneConfig requires at least one table."; |
| 150 | Invalid, Input, Missing)); |
| 151 | } |
| 152 | // Detect duplicate table names via a pairwise scan. Table counts are |
| 153 | // small (single-digits to low tens) so quadratic is fine and avoids |
| 154 | // pulling HashSet into a config type. |
| 155 | for i in 0..tables.len() { |
| 156 | for j in (i + 1)..tables.len() { |
| 157 | if tables[i].name == tables[j].name { |
| 158 | return Err(err!( |
| 159 | "Duplicate table name in DistOzoneConfig: {}.", |
| 160 | tables[i].name; |
| 161 | Invalid, Input, Duplicate)); |
| 162 | } |
| 163 | } |
| 164 | } |
| 165 | Ok(Self { local_peer_id, bootstrap_peers, oam, tables }) |
| 166 | } |
| 167 | |
| 168 | pub fn table(&self, name: &str) -> Option<&TableConfig> { |
| 169 | self.tables.iter().find(|t| t.name == name) |
| 170 | } |
| 171 | } |