Skip to main content

kevy_embedded/
lib.rs

1//! kevy-embedded — kevy without the network.
2//!
3//! In-process Redis-compatible key–value store: load + reply directly from
4//! your own threads, no TCP, no shards, no reactor. Use this when you want
5//! kevy's data structures + persistence in the same address space as your
6//! app — caches, embedded databases, WASM blobs, sidecar tools.
7//!
8//! Zero crates.io dependencies: only `kevy-store` (the keyspace)
9//! and `kevy-persist` (snapshot + AOF). The whole network layer
10//! (`kevy-rt`, `kevy-sys`, `kevy-uring`) is intentionally NOT pulled in.
11//!
12//! # Quick start
13//!
14//! ```
15//! use kevy_embedded::{Store, Config};
16//!
17//! # fn main() -> kevy_embedded::KevyResult<()> {
18//! let s = Store::open(Config::default())?;
19//! s.set(b"greeting", b"hello")?;
20//! assert_eq!(s.get(b"greeting")?, Some(b"hello".to_vec()));
21//! # Ok(())
22//! # }
23//! ```
24//!
25//! # With persistence
26//!
27//! `with_persist(dir)` enables AOF auto-append on every write and replays
28//! on `open` — restart-safe out of the box. Snapshot (`dump-0.rdb`) is
29//! loaded first if present; AOF (`aof-0.aof`) is replayed on top.
30//!
31//! ```no_run
32//! use kevy_embedded::{Store, Config};
33//!
34//! # fn main() -> kevy_embedded::KevyResult<()> {
35//! let s = Store::open(Config::default().with_persist("./data"))?;
36//! s.set(b"counter", b"42")?;
37//! drop(s); // flushes AOF on drop
38//!
39//! // Next process: state survives.
40//! let s2 = Store::open(Config::default().with_persist("./data"))?;
41//! assert_eq!(s2.get(b"counter")?, Some(b"42".to_vec()));
42//! # Ok(())
43//! # }
44//! ```
45//!
46//! # When NOT to use this crate
47//!
48//! - You want a Redis-protocol TCP server → use the `kevy` crate's
49//!   [`serve`](https://docs.rs/kevy/latest/kevy/fn.serve.html) instead.
50//! - You need cross-process concurrency → kevy-embedded is single-process
51//!   (one mutex). Multi-process needs the network layer. A persist dir is
52//!   one engine's: `Store::open` on a dir a live engine holds — in this
53//!   process or another — errors (`flock` on `<dir>/LOCK`) instead of
54//!   letting two writers interleave one AOF.
55//!
56//! # Locking & concurrency
57//!
58//! The keyspace is split into `Config::shards` independent shards, each a
59//! `kevy_store::Store` behind its own `RwLock` (default: **1 shard** = one
60//! lock over the whole keyspace). A key maps to its shard by hash; writes take
61//! that shard's exclusive lock.
62//!
63//! **Reads take a *shared* per-shard lock where it's sound to.** `GET` (and the
64//! FFI zero-copy `get_shared` lane) use the shared lock whenever the active
65//! eviction policy won't consume a per-read LRU/LFU tick — `maxmemory == 0`
66//! (the default), or the `NoEviction` / `*Random` / `VolatileTtl` policies. The
67//! true LRU/LFU policies (`*Lru` / `*Lfu`) instead take the exclusive lock so
68//! each access stamps the clock the eviction scorer ranks by. Read-only
69//! aggregations (`DBSIZE`, `used_memory`, the `INFO` counters) likewise take
70//! shared locks so a full-keyspace scan doesn't stall concurrent writers.
71//!
72//! This is a **lock-correctness** property, not a throughput one: a read-only
73//! operation doesn't hold the exclusive lock against a concurrent writer on its
74//! shard. It is **not** a lock-free read path — concurrent readers still
75//! contend on the shard's `RwLock` word (a shared cache line), so read scaling
76//! is bounded by **shard count**, not core count. To spread read/write
77//! contention across cores, raise `Config::shards`.
78//!
79//! **Known limitation:** the sibling reads (`hget`, `exists`, `smembers`,
80//! `zscore`, `llen`, `scard`, `zcard`, `type_of`, `ttl_ms`, …) currently still
81//! take the shard's *write* lock even though the underlying keyspace methods
82//! are read-only. Moving them onto the shared lane is a tracked follow-up
83//! (bench-gated separately from the `GET` lane above).
84//!
85//! # Cargo features
86//!
87//! `default` is the full surface. For constrained targets (IoT / edge)
88//! cut it down with `default-features = false, features = [...]`:
89//!
90//! | feature | adds |
91//! |---------|------|
92//! | `core` | in-memory KV + TTL + pub/sub + pipeline/atomic (the minimal base) |
93//! | `persist` | snapshot + AOF durability (`with_persist`, replay on open) |
94//! | `index` | secondary indexes + views |
95//! | `text` | full-text index segments (implies `index`) |
96//! | `vector` | HNSW vector index segments (implies `index`) |
97//! | `replicate` | embed-as-replica / embed-as-writer + CDC feed (implies `persist`) |
98//! | `listener` | the read-only RESP listener |
99#![forbid(unsafe_code)]
100#![warn(missing_docs)]
101
102mod config;
103mod dispatch;
104mod info;
105// Unconditional: `OpenReport` rides the DropGuard and the Store
106// handle in every archetype (a no-persist open reports zeros); only
107// the sink WIRING stays persist-gated in config.rs.
108#[cfg(all(feature = "listener", not(target_arch = "wasm32")))]
109mod listener;
110mod metric;
111mod op_manifest;
112mod ops;
113mod ops_atomic;
114mod ops_atomic_all;
115#[cfg(feature = "index")]
116mod ops_atomic_all_index;
117mod ops_atomic_all_reads;
118mod ops_bitmap;
119mod ops_blocking;
120mod ops_bonus;
121#[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
122mod ops_feed;
123mod ops_hash_ttl;
124#[cfg(feature = "index")]
125mod ops_index;
126#[cfg(feature = "index")]
127mod ops_index_cold;
128#[cfg(feature = "index")]
129mod ops_index_sync;
130#[cfg(all(feature = "index", feature = "persist", not(target_arch = "wasm32")))]
131mod ops_index_window;
132mod ops_keyspace;
133mod ops_more;
134mod ops_p2;
135mod ops_p3;
136mod ops_pipeline;
137mod ops_reconcile;
138mod ops_scan;
139mod ops_snapshot_view;
140#[cfg(feature = "index")]
141mod ops_table;
142#[cfg(feature = "index")]
143mod ops_view;
144mod ops_zset_algebra;
145mod ops_zset_flags;
146mod store_glue;
147pub use ops_atomic::AtomicCtx;
148pub use ops_atomic_all::AtomicAllShards;
149// BITOP's operator now lives with its arithmetic in kevy-store, so
150// the server's wire path can reach the same one. Re-exported here
151// because it has been part of this crate's surface since 1.x.
152pub use kevy_store::BitOp;
153pub use ops_pipeline::Pipeline;
154mod pubsub;
155mod pubsub_bus;
156mod reaper;
157#[cfg(feature = "persist")]
158mod replay;
159#[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
160mod replica_glue;
161#[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
162mod replica_runner;
163#[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
164mod replica_source;
165mod shard;
166#[cfg(feature = "persist")]
167mod shard_restore;
168mod store;
169mod store_inner;
170#[cfg(feature = "persist")]
171mod store_persist;
172mod store_tick;
173mod store_wire;
174
175#[cfg(feature = "tier")]
176pub use config::TierBudgetSpec;
177pub use config::{Config, EvictionPolicy, TtlReaperMode};
178#[cfg(feature = "tier")]
179mod config_tier;
180#[cfg(feature = "persist")]
181pub use config::AppendFsync;
182pub use info::{KevyInfo, KevyTierInfo};
183#[cfg(feature = "index")]
184pub use kevy_index::{AggBy, AnnSpec, GroupStats, Leaf as ViewLeaf, Tree as ViewTree, ViewMode};
185#[cfg(feature = "index")]
186pub use kevy_index::{
187    Cursor as IndexCursor, IndexKind, IndexValue, SegmentStats as IndexStats,
188    ValType as IndexValType,
189};
190#[cfg(feature = "persist")]
191pub use kevy_persist::RewriteStats;
192pub use kevy_store::{
193    ExpireStats, GetShared, HExpireCode, HExpireCond, KevyError, KevyResult, ScoreBound,
194    StoreError, ZAggregate, ZaddFlags, ZaddReport,
195};
196#[cfg(feature = "persist")]
197pub use metric::KevyMetric;
198pub use metric::OpenReport;
199#[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
200pub use ops_feed::{Change, ChangeBatch, FeedError, PrefixInfo};
201#[cfg(feature = "index")]
202pub use ops_index::IndexPage;
203#[cfg(feature = "index")]
204pub use ops_index::advise::IdxAdvice;
205#[cfg(feature = "index")]
206pub use ops_index::claused::{ScalarPage, ScalarQueryOpts, ValueFilter};
207#[cfg(feature = "text")]
208pub use ops_index::highlight::{FacetCounts, MatchOpts, MatchPage};
209pub use ops_reconcile::ReconcileReport;
210pub use ops_snapshot_view::{Snapshot, SnapshotEntry};
211#[cfg(feature = "index")]
212pub use ops_view::ViewPage;
213// The TABLE face — the dogfood report's F7: `Store::table_declare` takes
214// a `TableSpec` the facade did not export, so the typed face of a
215// flagship v4 feature was uncallable without depending on kevy-index
216// directly. The consumer gate (tools/facadegate) now builds against
217// these from outside the workspace, which is what would have caught it.
218#[cfg(feature = "index")]
219pub use kevy_index::{IndexVerify, OrderPath, TableEnsure, TableIndex, TableSpec, TableVerify};
220// `each_prefix` hands the callback a `kevy_store::Value` — same class of
221// gap: a public signature whose type the facade could not name.
222pub use kevy_store::Value;
223pub use pubsub::{PubsubFrame, Subscription};
224pub use store::{Store, WeakStore};
225
226/// Feed kevy's clocks on `wasm32-unknown-unknown`, which has neither
227/// `Instant` nor `SystemTime`. Without a host-fed clock, TTL operations and
228/// the reaper would trap. Call [`set_clock_ns`] (monotonic ns, e.g.
229/// `Date.now() * 1e6`) before TTL-sensitive ops and once per `tick`, and
230/// [`set_wall_clock_ms`] (Unix-epoch millis) if you use `XADD` auto-IDs or
231/// `EXPIREAT`. No-ops conceptually on native targets — hence wasm-only.
232#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
233pub use kevy_store::{set_clock_ns, set_wall_clock_ms};