use std::sync::Arc;
use nodedb_array::sync::hlc::{Hlc, HlcGenerator};
use nodedb_array::sync::replica_id::ReplicaId;
use nodedb_types::Namespace;
use crate::error::LiteError;
use crate::storage::engine::StorageEngine;
const REPLICA_ID_KEY: &[u8] = b"array.replica_id";
pub struct ReplicaState {
replica_id: ReplicaId,
hlc_gen: Arc<HlcGenerator>,
}
impl ReplicaState {
pub async fn load_or_init<S: StorageEngine>(storage: &S) -> Result<Self, LiteError> {
let existing = storage.get(Namespace::Meta, REPLICA_ID_KEY).await?;
let replica_id = if let Some(bytes) = existing {
if bytes.len() != 8 {
return Err(LiteError::Storage {
detail: format!("array.replica_id: expected 8 bytes, got {}", bytes.len()),
});
}
let arr: [u8; 8] = bytes.try_into().map_err(|_| LiteError::Storage {
detail: "array.replica_id: byte conversion failed".into(),
})?;
ReplicaId::new(u64::from_be_bytes(arr))
} else {
let id = ReplicaId::generate();
storage
.put(Namespace::Meta, REPLICA_ID_KEY, &id.as_u64().to_be_bytes())
.await?;
id
};
let hlc_gen = Arc::new(HlcGenerator::new(replica_id));
Ok(Self {
replica_id,
hlc_gen,
})
}
pub fn replica_id(&self) -> ReplicaId {
self.replica_id
}
pub fn hlc_gen(&self) -> Arc<HlcGenerator> {
Arc::clone(&self.hlc_gen)
}
pub fn next_hlc(&self) -> Result<Hlc, LiteError> {
self.hlc_gen.next().map_err(|e| LiteError::Storage {
detail: format!("array sync HLC: {e}"),
})
}
pub fn observe(&self, remote: Hlc) -> Result<(), LiteError> {
self.hlc_gen
.observe(remote)
.map_err(|e| LiteError::Storage {
detail: format!("array sync HLC observe: {e}"),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::pagedb_storage::PagedbStorageMem;
async fn open_storage() -> Arc<PagedbStorageMem> {
Arc::new(PagedbStorageMem::open_in_memory().await.unwrap())
}
use crate::storage::engine::StorageEngine;
#[tokio::test]
async fn load_or_init_generates_new_on_empty() {
let storage = open_storage().await;
let state = ReplicaState::load_or_init(&*storage).await.unwrap();
let bytes = storage
.get(Namespace::Meta, REPLICA_ID_KEY)
.await
.unwrap()
.expect("replica_id must be persisted");
assert_eq!(bytes.len(), 8);
let stored = u64::from_be_bytes(bytes.try_into().unwrap());
assert_eq!(stored, state.replica_id().as_u64());
}
#[tokio::test]
async fn load_or_init_returns_same_on_reload() {
let storage = open_storage().await;
let id1 = ReplicaState::load_or_init(&*storage)
.await
.unwrap()
.replica_id();
let id2 = ReplicaState::load_or_init(&*storage)
.await
.unwrap()
.replica_id();
assert_eq!(id1, id2, "reload must return the same replica_id");
}
#[tokio::test]
async fn next_hlc_monotonic() {
let storage = open_storage().await;
let state = ReplicaState::load_or_init(&*storage).await.unwrap();
let mut prev = state.next_hlc().unwrap();
for _ in 0..99 {
let curr = state.next_hlc().unwrap();
assert!(
curr > prev,
"HLC must be strictly increasing: {curr:?} <= {prev:?}"
);
prev = curr;
}
}
#[tokio::test]
async fn observe_advances_clock() {
let storage = open_storage().await;
let state = ReplicaState::load_or_init(&*storage).await.unwrap();
let baseline = state.next_hlc().unwrap();
let future_replica = ReplicaId::new(0xdead);
let future_hlc = Hlc::new(baseline.physical_ms + 100_000, 0, future_replica).unwrap();
state.observe(future_hlc).unwrap();
let next = state.next_hlc().unwrap();
assert!(
next > future_hlc,
"local HLC after observe must exceed observed remote: {next:?} <= {future_hlc:?}"
);
}
}