Skip to main content

kevy_persist/
lib.rs

1//! kevy-persist — durability for a [`kevy_store::Store`].
2//!
3//! Two mechanisms, both zero-dependency pure Rust over `std::fs`:
4//!
5//! - **Snapshot (RDB-style):** [`save_snapshot`] dumps a whole store to a temp
6//!   file then atomically renames it (fsync before rename); [`load_snapshot`]
7//!   restores it. A compact, type-tagged binary format.
8//! - **AOF:** an [`Aof`] append-only command log with a configurable fsync
9//!   policy; [`replay_aof`] re-applies it on startup, tolerating a truncated
10//!   trailing frame from a crash mid-write.
11//!
12//! In a shared-nothing runtime each shard persists its own store to its own
13//! file, so there is no cross-core coordination. Part of the [kevy] server.
14//!
15//! [kevy]: https://crates.io/crates/kevy
16//!
17//! # Example (AOF)
18//!
19//! ```
20//! use kevy_persist::{Aof, Argv, Fsync, replay_aof};
21//!
22//! # fn main() -> std::io::Result<()> {
23//! let path = std::env::temp_dir().join("kevy-persist-doctest.aof");
24//! # let _ = std::fs::remove_file(&path);
25//! {
26//!     let mut aof = Aof::open(&path, Fsync::No)?;
27//!     aof.append(&Argv::from(vec![b"SET".to_vec(), b"k".to_vec(), b"v".to_vec()]))?;
28//! } // flushed on drop
29//!
30//! let mut replayed: Vec<Argv> = Vec::new();
31//! replay_aof(&path, |args| replayed.push(args))?;
32//! assert_eq!(replayed, vec![vec![b"SET".to_vec(), b"k".to_vec(), b"v".to_vec()]]);
33//! # std::fs::remove_file(&path).ok();
34//! # Ok(())
35//! # }
36//! ```
37#![forbid(unsafe_code)]
38#![warn(missing_docs)]
39
40mod aof;
41mod aof_policy;
42mod aof_queue;
43mod aof_rewrite;
44mod aof_txn;
45mod aof_util;
46mod baseline;
47mod crc32c;
48mod dump_cache;
49pub mod feed_meta;
50pub mod layout;
51mod record;
52mod replay;
53mod replay_log;
54mod replay_resync;
55mod replay_txn;
56mod replay_walk;
57pub mod reshard;
58mod rewrite_chunk;
59mod rewrite_fmt;
60mod rewrite_frames;
61mod rewrite_stream_fmt;
62mod segmented;
63mod shards_meta;
64mod snapshot_fmt;
65mod snapshot_payload;
66mod snapshot_read;
67mod snapshot_write;
68
69pub use aof::{AOF_MAGIC, Aof, Fsync, RewritePlan, RewriteStats};
70pub use aof_policy::RewritePolicy;
71pub use aof_util::write_aof_base;
72pub use baseline::estimate_rewrite_size;
73pub use record::{AOF2_MAGIC, AofFormat, RecordStep, next_record, write_record_multibulk};
74pub use replay::{ReplayReport, replay_aof, replay_aof_quiet, replay_aof_resync};
75pub use segmented::{SEGMENTED, segmented_argv, segmented_frame};
76
77/// How often bulk-load paths check the tiering demote watermark:
78/// every this many applied frames/records, the loading store runs
79/// `demote_to_watermark`. Replay executes into the hot map, so a boot
80/// whose dataset exceeds the tier budget would OOM before tiering ever
81/// ran without the inline spill. One shared constant so AOF replay
82/// (whose drive loops live in the callers — kevy-rt / kevy-embedded)
83/// and the snapshot loader stride identically.
84pub const REPLAY_DEMOTE_INTERVAL: u64 = 1024;
85pub use kevy_resp::{Argv, ArgvView};
86use kevy_store::Store;
87use kevy_store::Value;
88pub(crate) use rewrite_fmt::estimate_multibulk_bytes;
89pub use rewrite_fmt::{dump_aof, dump_store_to_buf, write_multibulk};
90pub use rewrite_frames::value_as_v1_frames;
91pub use rewrite_stream_fmt::write_stream_as_commands;
92pub use shards_meta::{Routing, ShardsMeta, read_shards_meta, write_shards_meta};
93pub(crate) use snapshot_fmt::{SNAPSHOT_BUF_CAP, write_bytes};
94pub use snapshot_read::{
95    load_snapshot, load_snapshot_filtered, load_snapshot_from, read_snapshot_cursor,
96};
97pub(crate) use snapshot_write::write_stream_groups;
98pub use snapshot_write::{
99    save_snapshot, write_snapshot_tmp, write_snapshot_to, write_snapshot_to_with_cursor,
100};
101
102/// Anything that can enumerate `(key, &Value, ttl_ms)` triples for
103/// serialization: a live [`Store`] (its `snapshot_each`, the synchronous
104/// paths) or a frozen [`kevy_store::SnapshotView`] (the COW paths — collect
105/// on the owning thread, serialize on a background one).
106///
107/// **Tiering contract**: `for_each_entry` yields VLOG-backed
108/// `Value::Cold` stubs materialized (the store reads its own log; a
109/// view reads through the `Arc<VlogFile>` pins captured at collect
110/// time) one value at a time, so serializer memory stays bounded and
111/// nothing is ever promoted into the hot map. SEG-backed stubs pass
112/// through AS STUBS: their data is truth in the segment directory, and
113/// the consumers persist the reference, not the payload.
114pub trait SnapshotSource {
115    /// Visit every live entry as `(key, &value, remaining_ttl_ms)`.
116    fn for_each_entry(&self, f: impl FnMut(&[u8], &Value, Option<u64>));
117
118    /// Visit every live hash field TTL as `(key, field,
119    /// absolute_unix_ms)`. Default = none (sources without the
120    /// feature).
121    fn for_each_hash_ttl(&self, _f: impl FnMut(&[u8], &[u8], u64)) {}
122
123    /// The live row segments' `(seq, file)` identities — the AOF
124    /// rewrite's trailing SEGMENTED frames and the snapshot writer's
125    /// version choice read these. Default = none.
126    fn row_seg_files(&self) -> Vec<(u32, String)> {
127        Vec::new()
128    }
129}
130
131/// Whether `v` is a row-segment stub (persisted as a reference).
132pub(crate) fn is_seg_stub(v: &Value) -> bool {
133    matches!(v, Value::Cold(c) if c.seg_parts().is_some())
134}
135
136impl SnapshotSource for Store {
137    fn for_each_entry(&self, mut f: impl FnMut(&[u8], &Value, Option<u64>)) {
138        self.snapshot_each(|k, v, ttl| {
139            if is_seg_stub(v) {
140                return f(k, v, ttl);
141            }
142            match self.materialize_cold(k, v) {
143                // Vlog stub: decode the record into a transient hot
144                // value (dropped after the callback — memory bound =
145                // one value) and emit exactly what the hot value
146                // would have.
147                Some(hot) => f(k, &hot, ttl),
148                None => f(k, v, ttl),
149            }
150        });
151    }
152    fn for_each_hash_ttl(&self, f: impl FnMut(&[u8], &[u8], u64)) {
153        self.hash_ttl_each(f);
154    }
155    fn row_seg_files(&self) -> Vec<(u32, String)> {
156        self.row_seg_files()
157    }
158}
159
160impl SnapshotSource for kevy_store::SnapshotView {
161    fn for_each_entry(&self, mut f: impl FnMut(&[u8], &Value, Option<u64>)) {
162        self.each(|k, v, ttl| {
163            if is_seg_stub(v) {
164                return f(k, v, ttl);
165            }
166            match self.materialize_cold(k, v) {
167                // Vlog stub: resolve against the view's pinned files —
168                // the serializer thread never touches the store.
169                Some(hot) => f(k, &hot, ttl),
170                None => f(k, v, ttl),
171            }
172        });
173    }
174    fn row_seg_files(&self) -> Vec<(u32, String)> {
175        kevy_store::SnapshotView::row_seg_files(self)
176    }
177    fn for_each_hash_ttl(&self, f: impl FnMut(&[u8], &[u8], u64)) {
178        self.each_hash_ttl(f);
179    }
180}
181
182#[cfg(test)]
183mod tests;
184#[cfg(test)]
185mod tests_aof;
186#[cfg(test)]
187mod tests_rewrite;
188#[cfg(test)]
189mod tests_tier_stream;