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
39mod aof;
40pub mod feed_meta;
41pub mod layout;
42mod replay;
43pub mod reshard;
44mod rewrite_fmt;
45mod shards_meta;
46mod snapshot_payload;
47
48pub use aof::{Aof, Fsync, RewritePlan, RewriteStats, write_aof_base};
49pub use replay::replay_aof;
50pub use shards_meta::{Routing, ShardsMeta, read_shards_meta, write_shards_meta};
51pub use kevy_resp::{Argv, ArgvView};
52pub use rewrite_fmt::dump_aof;
53pub(crate) use rewrite_fmt::{dump_store_to_buf, estimate_multibulk_bytes, write_multibulk};
54use kevy_store::Store;
55use kevy_store::Value;
56// ZSet snapshot iterates ordered (member, score) pairs via `Value::ZSet`.
57use std::fs::File;
58use std::io::{self, BufReader, BufWriter, Read, Write};
59use std::path::Path;
60
61/// File magic + format version. Bump `VERSION` on any layout change.
62///
63/// v2 stored each entry's TTL as **remaining millis** (relative), so a load
64/// re-anchored the deadline to load-time — a restart reset every key to a
65/// fresh full TTL (INC-2026-06-09). v3 stores the **absolute** Unix-ms
66/// deadline, so a load reconstructs the original instant. v4 appends a
67/// consumer-group section to each `OP_STREAM` payload (groups + consumers
68/// plus PEL) — before that, SAVE/reshard silently dropped group state. The
69/// loader still accepts v2 (relative TTL) and v3 (no group section).
70const MAGIC: &[u8; 8] = b"KEVYSNAP";
71const VERSION: u8 = 4;
72/// v2.3: version 5 carries a 16-byte feed cursor (`gen u64 LE` +
73/// `offset u64 LE`) right after the version byte — the snapshot half
74/// of the recovery-point contract (docs/cdc.md): snapshot S + feed
75/// frames from S's cursor = exact restore. Writers emit v5 only when
76/// a cursor is supplied; cursor-less writes stay at v4 so every
77/// existing path is byte-identical.
78const VERSION_FEED_CURSOR: u8 = 5;
79/// v2.4: version 6 additionally carries `OP_HFTTL` hash field-TTL
80/// records after the entry stream. Written only when field TTLs
81/// exist; the header still carries the (possibly zero) feed cursor.
82const VERSION_HASH_TTL: u8 = 6;
83const VERSION_RELATIVE_TTL: u8 = 2;
84const VERSION_ABSOLUTE_TTL: u8 = 3;
85
86// Record opcodes (one per value type). Each record is:
87//   [op][ttl: u8 flag + optional u64][key][type payload]
88const OP_EOF: u8 = 0;
89const OP_STR: u8 = 1;
90const OP_HASH: u8 = 2;
91const OP_LIST: u8 = 3;
92const OP_SET: u8 = 4;
93const OP_ZSET: u8 = 5;
94const OP_STREAM: u8 = 6;
95/// v2.4 hash field TTL record: `[key][field][deadline_ms: u64 LE]`.
96/// Appears only in format v6+ snapshots, after the entry stream's
97/// records (before OP_EOF).
98const OP_HFTTL: u8 = 7;
99
100/// BufWriter capacity for bulk snapshot / AOF-rewrite writes. The 8 KiB
101/// default made SAVE ~12 % of disk bandwidth (tens of thousands of small
102/// `write(2)`s); 1 MiB amortizes the syscalls toward disk speed.
103pub(crate) const SNAPSHOT_BUF_CAP: usize = 1 << 20;
104
105/// Anything that can enumerate `(key, &Value, ttl_ms)` triples for
106/// serialization: a live [`Store`] (its `snapshot_each`, the synchronous
107/// paths) or a frozen [`kevy_store::SnapshotView`] (the COW paths — collect
108/// on the owning thread, serialize on a background one).
109pub trait SnapshotSource {
110    /// Visit every live entry as `(key, &value, remaining_ttl_ms)`.
111    fn for_each_entry(&self, f: impl FnMut(&[u8], &Value, Option<u64>));
112
113    /// v2.4: visit every live hash field TTL as `(key, field,
114    /// absolute_unix_ms)`. Default = none (sources predating the
115    /// feature).
116    fn for_each_hash_ttl(&self, _f: impl FnMut(&[u8], &[u8], u64)) {}
117}
118
119impl SnapshotSource for Store {
120    fn for_each_entry(&self, f: impl FnMut(&[u8], &Value, Option<u64>)) {
121        self.snapshot_each(f);
122    }
123    fn for_each_hash_ttl(&self, f: impl FnMut(&[u8], &[u8], u64)) {
124        self.hash_ttl_each(f);
125    }
126}
127
128impl SnapshotSource for kevy_store::SnapshotView {
129    fn for_each_entry(&self, f: impl FnMut(&[u8], &Value, Option<u64>)) {
130        self.each(f);
131    }
132    fn for_each_hash_ttl(&self, f: impl FnMut(&[u8], &[u8], u64)) {
133        self.each_hash_ttl(f);
134    }
135}
136
137/// Write a point-in-time snapshot of `src` (a live [`Store`] or a frozen
138/// [`kevy_store::SnapshotView`]) to `path`, atomically: data is written to
139/// `<path>.tmp`, fsynced, then renamed over `path`.
140pub fn save_snapshot<S: SnapshotSource>(src: &S, path: &Path) -> io::Result<()> {
141    let tmp = write_snapshot_tmp(src, path)?;
142    std::fs::rename(&tmp, path)
143}
144
145/// Serialize a point-in-time snapshot of `src` into any `Write` sink.
146/// Used by both [`write_snapshot_tmp`] (sink = `BufWriter<File>` +
147/// extra fsync after) and the v3-cluster replication path (sink =
148/// `&mut Vec<u8>` for in-memory snapshot ship, see
149/// `kevy-replicate/docs/snapshot.md`).
150///
151/// On-disk bytes are identical regardless of sink — the same magic +
152/// version header, same entry stream, same `OP_EOF` trailer. Callers
153/// that need durability (disk) wrap in `BufWriter<File>` and call
154/// `sync_all` themselves; callers that need bytes (network ship)
155/// pass a `Vec<u8>`.
156pub fn write_snapshot_to<S: SnapshotSource, W: Write>(src: &S, sink: &mut W) -> io::Result<()> {
157    write_snapshot_to_with_cursor(src, sink, None)
158}
159
160/// [`write_snapshot_to`] with the v2.3 recovery-point header: when
161/// `cursor = Some((generation, offset))` the snapshot records the feed
162/// position it was taken at (format v5); `None` writes the legacy v4
163/// stream unchanged.
164pub fn write_snapshot_to_with_cursor<S: SnapshotSource, W: Write>(
165    src: &S,
166    sink: &mut W,
167    cursor: Option<(u64, u64)>,
168) -> io::Result<()> {
169    // v2.4: field-TTL records force format v6; collect them first so
170    // the header version is known before anything is written.
171    let mut fttl: Vec<(Vec<u8>, Vec<u8>, u64)> = Vec::new();
172    src.for_each_hash_ttl(|k, f, d| fttl.push((k.to_vec(), f.to_vec(), d)));
173    let mut w = BufWriter::with_capacity(SNAPSHOT_BUF_CAP, sink);
174    w.write_all(MAGIC)?;
175    let version = if !fttl.is_empty() {
176        VERSION_HASH_TTL
177    } else if cursor.is_some() {
178        VERSION_FEED_CURSOR
179    } else {
180        VERSION
181    };
182    w.write_all(&[version])?;
183    if version >= VERSION_FEED_CURSOR {
184        let (generation, offset) = cursor.unwrap_or((0, 0));
185        w.write_all(&generation.to_le_bytes())?;
186        w.write_all(&offset.to_le_bytes())?;
187    }
188    // The source yields *remaining* ms; v3 persists the absolute
189    // Unix-ms deadline (now + remaining) so the TTL survives a restart.
190    let now = kevy_store::now_unix_ms();
191    // Enumeration is infallible; capture the first write error to surface.
192    let mut err: Option<io::Error> = None;
193    src.for_each_entry(|key, value, ttl| {
194        let deadline = ttl.map(|ms| now.saturating_add(ms));
195        if err.is_none()
196            && let Err(e) = write_entry(&mut w, key, value, deadline)
197        {
198            err = Some(e);
199        }
200    });
201    if let Some(e) = err {
202        return Err(e);
203    }
204    for (k, f, d) in &fttl {
205        w.write_all(&[OP_HFTTL])?;
206        write_bytes(&mut w, k)?;
207        write_bytes(&mut w, f)?;
208        w.write_all(&d.to_le_bytes())?;
209    }
210    w.write_all(&[OP_EOF])?;
211    w.flush()?;
212    Ok(())
213}
214
215/// The write half of [`save_snapshot`]: produce the durable (fsynced)
216/// `<path>.tmp` and return its path **without** the final rename. For the
217/// COW background-save flow: the serializer thread writes the temp file at
218/// leisure, then the store-owning thread renames it in the same critical
219/// section that resets the AOF — keeping the snapshot/AOF commit adjacent
220/// instead of seconds apart.
221pub fn write_snapshot_tmp<S: SnapshotSource>(src: &S, path: &Path) -> io::Result<std::path::PathBuf> {
222    let tmp = tmp_path(path);
223    {
224        let mut file = File::create(&tmp)?;
225        write_snapshot_to(src, &mut file)?;
226        file.sync_all()?; // durably on disk before the rename
227    }
228    Ok(tmp)
229}
230
231/// Read the v2.3 recovery-point cursor from a snapshot's header:
232/// `Some((generation, offset))` for format v5+, `None` for older
233/// (cursor-less) snapshots. Does not load entries.
234pub fn read_snapshot_cursor(path: &Path) -> io::Result<Option<(u64, u64)>> {
235    let mut r = BufReader::new(File::open(path)?);
236    let mut magic = [0u8; 8];
237    r.read_exact(&mut magic)?;
238    if &magic != MAGIC {
239        return Err(io::Error::new(io::ErrorKind::InvalidData, "kevy snapshot: bad magic"));
240    }
241    let version = read_u8(&mut r)?;
242    if version < VERSION_FEED_CURSOR {
243        return Ok(None);
244    }
245    let mut cur = [0u8; 16];
246    r.read_exact(&mut cur)?;
247    let generation = u64::from_le_bytes(cur[..8].try_into().expect("8 bytes"));
248    let offset = u64::from_le_bytes(cur[8..].try_into().expect("8 bytes"));
249    Ok(Some((generation, offset)))
250}
251
252/// Load a snapshot from `path` into `store` (entries are inserted, not cleared
253/// first — call on a fresh store). Errors on a bad magic/version or truncation.
254pub fn load_snapshot(store: &mut Store, path: &Path) -> io::Result<()> {
255    let r = BufReader::new(File::open(path)?);
256    load_snapshot_from(store, r)
257}
258
259/// Load a snapshot from any [`std::io::Read`] sink into `store` —
260/// symmetric to [`write_snapshot_to`]. Used by the v3-cluster
261/// replication path (sink = `&[u8]` wrapped in `std::io::Cursor`) to
262/// apply a primary-shipped snapshot to a fresh local store without
263/// touching disk. Entries are inserted, not cleared first — call on
264/// a fresh store. Errors on bad magic/version or truncation.
265pub fn load_snapshot_from<R: Read>(store: &mut Store, mut r: R) -> io::Result<()> {
266    let mut magic = [0u8; 8];
267    r.read_exact(&mut magic)?;
268    if &magic != MAGIC {
269        return Err(io::Error::new(
270            io::ErrorKind::InvalidData,
271            "kevy snapshot: bad magic",
272        ));
273    }
274    let version = read_u8(&mut r)?;
275    if !(VERSION_RELATIVE_TTL..=VERSION_HASH_TTL).contains(&version) {
276        return Err(io::Error::new(
277            io::ErrorKind::InvalidData,
278            "kevy snapshot: bad version",
279        ));
280    }
281    if version >= VERSION_FEED_CURSOR {
282        // Loader-side the cursor is advisory (read via
283        // [`read_snapshot_cursor`] by restore tooling); skip it here.
284        let mut cur = [0u8; 16];
285        r.read_exact(&mut cur)?;
286    }
287    // v3+ stores absolute Unix-ms deadlines; convert each to remaining ms
288    // against one `now` read so the load is internally consistent. A deadline
289    // already past becomes `Some(0)` → loaded then immediately reaped (lazy
290    // get / active reaper), matching "expired key is gone". v2 ttls are
291    // already remaining, so pass them through.
292    let absolute_ttl = version >= VERSION_ABSOLUTE_TTL;
293    let now = kevy_store::now_unix_ms();
294
295    loop {
296        let op = read_u8(&mut r)?;
297        if op == OP_EOF {
298            return Ok(());
299        }
300        // v2.4 field-TTL records: no ttl/value framing of their own.
301        if op == OP_HFTTL {
302            let key = read_bytes(&mut r)?;
303            let field = read_bytes(&mut r)?;
304            let mut d = [0u8; 8];
305            r.read_exact(&mut d)?;
306            store.load_hash_field_ttl(&key, &field, u64::from_le_bytes(d));
307            continue;
308        }
309        let raw_ttl = read_ttl(&mut r)?;
310        let ttl = if absolute_ttl {
311            raw_ttl.map(|deadline| deadline.saturating_sub(now))
312        } else {
313            raw_ttl
314        };
315        let key = read_bytes(&mut r)?;
316        match op {
317            OP_STR => {
318                let val = read_bytes(&mut r)?;
319                store.load_str(key, val, ttl);
320            }
321            OP_HASH => {
322                let n = read_u32(&mut r)? as usize;
323                let mut fields = Vec::with_capacity(n);
324                for _ in 0..n {
325                    let f = read_bytes(&mut r)?;
326                    let v = read_bytes(&mut r)?;
327                    fields.push((f, v));
328                }
329                store.load_hash(key, fields, ttl);
330            }
331            OP_LIST => {
332                let n = read_u32(&mut r)? as usize;
333                let mut items = Vec::with_capacity(n);
334                for _ in 0..n {
335                    items.push(read_bytes(&mut r)?);
336                }
337                store.load_list(key, items, ttl);
338            }
339            OP_SET => {
340                let n = read_u32(&mut r)? as usize;
341                let mut members = Vec::with_capacity(n);
342                for _ in 0..n {
343                    members.push(read_bytes(&mut r)?);
344                }
345                store.load_set(key, members, ttl);
346            }
347            OP_ZSET => {
348                let n = read_u32(&mut r)? as usize;
349                let mut pairs = Vec::with_capacity(n);
350                for _ in 0..n {
351                    let m = read_bytes(&mut r)?;
352                    let score = f64::from_bits(read_u64(&mut r)?);
353                    pairs.push((m, score));
354                }
355                store.load_zset(key, pairs, ttl);
356            }
357            OP_STREAM => {
358                let last_ms = read_u64(&mut r)?;
359                let last_seq = read_u64(&mut r)?;
360                let mxd_ms = read_u64(&mut r)?;
361                let mxd_seq = read_u64(&mut r)?;
362                let entries_added = read_u64(&mut r)?;
363                let n = read_u32(&mut r)? as usize;
364                let mut entries = Vec::with_capacity(n);
365                for _ in 0..n {
366                    let ms = read_u64(&mut r)?;
367                    let seq = read_u64(&mut r)?;
368                    let nf = read_u32(&mut r)? as usize;
369                    let mut fv = Vec::with_capacity(nf);
370                    for _ in 0..nf {
371                        let f = read_bytes(&mut r)?;
372                        let v = read_bytes(&mut r)?;
373                        fv.push((f, v));
374                    }
375                    entries.push((ms, seq, fv));
376                }
377                // v4 appends the consumer-group section; v2/v3 files
378                // predate groups-in-snapshot, so they load with none.
379                let groups = if version >= VERSION {
380                    read_stream_groups(&mut r)?
381                } else {
382                    Vec::new()
383                };
384                store.load_stream(
385                    key,
386                    entries,
387                    (last_ms, last_seq),
388                    (mxd_ms, mxd_seq),
389                    entries_added,
390                    groups,
391                    ttl,
392                );
393            }
394            other => {
395                return Err(io::Error::new(
396                    io::ErrorKind::InvalidData,
397                    format!("kevy snapshot: unknown opcode {other}"),
398                ));
399            }
400        }
401    }
402}
403
404/// Serialize one entry: `[op][ttl][key][payload]`.
405fn write_entry<W: Write>(w: &mut W, key: &[u8], value: &Value, ttl: Option<u64>) -> io::Result<()> {
406    let op = match value {
407        Value::Str(_) | Value::Int(_) | Value::ArcBulk(_) => OP_STR, // L1/L2: all reuse OP_STR.
408
409        Value::Hash(_) | Value::SmallHashInline(_) => OP_HASH,
410        Value::List(_) | Value::SmallListInline(_) => OP_LIST,
411        // A.7 O5: both Set encodings share the OP_SET wire format —
412        // payload is `[len: u32 LE][bulk: len-prefixed bytes]*`, agnostic
413        // of whether the in-memory representation is `SmallSetInline` or
414        // `Arc<KevySet>`.
415        Value::Set(_) | Value::SmallSetInline(_) => OP_SET,
416        Value::ZSet(_) | Value::SmallZSetInline(_) => OP_ZSET,
417        Value::Stream(_) => OP_STREAM,
418    };
419    w.write_all(&[op])?;
420    write_ttl(w, ttl)?;
421    write_bytes(w, key)?;
422    match value {
423        Value::Str(v) => write_bytes(w, v.as_slice()),
424        Value::Int(n) => write_bytes(w, n.to_string().as_bytes()),
425        Value::ArcBulk(a) => write_bytes(w, a.as_ref()),
426        Value::Hash(h) => snapshot_payload::write_hash_payload(w, h),
427        Value::SmallHashInline(h) => snapshot_payload::write_small_hash_payload(w, h),
428        Value::List(l) => snapshot_payload::write_list_payload(w, l),
429        Value::SmallListInline(l) => snapshot_payload::write_small_list_payload(w, l),
430        Value::Set(set) => snapshot_payload::write_set_payload(w, set),
431        Value::SmallSetInline(s) => snapshot_payload::write_small_set_payload(w, s),
432        Value::ZSet(z) => snapshot_payload::write_zset_payload(w, z),
433        Value::SmallZSetInline(z) => snapshot_payload::write_small_zset_payload(w, z),
434        Value::Stream(s) => snapshot_payload::write_stream_payload(w, s),
435    }
436}
437
438/// v4 consumer-group section: `[n_groups][per group: name, last_delivered,
439/// consumers (name + last_seen_ms), PEL rows]`. Tombstone PEL rows are kept
440/// — the snapshot path is the full-fidelity one (the AOF rewrite can't
441/// re-create them via XCLAIM, see `rewrite_fmt`).
442pub(crate) fn write_stream_groups<W: Write>(w: &mut W, groups: &[kevy_store::LoadedGroup]) -> io::Result<()> {
443    w.write_all(&(groups.len() as u32).to_le_bytes())?;
444    for g in groups {
445        write_bytes(w, &g.name)?;
446        w.write_all(&g.last_delivered.0.to_le_bytes())?;
447        w.write_all(&g.last_delivered.1.to_le_bytes())?;
448        w.write_all(&(g.consumers.len() as u32).to_le_bytes())?;
449        for (name, last_seen_ms) in &g.consumers {
450            write_bytes(w, name)?;
451            w.write_all(&last_seen_ms.to_le_bytes())?;
452        }
453        w.write_all(&(g.pel.len() as u32).to_le_bytes())?;
454        for (ms, seq, consumer, delivery_time_ms, delivery_count) in &g.pel {
455            w.write_all(&ms.to_le_bytes())?;
456            w.write_all(&seq.to_le_bytes())?;
457            write_bytes(w, consumer)?;
458            w.write_all(&delivery_time_ms.to_le_bytes())?;
459            w.write_all(&delivery_count.to_le_bytes())?;
460        }
461    }
462    Ok(())
463}
464
465/// Loader-side twin of [`write_stream_groups`].
466fn read_stream_groups<R: Read>(r: &mut R) -> io::Result<Vec<kevy_store::LoadedGroup>> {
467    let n = read_u32(r)? as usize;
468    let mut groups = Vec::with_capacity(n);
469    for _ in 0..n {
470        let name = read_bytes(r)?;
471        let last_delivered = (read_u64(r)?, read_u64(r)?);
472        let nc = read_u32(r)? as usize;
473        let mut consumers = Vec::with_capacity(nc);
474        for _ in 0..nc {
475            let cname = read_bytes(r)?;
476            consumers.push((cname, read_u64(r)?));
477        }
478        let np = read_u32(r)? as usize;
479        let mut pel = Vec::with_capacity(np);
480        for _ in 0..np {
481            let ms = read_u64(r)?;
482            let seq = read_u64(r)?;
483            let consumer = read_bytes(r)?;
484            let delivery_time_ms = read_u64(r)?;
485            let delivery_count = read_u32(r)?;
486            pel.push((ms, seq, consumer, delivery_time_ms, delivery_count));
487        }
488        groups.push(kevy_store::LoadedGroup { name, last_delivered, consumers, pel });
489    }
490    Ok(groups)
491}
492
493fn write_ttl<W: Write>(w: &mut W, ttl: Option<u64>) -> io::Result<()> {
494    match ttl {
495        Some(ms) => {
496            w.write_all(&[1u8])?;
497            w.write_all(&ms.to_le_bytes())?;
498        }
499        None => w.write_all(&[0u8])?,
500    }
501    Ok(())
502}
503
504fn read_ttl<R: Read>(r: &mut R) -> io::Result<Option<u64>> {
505    if read_u8(r)? == 1 {
506        Ok(Some(read_u64(r)?))
507    } else {
508        Ok(None)
509    }
510}
511
512fn tmp_path(path: &Path) -> std::path::PathBuf {
513    let mut s = path.as_os_str().to_owned();
514    s.push(".tmp");
515    s.into()
516}
517
518pub(crate) fn write_bytes<W: Write>(w: &mut W, b: &[u8]) -> io::Result<()> {
519    w.write_all(&(b.len() as u32).to_le_bytes())?;
520    w.write_all(b)
521}
522
523fn read_bytes<R: Read>(r: &mut R) -> io::Result<Vec<u8>> {
524    let len = read_u32(r)? as usize;
525    let mut buf = vec![0u8; len];
526    r.read_exact(&mut buf)?;
527    Ok(buf)
528}
529
530fn read_u8<R: Read>(r: &mut R) -> io::Result<u8> {
531    let mut b = [0u8; 1];
532    r.read_exact(&mut b)?;
533    Ok(b[0])
534}
535
536fn read_u32<R: Read>(r: &mut R) -> io::Result<u32> {
537    let mut b = [0u8; 4];
538    r.read_exact(&mut b)?;
539    Ok(u32::from_le_bytes(b))
540}
541
542fn read_u64<R: Read>(r: &mut R) -> io::Result<u64> {
543    let mut b = [0u8; 8];
544    r.read_exact(&mut b)?;
545    Ok(u64::from_le_bytes(b))
546}
547
548#[cfg(test)]
549mod tests;
550#[cfg(test)]
551mod tests_aof;
552#[cfg(test)]
553mod tests_rewrite;