khive_runtime/events_split/
registry.rs1use super::*;
2
3#[derive(Debug, Clone)]
5pub struct EventsSplitConfig {
6 pub db_path: PathBuf,
9 pub socket_path: Option<PathBuf>,
13}
14
15#[cfg(unix)]
25pub(super) type ClientMap = std::collections::HashMap<PathBuf, Arc<EventsSplitClient>>;
26pub(super) type BackendMap = std::collections::HashMap<PathBuf, (bool, Arc<StorageBackend>)>;
30
31#[cfg(unix)]
32pub(super) fn client_registry() -> &'static std::sync::Mutex<ClientMap> {
33 static REGISTRY: std::sync::OnceLock<std::sync::Mutex<ClientMap>> = std::sync::OnceLock::new();
34 REGISTRY.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
35}
36
37pub(super) fn direct_backend_registry() -> &'static std::sync::Mutex<BackendMap> {
38 static REGISTRY: std::sync::OnceLock<std::sync::Mutex<BackendMap>> = std::sync::OnceLock::new();
39 REGISTRY.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
40}
41
42#[cfg(any(test, feature = "test-internals"))]
49#[doc(hidden)]
50pub struct TestRegistryGuard<'a> {
51 pub(super) root: &'a Path,
52 pub(super) canonical_root: PathBuf,
53}
54
55#[cfg(any(test, feature = "test-internals"))]
56impl<'a> TestRegistryGuard<'a> {
57 pub fn new(root: &'a Path) -> Self {
59 Self {
60 root,
61 canonical_root: std::fs::canonicalize(root).expect("existing unique fixture root"),
62 }
63 }
64
65 pub(super) fn remove_entries<T>(
66 &self,
67 registry: &std::sync::Mutex<std::collections::HashMap<PathBuf, T>>,
68 ) -> Vec<T> {
69 let mut entries = registry
70 .lock()
71 .unwrap_or_else(std::sync::PoisonError::into_inner);
72 let keys: Vec<_> = entries
73 .keys()
74 .filter(|path| path.starts_with(self.root) || path.starts_with(&self.canonical_root))
75 .cloned()
76 .collect();
77 keys.into_iter()
78 .filter_map(|key| entries.remove(&key))
79 .collect()
80 }
81}
82
83#[cfg(any(test, feature = "test-internals"))]
84impl Drop for TestRegistryGuard<'_> {
85 fn drop(&mut self) {
86 let backends = self.remove_entries(direct_backend_registry());
89 #[cfg(unix)]
90 let clients = self.remove_entries(client_registry());
91 drop(backends);
92 #[cfg(unix)]
93 drop(clients);
94 }
95}
96
97#[cfg(unix)]
100pub fn client_for(socket_path: &Path) -> crate::error::RuntimeResult<Arc<EventsSplitClient>> {
101 let mut registry = client_registry()
102 .lock()
103 .unwrap_or_else(std::sync::PoisonError::into_inner);
104 if let Some(existing) = registry.get(socket_path) {
105 return Ok(Arc::clone(existing));
106 }
107 let client = EventsSplitClient::new(socket_path.to_path_buf())?;
108 registry.insert(socket_path.to_path_buf(), Arc::clone(&client));
109 Ok(client)
110}
111
112pub fn direct_backend_for(db_path: &Path) -> crate::error::RuntimeResult<Arc<StorageBackend>> {
115 direct_backend_with_max_readers(db_path, false, None)
116}
117
118pub fn direct_backend_read_only_for(
123 db_path: &Path,
124) -> crate::error::RuntimeResult<Arc<StorageBackend>> {
125 direct_backend_with_max_readers(db_path, true, None)
126}
127
128pub(crate) fn direct_backend_with_max_readers(
130 db_path: &Path,
131 read_only: bool,
132 max_readers: Option<usize>,
133) -> crate::error::RuntimeResult<Arc<StorageBackend>> {
134 let mut config = crate::RuntimeConfig {
135 db_path: Some(db_path.to_path_buf()),
136 ..crate::RuntimeConfig::no_embeddings()
137 };
138 let wal_ceiling = config.resolve_wal_ceiling_policy(read_only)?;
139 let disk_guard = config.resolve_disk_guard_policy(read_only)?;
140 direct_backend_with_policies(
141 db_path,
142 read_only,
143 max_readers,
144 wal_ceiling,
145 disk_guard,
146 config.volume_lock_dir,
147 )
148}
149
150pub(crate) fn direct_backend_with_policies(
154 db_path: &Path,
155 read_only: bool,
156 max_readers: Option<usize>,
157 wal_ceiling: WalCeilingPolicy,
158 disk_guard: Option<khive_db::EffectiveDiskGuardConfig>,
159 volume_lock_dir: Option<PathBuf>,
160) -> crate::error::RuntimeResult<Arc<StorageBackend>> {
161 wal_ceiling.validate_static(true, true, read_only)?;
162 if !read_only {
163 disk_guard
164 .ok_or_else(|| {
165 crate::error::RuntimeError::Internal("missing events disk policy".into())
166 })?
167 .validate()?;
168 khive_db::require_volume_lock_dir(volume_lock_dir.clone())?;
169 }
170 let mut registry = direct_backend_registry()
171 .lock()
172 .unwrap_or_else(std::sync::PoisonError::into_inner);
173 let absolute_path = absolutize(db_path);
174 let db_path = absolute_path.as_path();
175 refuse_events_db_symlinks(db_path)
176 .map_err(|e| crate::error::RuntimeError::Internal(e.to_string()))?;
177 #[cfg(unix)]
178 if !read_only {
179 if let Some(parent) = db_path.parent().filter(|p| !p.as_os_str().is_empty()) {
180 let _ = std::fs::create_dir_all(parent);
181 }
182 }
183 #[cfg(unix)]
189 ensure_events_db_parent_trusted(db_path)
190 .map_err(|e| crate::error::RuntimeError::Internal(e.to_string()))?;
191 let key = std::fs::canonicalize(db_path)
192 .or_else(|error| {
193 if error.kind() != std::io::ErrorKind::NotFound {
194 return Err(error);
195 }
196 let parent = db_path.parent().ok_or(error)?;
197 let name = db_path.file_name().ok_or_else(|| {
198 std::io::Error::new(
199 std::io::ErrorKind::InvalidInput,
200 "events path has no file name",
201 )
202 })?;
203 std::fs::canonicalize(parent).map(|parent| parent.join(name))
204 })
205 .map_err(|e| crate::error::RuntimeError::Internal(e.to_string()))?;
206 if let Some((existing_read_only, existing)) = registry.get(&key) {
207 if *existing_read_only != read_only {
208 let existing_mode = if *existing_read_only {
209 "read-only"
210 } else {
211 "writable"
212 };
213 let requested_mode = if read_only { "read-only" } else { "writable" };
214 return Err(crate::error::RuntimeError::InvalidInput(format!(
215 "events database {} is already open {existing_mode} in this process; cannot \
216 open it {requested_mode}; read-only events access requires a separate frozen snapshot",
217 key.display()
218 )));
219 }
220 let numbers = |p: Option<khive_db::EffectiveDiskGuardConfig>| {
221 p.map(|p| (p.reserve_bytes, p.guard_deadline_ms))
222 };
223 if numbers(existing.pool().effective_disk_guard_config()) != numbers(disk_guard) {
224 return Err(crate::error::RuntimeError::Internal(
225 "events database is already open with a different disk reserve/deadline policy"
226 .into(),
227 ));
228 }
229 if !read_only && existing.pool().config().volume_lock_dir != volume_lock_dir {
230 return Err(crate::error::RuntimeError::Internal(
231 "events database is already open with a different volume-lock directory".into(),
232 ));
233 }
234 let existing_bytes = existing.pool().config().wal_ceiling.bytes;
235 if existing_bytes != wal_ceiling.bytes {
236 return Err(khive_db::SqliteError::InvalidConfig(format!(
237 "events database {} is already open with wal_ceiling_bytes={existing_bytes}; \
238 requested {}; drain and restart before changing the WAL ceiling",
239 key.display(),
240 wal_ceiling.bytes
241 ))
242 .into());
243 }
244 return Ok(Arc::clone(existing));
245 }
246 let db_path = key.as_path();
247 #[cfg(unix)]
253 if !read_only {
254 use std::os::unix::fs::OpenOptionsExt;
255 let _ = std::fs::OpenOptions::new()
256 .write(true)
257 .create_new(true)
258 .mode(0o600)
259 .open(db_path);
260 let _before_open = harden_events_db_sidecars(db_path)
263 .map_err(|e| crate::error::RuntimeError::Internal(e.to_string()))?;
264 }
265 let backend = Arc::new(if read_only {
266 StorageBackend::sqlite_read_only_with_max_readers_and_wal_ceiling(
267 db_path,
268 max_readers,
269 wal_ceiling,
270 )?
271 } else {
272 StorageBackend::sqlite_with_max_readers_and_policies(
273 db_path,
274 max_readers,
275 wal_ceiling,
276 disk_guard.ok_or_else(|| {
277 crate::error::RuntimeError::Internal("missing events disk policy".into())
278 })?,
279 khive_db::require_volume_lock_dir(volume_lock_dir)?,
280 )?
281 });
282 registry.insert(key, (read_only, Arc::clone(&backend)));
283 Ok(backend)
284}
285
286#[cfg(unix)]
289pub fn forwarding_metrics(socket_path: &Path) -> Option<EventsForwardingMetrics> {
290 let registry = client_registry()
291 .lock()
292 .unwrap_or_else(std::sync::PoisonError::into_inner);
293 registry.get(socket_path).map(|client| client.metrics())
294}