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;
56pub mod reshard;
57mod rewrite_chunk;
58mod rewrite_fmt;
59mod rewrite_stream_fmt;
60mod rewrite_frames;
61mod segmented;
62mod shards_meta;
63mod snapshot_fmt;
64mod snapshot_payload;
65mod snapshot_read;
66mod snapshot_write;
67
68pub use aof::{AOF_MAGIC, Aof, Fsync, RewritePlan, RewriteStats};
69pub use aof_policy::RewritePolicy;
70pub use aof_util::write_aof_base;
71pub use baseline::estimate_rewrite_size;
72pub use record::{AOF2_MAGIC, AofFormat, RecordStep, next_record, write_record_multibulk};
73pub use replay::{ReplayReport, replay_aof, replay_aof_quiet, replay_aof_resync};
74pub use segmented::{SEGMENTED, segmented_argv, segmented_frame};
75
76/// How often bulk-load paths check the tiering demote watermark:
77/// every this many applied frames/records, the loading store runs
78/// `demote_to_watermark`. Replay executes into the hot map, so a boot
79/// whose dataset exceeds the tier budget would OOM before tiering ever
80/// ran without the inline spill. One shared constant so AOF replay
81/// (whose drive loops live in the callers — kevy-rt / kevy-embedded)
82/// and the snapshot loader stride identically.
83pub const REPLAY_DEMOTE_INTERVAL: u64 = 1024;
84pub use kevy_resp::{Argv, ArgvView};
85use kevy_store::Store;
86use kevy_store::Value;
87pub(crate) use rewrite_fmt::estimate_multibulk_bytes;
88pub use rewrite_fmt::{dump_aof, dump_store_to_buf, write_multibulk};
89pub use rewrite_stream_fmt::write_stream_as_commands;
90pub use rewrite_frames::value_as_v1_frames;
91pub use shards_meta::{Routing, ShardsMeta, read_shards_meta, write_shards_meta};
92pub(crate) use snapshot_fmt::{SNAPSHOT_BUF_CAP, write_bytes};
93pub use snapshot_read::{
94    load_snapshot, load_snapshot_filtered, load_snapshot_from, read_snapshot_cursor,
95};
96pub(crate) use snapshot_write::write_stream_groups;
97pub use snapshot_write::{
98    save_snapshot, write_snapshot_tmp, write_snapshot_to, write_snapshot_to_with_cursor,
99};
100
101/// Anything that can enumerate `(key, &Value, ttl_ms)` triples for
102/// serialization: a live [`Store`] (its `snapshot_each`, the synchronous
103/// paths) or a frozen [`kevy_store::SnapshotView`] (the COW paths — collect
104/// on the owning thread, serialize on a background one).
105///
106/// **Tiering contract**: `for_each_entry` yields VLOG-backed
107/// `Value::Cold` stubs materialized (the store reads its own log; a
108/// view reads through the `Arc<VlogFile>` pins captured at collect
109/// time) one value at a time, so serializer memory stays bounded and
110/// nothing is ever promoted into the hot map. SEG-backed stubs pass
111/// through AS STUBS: their data is truth in the segment directory, and
112/// the consumers persist the reference, not the payload.
113pub trait SnapshotSource {
114    /// Visit every live entry as `(key, &value, remaining_ttl_ms)`.
115    fn for_each_entry(&self, f: impl FnMut(&[u8], &Value, Option<u64>));
116
117    /// Visit every live hash field TTL as `(key, field,
118    /// absolute_unix_ms)`. Default = none (sources without the
119    /// feature).
120    fn for_each_hash_ttl(&self, _f: impl FnMut(&[u8], &[u8], u64)) {}
121
122    /// The live row segments' `(seq, file)` identities — the AOF
123    /// rewrite's trailing SEGMENTED frames and the snapshot writer's
124    /// version choice read these. Default = none.
125    fn row_seg_files(&self) -> Vec<(u32, String)> {
126        Vec::new()
127    }
128}
129
130/// Whether `v` is a row-segment stub (persisted as a reference).
131pub(crate) fn is_seg_stub(v: &Value) -> bool {
132    matches!(v, Value::Cold(c) if c.seg_parts().is_some())
133}
134
135impl SnapshotSource for Store {
136    fn for_each_entry(&self, mut f: impl FnMut(&[u8], &Value, Option<u64>)) {
137        self.snapshot_each(|k, v, ttl| {
138            if is_seg_stub(v) {
139                return f(k, v, ttl);
140            }
141            match self.materialize_cold(k, v) {
142                // Vlog stub: decode the record into a transient hot
143                // value (dropped after the callback — memory bound =
144                // one value) and emit exactly what the hot value
145                // would have.
146                Some(hot) => f(k, &hot, ttl),
147                None => f(k, v, ttl),
148            }
149        });
150    }
151    fn for_each_hash_ttl(&self, f: impl FnMut(&[u8], &[u8], u64)) {
152        self.hash_ttl_each(f);
153    }
154    fn row_seg_files(&self) -> Vec<(u32, String)> {
155        self.row_seg_files()
156    }
157}
158
159impl SnapshotSource for kevy_store::SnapshotView {
160    fn for_each_entry(&self, mut f: impl FnMut(&[u8], &Value, Option<u64>)) {
161        self.each(|k, v, ttl| {
162            if is_seg_stub(v) {
163                return f(k, v, ttl);
164            }
165            match self.materialize_cold(k, v) {
166                // Vlog stub: resolve against the view's pinned files —
167                // the serializer thread never touches the store.
168                Some(hot) => f(k, &hot, ttl),
169                None => f(k, v, ttl),
170            }
171        });
172    }
173    fn row_seg_files(&self) -> Vec<(u32, String)> {
174        kevy_store::SnapshotView::row_seg_files(self)
175    }
176    fn for_each_hash_ttl(&self, f: impl FnMut(&[u8], &[u8], u64)) {
177        self.each_hash_ttl(f);
178    }
179}
180
181#[cfg(test)]
182mod tests;
183#[cfg(test)]
184mod tests_aof;
185#[cfg(test)]
186mod tests_rewrite;
187#[cfg(test)]
188mod tests_tier_stream;