kevy_embedded/store_inner.rs
1//! The [`Store`]'s shared internals — per-shard [`Inner`], the
2//! last-clone [`DropGuard`], and the [`WeakStore`] handle (split out
3//! of `store.rs` to keep it under the 500-LOC project ceiling;
4//! behaviour unchanged).
5
6// A discarded fsync. A transient failure self-heals — `dirty` stays
7// set and the next tick retries — but a persistent one (full disk,
8// read-only remount, EIO) means `appendfsync everysec` has quietly
9// become "never" with nothing saying so. Open question §2.
10#![expect(
11 clippy::let_underscore_must_use,
12 reason = "a persistent fsync failure is invisible; see .claude/OPEN-QUESTIONS-6.4.md"
13)]
14
15use std::sync::atomic::{AtomicBool, Ordering};
16use std::sync::{Arc, Mutex, RwLock, Weak};
17use std::thread::JoinHandle;
18
19#[cfg(feature = "persist")]
20use kevy_persist::Aof;
21
22use crate::config::Config;
23use crate::pubsub::PubsubBus;
24#[cfg(feature = "persist")]
25use crate::store::lock_write;
26use crate::store::{Shards, Store};
27
28/// Weak handle to a `Store` — does not keep the underlying keyspace alive.
29///
30/// Used by the URL-keyed registry in `kevy-client` so that multiple
31/// `Connection::connect("mem://name")` calls share the same backing store
32/// without leaking it when all strong handles go away.
33#[derive(Debug, Clone)]
34pub struct WeakStore {
35 shards: Weak<Vec<Arc<RwLock<Inner>>>>,
36 guard: Weak<DropGuard>,
37 config: Config,
38 #[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
39 feed_weak: Option<std::sync::Weak<Mutex<kevy_replicate::feed::FeedSource>>>,
40 blocker_weak: Weak<crate::ops_blocking::Blocker>,
41 #[cfg(feature = "index")]
42 indexes_weak: Weak<crate::ops_index::IndexReg>,
43 #[cfg(feature = "index")]
44 views_weak: Weak<crate::ops_view::ViewReg>,
45}
46
47impl WeakStore {
48 /// Try to upgrade back to a `Store`. Returns `None` if the last strong
49 /// reference has already been dropped.
50 pub fn upgrade(&self) -> Option<Store> {
51 let guard = self.guard.upgrade()?;
52 Some(Store {
53 shards: self.shards.upgrade()?,
54 config: self.config.clone(),
55 #[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
56 feed: self.feed_weak.as_ref().and_then(std::sync::Weak::upgrade),
57 blocker: self.blocker_weak.upgrade()?,
58 #[cfg(feature = "index")]
59 indexes: self.indexes_weak.upgrade()?,
60 #[cfg(feature = "index")]
61 views: self.views_weak.upgrade()?,
62 #[cfg(feature = "index")]
63 tables: guard.tables.clone(),
64 // The report rides the DropGuard (engine lifetime), so a
65 // resurrection that outlives every full Store handle
66 // still reports the ORIGINAL boot's replay verdict.
67 open_report: guard.open_report.clone(),
68 guard,
69 })
70 }
71}
72
73impl Store {
74 /// Get a weak handle that does not keep the keyspace alive.
75 pub fn downgrade(&self) -> WeakStore {
76 WeakStore {
77 shards: Arc::downgrade(&self.shards),
78 guard: Arc::downgrade(&self.guard),
79 config: self.config.clone(),
80 #[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
81 feed_weak: self.feed.as_ref().map(Arc::downgrade),
82 blocker_weak: Arc::downgrade(&self.blocker),
83 #[cfg(feature = "index")]
84 indexes_weak: Arc::downgrade(&self.indexes),
85 #[cfg(feature = "index")]
86 views_weak: Arc::downgrade(&self.views),
87 }
88 }
89}
90
91#[derive(Debug)]
92pub(crate) struct Inner {
93 pub(crate) store: kevy_store::Store,
94 #[cfg(feature = "persist")]
95 pub(crate) aof: Option<Aof>,
96 /// Pub/sub bus. Only shard 0's is ever used (pub/sub is process-wide);
97 /// other shards carry an idle one (cheap).
98 pub(crate) bus: PubsubBus,
99 /// Shared replication source if this store is an embed-as-writer.
100 /// Every shard holds a clone of the same `Arc<Mutex<...>>` so
101 /// `commit_write` can push mutations without reaching back up
102 /// through the `DropGuard`.
103 #[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
104 pub(crate) writer_source:
105 Option<std::sync::Arc<Mutex<kevy_replicate::source::ReplicationSource>>>,
106 /// CDC feed (one stream per store); every shard holds a clone
107 /// so `commit_write` pushes effects inline. `None` = feed off.
108 #[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
109 pub(crate) feed: Option<std::sync::Arc<Mutex<kevy_replicate::feed::FeedSource>>>,
110 /// Blocking-pop wake channel clone (see `Store::blocker`).
111 pub(crate) blocker: Option<Arc<crate::ops_blocking::Blocker>>,
112 /// This shard's index segments + the store-level registry
113 /// handle (for the commit_write hook).
114 #[cfg(feature = "index")]
115 pub(crate) idx_segs: crate::ops_index::ShardSegs,
116 #[cfg(feature = "index")]
117 pub(crate) idx_reg: Option<Arc<crate::ops_index::IndexReg>>,
118 /// This shard's view states + registry handle.
119 #[cfg(feature = "index")]
120 pub(crate) view_segs: crate::ops_view::ShardViews,
121 #[cfg(feature = "index")]
122 pub(crate) view_reg: Option<Arc<crate::ops_view::ViewReg>>,
123}
124
125impl Inner {
126 pub(crate) fn new(
127 store: kevy_store::Store,
128 #[cfg(feature = "persist")] aof: Option<Aof>,
129 ) -> Self {
130 Inner {
131 store,
132 #[cfg(feature = "persist")]
133 aof,
134 bus: PubsubBus::new(),
135 #[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
136 writer_source: None,
137 #[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
138 feed: None,
139 blocker: None,
140 #[cfg(feature = "index")]
141 idx_segs: crate::ops_index::ShardSegs::default(),
142 #[cfg(feature = "index")]
143 idx_reg: None,
144 #[cfg(feature = "index")]
145 view_segs: crate::ops_view::ShardViews::default(),
146 #[cfg(feature = "index")]
147 view_reg: None,
148 }
149 }
150}
151
152/// Owns the reaper-thread handle + the shards for the final AOF flush. Lives
153/// in an `Arc<DropGuard>` shared across every `Store` clone; the drop logic
154/// fires only when the last clone goes away.
155#[derive(Debug)]
156pub(crate) struct DropGuard {
157 /// Set by [`Store::shutdown`]: every later write fails with
158 /// `KevyError::Closed`. Shared across clones (it lives here so ANY
159 /// clone's shutdown gates ALL clones' writes).
160 pub(crate) shutdown: AtomicBool,
161 /// The boot replay verdict. Owned by the guard (engine lifetime),
162 /// so `WeakStore::upgrade` can rebuild a full `Store` — with the
163 /// original boot's report — even after every full handle dropped
164 /// while a subscription kept the engine alive.
165 pub(crate) open_report: Arc<crate::metric::OpenReport>,
166 /// The table registry — owned by the guard (engine lifetime) for
167 /// the same reason as `open_report`: a `WeakStore::upgrade` after
168 /// every full handle dropped must still see the declared tables.
169 #[cfg(feature = "index")]
170 pub(crate) tables: Arc<crate::ops_table::TableReg>,
171 pub(crate) reaper_stop: Option<Arc<AtomicBool>>,
172 pub(crate) reaper_join: Mutex<Option<JoinHandle<()>>>,
173 // Read by the persist flush; without it the strong ref still
174 // pins the shards until the LAST clone (incl. subscriptions) drops.
175 #[cfg_attr(not(feature = "persist"), allow(dead_code))]
176 pub(crate) shards_for_flush: Shards,
177 /// Replica runner thread + reconnect machinery, present iff this
178 /// store was opened with `Config::replica_upstream = Some(...)`.
179 /// Joined here so the runner stops cleanly when the last `Store`
180 /// clone goes away.
181 #[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
182 pub(crate) replica_runner: Option<crate::replica_runner::ReplicaRunner>,
183 /// Feed close-marker inputs — the feed handle + data dir,
184 /// present iff feed enabled AND persistent. Written after the AOF
185 /// flush so the marker's cursor describes durable state.
186 #[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
187 pub(crate) feed_close:
188 Option<(std::sync::Arc<Mutex<kevy_replicate::feed::FeedSource>>, std::path::PathBuf)>,
189 /// Replica-source listener + accepted connection threads, present
190 /// iff this store is an embed-as-writer
191 /// (`Config::embed_writer_listen_addr = Some(...)`). Joined on
192 /// last-clone drop.
193 #[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
194 pub(crate) replica_source: Option<crate::replica_source::ReplicaSource>,
195 /// Exclusive claim on the persist dir (`with_persist`) — one engine
196 /// per directory; a second `Store::open` on the same dir errors
197 /// instead of interleaving appends into this engine's AOF. Held for
198 /// the engine lifetime; released here when the last clone drops
199 /// (the drop body's final AOF flush runs before fields drop).
200 #[cfg(feature = "persist")]
201 pub(crate) _dir_lock: Option<kevy_persist::DirLock>,
202}
203
204impl Drop for DropGuard {
205 fn drop(&mut self) {
206 // Stop the replica runner FIRST so no more frames arrive while
207 // we're shutting down + flushing the AOF (frames would race
208 // with the shutdown path).
209 #[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
210 if let Some(r) = &self.replica_runner {
211 r.shutdown();
212 }
213 // Stop the writer-source accept + connection threads next, so
214 // no new replica picks up bytes mid-flush.
215 #[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
216 if let Some(rs) = &self.replica_source {
217 rs.shutdown();
218 }
219 // Stop + join the reaper, then flush every shard's AOF so EverySec
220 // users don't lose the last sub-second of writes.
221 if let Some(stop) = &self.reaper_stop {
222 stop.store(true, Ordering::Relaxed);
223 }
224 if let Some(j) =
225 self.reaper_join.lock().unwrap_or_else(std::sync::PoisonError::into_inner).take()
226 {
227 let _ = j.join();
228 }
229 #[cfg(feature = "persist")]
230 for shard in self.shards_for_flush.iter() {
231 let mut g = lock_write(shard);
232 if let Some(aof) = &mut g.aof {
233 // Unconditional: `maybe_sync` is a no-op inside the EverySec
234 // window, which let the fsynced close marker below claim
235 // durability the AOF tail didn't have yet — a power loss in
236 // that gap resumed the cursor over a rolled-back store, the
237 // one phantom the generation fence cannot detect. Same
238 // discipline as the server's shutdown_drain.
239 let _ = aof.sync_now();
240 }
241 }
242 // With the AOF durable, record the feed continuity
243 // marker — the cursor now exactly describes on-disk state.
244 #[cfg(all(feature = "replicate", not(target_arch = "wasm32")))]
245 if let Some((feed, dir)) = &self.feed_close {
246 Store::feed_write_close_marker(feed, dir);
247 }
248 }
249}