Skip to main content

a3s_vec/
config.rs

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