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