use std::hash::Hash;
use std::io;
use std::ops::Bound;
use std::sync::Arc;
use std::time::Duration;
use tracing::warn;
use crate::bounds::{Key, Value};
use crate::clock::Timestamp;
use crate::entry::Entry;
use crate::persistence::{DatedEntries, PersistedState, Persistence};
use super::ReplicatedMap;
pub(super) const SNAPSHOT_INTERVAL: Duration = Duration::from_secs(5);
pub(super) const LOAD_RETRY_ATTEMPTS: u32 = 5;
pub(super) const LOAD_RETRY_BASE_DELAY: Duration = Duration::from_millis(100);
pub(super) fn backoff_delay(attempt: u32) -> Duration {
LOAD_RETRY_BASE_DELAY * 2u32.pow(attempt - 1)
}
pub(super) const SNAPSHOT_CHUNK_SIZE: usize = 4096;
impl<K: Key + Hash, V: Value> ReplicatedMap<K, V> {
pub fn with_persistence(mut self, backend: Arc<dyn Persistence<K, V>>) -> Self {
if self.engine.node_id_is_random() {
warn!(
"persistence is enabled but no stable node_id was configured \
(Config::with_node_id was not called). The node id is randomly generated on \
every start, so this node's LWW conflict-resolution identity changes across \
restarts. Conflicts between a pre-restart write and a post-restart write from \
the same node are resolved non-deterministically. Set a stable, unique \
Config::with_node_id to preserve consistent LWW ordering across restarts."
);
}
let loaded = {
let mut attempt = 0u32;
loop {
match backend.load() {
Ok(state) => break state,
Err(err) if err.kind() == io::ErrorKind::InvalidData => {
panic!("persisted state is corrupt or from an incompatible format, refusing to silently start fresh: {err}");
}
Err(err) if attempt + 1 < LOAD_RETRY_ATTEMPTS => {
attempt += 1;
let delay = backoff_delay(attempt);
warn!(
"transient failure loading persisted state (attempt {attempt}/{LOAD_RETRY_ATTEMPTS}): \
{err}; retrying in {delay:?}"
);
std::thread::sleep(delay);
}
Err(err) => {
panic!(
"failed to load persisted state after {LOAD_RETRY_ATTEMPTS} attempts: {err}"
);
}
}
}
};
if let Some(state) = loaded {
*self.engine.members.write() = state.members;
*self.engine.tombstone_acks.write() = state.tombstone_acks;
for (_, entry) in &state.entries {
self.engine.clock_observe_trusted(entry.stamp);
}
self.engine.just_insert_bulk(&state.entries);
}
self.persistence = backend;
self
}
pub(super) fn snapshot(&self) {
let mut entries: DatedEntries<K, V> = Vec::new();
let mut cursor: Option<K> = None;
loop {
let guard = self.engine.map.read();
let chunk: Vec<(K, Entry<Timestamp, V>)> = match &cursor {
None => guard
.range(..)
.take(SNAPSHOT_CHUNK_SIZE)
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
Some(last) => guard
.range((Bound::Excluded(last.clone()), Bound::Unbounded))
.take(SNAPSHOT_CHUNK_SIZE)
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
};
drop(guard);
let Some((last_key, _)) = chunk.last() else {
break;
};
cursor = Some(last_key.clone());
entries.extend(chunk);
}
let state = PersistedState::new(
entries,
self.engine.members.read().clone(),
self.engine.tombstone_acks.read().clone(),
);
if let Err(err) = self.persistence.save(&state) {
warn!("failed to persist reconcile store snapshot: {err}");
}
}
pub(super) async fn snapshot_periodically(&self) {
loop {
tokio::time::sleep(SNAPSHOT_INTERVAL).await;
self.snapshot();
}
}
}