nodedb_lite/sync/array/
replica_state.rs1use std::sync::Arc;
9
10use nodedb_array::sync::hlc::{Hlc, HlcGenerator};
11use nodedb_array::sync::replica_id::ReplicaId;
12use nodedb_types::Namespace;
13
14use crate::error::LiteError;
15use crate::storage::engine::StorageEngine;
16
17const REPLICA_ID_KEY: &[u8] = b"array.replica_id";
19
20pub struct ReplicaState {
25 replica_id: ReplicaId,
26 hlc_gen: Arc<HlcGenerator>,
27}
28
29impl ReplicaState {
30 pub async fn load_or_init<S: StorageEngine>(storage: &S) -> Result<Self, LiteError> {
36 let existing = storage.get(Namespace::Meta, REPLICA_ID_KEY).await?;
37
38 let replica_id = if let Some(bytes) = existing {
39 if bytes.len() != 8 {
40 return Err(LiteError::Storage {
41 detail: format!("array.replica_id: expected 8 bytes, got {}", bytes.len()),
42 });
43 }
44 let arr: [u8; 8] = bytes.try_into().map_err(|_| LiteError::Storage {
45 detail: "array.replica_id: byte conversion failed".into(),
46 })?;
47 ReplicaId::new(u64::from_be_bytes(arr))
48 } else {
49 let id = ReplicaId::generate();
50 storage
51 .put(Namespace::Meta, REPLICA_ID_KEY, &id.as_u64().to_be_bytes())
52 .await?;
53 id
54 };
55
56 let hlc_gen = Arc::new(HlcGenerator::new(replica_id));
57 Ok(Self {
58 replica_id,
59 hlc_gen,
60 })
61 }
62
63 pub fn replica_id(&self) -> ReplicaId {
65 self.replica_id
66 }
67
68 pub fn hlc_gen(&self) -> Arc<HlcGenerator> {
70 Arc::clone(&self.hlc_gen)
71 }
72
73 pub fn next_hlc(&self) -> Result<Hlc, LiteError> {
78 self.hlc_gen.next().map_err(|e| LiteError::Storage {
79 detail: format!("array sync HLC: {e}"),
80 })
81 }
82
83 pub fn observe(&self, remote: Hlc) -> Result<(), LiteError> {
88 self.hlc_gen
89 .observe(remote)
90 .map_err(|e| LiteError::Storage {
91 detail: format!("array sync HLC observe: {e}"),
92 })
93 }
94}
95
96#[cfg(test)]
99mod tests {
100 use super::*;
101 use crate::storage::pagedb_storage::PagedbStorageMem;
102
103 async fn open_storage() -> Arc<PagedbStorageMem> {
104 Arc::new(PagedbStorageMem::open_in_memory().await.unwrap())
105 }
106
107 use crate::storage::engine::StorageEngine;
108
109 #[tokio::test]
110 async fn load_or_init_generates_new_on_empty() {
111 let storage = open_storage().await;
112 let state = ReplicaState::load_or_init(&*storage).await.unwrap();
113 let bytes = storage
116 .get(Namespace::Meta, REPLICA_ID_KEY)
117 .await
118 .unwrap()
119 .expect("replica_id must be persisted");
120 assert_eq!(bytes.len(), 8);
121 let stored = u64::from_be_bytes(bytes.try_into().unwrap());
122 assert_eq!(stored, state.replica_id().as_u64());
123 }
124
125 #[tokio::test]
126 async fn load_or_init_returns_same_on_reload() {
127 let storage = open_storage().await;
128 let id1 = ReplicaState::load_or_init(&*storage)
129 .await
130 .unwrap()
131 .replica_id();
132 let id2 = ReplicaState::load_or_init(&*storage)
133 .await
134 .unwrap()
135 .replica_id();
136 assert_eq!(id1, id2, "reload must return the same replica_id");
137 }
138
139 #[tokio::test]
140 async fn next_hlc_monotonic() {
141 let storage = open_storage().await;
142 let state = ReplicaState::load_or_init(&*storage).await.unwrap();
143 let mut prev = state.next_hlc().unwrap();
144 for _ in 0..99 {
145 let curr = state.next_hlc().unwrap();
146 assert!(
147 curr > prev,
148 "HLC must be strictly increasing: {curr:?} <= {prev:?}"
149 );
150 prev = curr;
151 }
152 }
153
154 #[tokio::test]
155 async fn observe_advances_clock() {
156 let storage = open_storage().await;
157 let state = ReplicaState::load_or_init(&*storage).await.unwrap();
158
159 let baseline = state.next_hlc().unwrap();
160 let future_replica = ReplicaId::new(0xdead);
162 let future_hlc = Hlc::new(baseline.physical_ms + 100_000, 0, future_replica).unwrap();
163
164 state.observe(future_hlc).unwrap();
165
166 let next = state.next_hlc().unwrap();
167 assert!(
168 next > future_hlc,
169 "local HLC after observe must exceed observed remote: {next:?} <= {future_hlc:?}"
170 );
171 }
172}