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;