Skip to main content

a3s_vec/
config.rs

1//! Process-wide configuration and lifecycle.
2
3use crate::error::{Error, Result};
4use crate::storage_ceilings::StorageCeilings;
5use serde::{Deserialize, Serialize};
6use std::sync::{OnceLock, RwLock};
7
8/// WAL acknowledgement policy.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
10pub enum Durability {
11    /// Sync the WAL file before acknowledging each mutation.
12    #[default]
13    Always,
14    /// Sync after the configured operation/byte threshold.
15    Interval,
16    /// Only sync when [`crate::Collection::flush`] is called.
17    Manual,
18}
19
20/// Query-time backend for validated derived index sidecars.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
22#[serde(rename_all = "snake_case")]
23pub enum IoBackend {
24    /// Read bounded extents directly from the sidecar with portable positioned
25    /// file operations.
26    #[default]
27    Positioned,
28    /// Copy the validated sidecar into an immutable anonymous memory map at
29    /// open time, then serve bounded query extents from that snapshot.
30    Mmap,
31}
32
33/// Supported process-wide durability and sidecar-I/O configuration.
34///
35/// Resource and logging controls are intentionally absent until
36/// they have an implemented execution path:
37///
38/// ```compile_fail
39/// use a3s_vec::{ConfigBuilder, LogLevel, LogType};
40///
41/// let _ = (LogLevel::Info, LogType::Console);
42/// let _ = ConfigBuilder::new()
43///     .memory_limit(1024)
44///     .num_threads(2)
45///     .enable_console_log(true)
46///     .fts_brute_force_by_keys_ratio(0.5);
47/// ```
48///
49/// The default [`IoBackend::Positioned`] reader can be replaced by the bounded
50/// immutable mmap snapshot backend:
51///
52/// ```
53/// use a3s_vec::{ConfigBuilder, IoBackend};
54///
55/// let _ = ConfigBuilder::new().io_backend(IoBackend::Mmap).build();
56/// ```
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct ConfigBuilder {
59    pub(crate) durability: Durability,
60    pub(crate) wal_max_ops: Option<u64>,
61    pub(crate) wal_max_bytes: Option<u64>,
62    #[serde(default)]
63    pub(crate) io_backend: IoBackend,
64    /// Persistence `DoS` ceilings for snapshot / index-cache / WAL replay /
65    /// `DiskANN`. Defaults are product constants; never inferred from the host.
66    #[serde(default)]
67    pub(crate) storage_ceilings: StorageCeilings,
68}
69
70impl ConfigBuilder {
71    pub fn new() -> Self {
72        Self {
73            durability: Durability::Always,
74            wal_max_ops: None,
75            wal_max_bytes: None,
76            io_backend: IoBackend::Positioned,
77            storage_ceilings: StorageCeilings::default(),
78        }
79    }
80
81    pub fn durability(mut self, durability: Durability) -> Self {
82        self.durability = durability;
83        self
84    }
85
86    pub fn wal_max_ops(mut self, limit: u64) -> Self {
87        self.wal_max_ops = (limit > 0).then_some(limit);
88        self
89    }
90
91    pub fn wal_max_bytes(mut self, limit: u64) -> Self {
92        self.wal_max_bytes = (limit > 0).then_some(limit);
93        self
94    }
95
96    /// Selects the process default for validated derived-sidecar query reads.
97    pub fn io_backend(mut self, backend: IoBackend) -> Self {
98        self.io_backend = backend;
99        self
100    }
101
102    /// Sets process-wide persistence `DoS` ceilings for collections that do not
103    /// override them through [`crate::CollectionOptions`].
104    pub fn storage_ceilings(mut self, ceilings: StorageCeilings) -> Self {
105        self.storage_ceilings = ceilings;
106        self
107    }
108
109    /// Finalizes the plain-data builder.  No resources are allocated here.
110    pub fn build(self) -> Self {
111        self
112    }
113}
114
115impl Default for ConfigBuilder {
116    fn default() -> Self {
117        Self::new()
118    }
119}
120
121static CONFIG: OnceLock<RwLock<ConfigBuilder>> = OnceLock::new();
122
123fn config_cell() -> &'static RwLock<ConfigBuilder> {
124    CONFIG.get_or_init(|| RwLock::new(ConfigBuilder::default()))
125}
126
127/// Returns a fresh builder with portable defaults.
128pub fn default_config() -> ConfigBuilder {
129    ConfigBuilder::default()
130}
131
132/// Sets the process defaults captured by collections created or opened after
133/// this call. Existing collection handles retain their resolved configuration.
134pub fn initialize(config: Option<&ConfigBuilder>) -> Result<()> {
135    let chosen = config.cloned().unwrap_or_default();
136    *config_cell()
137        .write()
138        .map_err(|_| Error::internal("configuration lock poisoned"))? = chosen;
139    INITIALIZED.store(true, std::sync::atomic::Ordering::Release);
140    Ok(())
141}
142
143/// Returns whether the runtime has been initialized explicitly.
144///
145/// The embedded engine also works with defaults without an explicit call, but
146/// exposing this bit preserves the zvec lifecycle vocabulary.
147pub fn is_initialized() -> bool {
148    INITIALIZED.load(std::sync::atomic::Ordering::Acquire)
149}
150
151static INITIALIZED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
152
153/// Returns a clone of the active configuration for internal consumers.
154pub(crate) fn current_config() -> ConfigBuilder {
155    config_cell()
156        .read()
157        .map_or_else(|_| ConfigBuilder::default(), |v| v.clone())
158}
159
160/// Resets process defaults and marks the runtime uninitialized.
161pub fn shutdown() -> Result<()> {
162    *config_cell()
163        .write()
164        .map_err(|_| Error::internal("configuration lock poisoned"))? = ConfigBuilder::default();
165    INITIALIZED.store(false, std::sync::atomic::Ordering::Release);
166    Ok(())
167}
168
169/// Version of the native Rust implementation.
170pub fn version() -> String {
171    env!("CARGO_PKG_VERSION").to_string()
172}
173
174pub fn check_version(major: i32, minor: i32, patch: i32) -> bool {
175    let mut pieces = env!("CARGO_PKG_VERSION").split('.');
176    let current = (
177        pieces
178            .next()
179            .and_then(|v| v.parse::<i32>().ok())
180            .unwrap_or(0),
181        pieces
182            .next()
183            .and_then(|v| v.parse::<i32>().ok())
184            .unwrap_or(0),
185        pieces
186            .next()
187            .and_then(|v| v.parse::<i32>().ok())
188            .unwrap_or(0),
189    );
190    current >= (major, minor, patch)
191}
192
193pub fn version_major() -> i32 {
194    env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap_or(0)
195}
196
197pub fn version_minor() -> i32 {
198    env!("CARGO_PKG_VERSION_MINOR").parse().unwrap_or(0)
199}
200
201pub fn version_patch() -> i32 {
202    env!("CARGO_PKG_VERSION_PATCH").parse().unwrap_or(0)
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn defaults_are_portable() {
211        let cfg = ConfigBuilder::default();
212        assert_eq!(cfg.durability, Durability::Always);
213        assert_eq!(cfg.io_backend, IoBackend::Positioned);
214        assert_eq!(cfg.storage_ceilings, StorageCeilings::default());
215    }
216
217    #[test]
218    fn zero_checkpoint_limits_are_disabled() {
219        let cfg = ConfigBuilder::default().wal_max_ops(0).wal_max_bytes(0);
220        assert_eq!(cfg.wal_max_ops, None);
221        assert_eq!(cfg.wal_max_bytes, None);
222    }
223}