storage_kit/state_stores/config/lance.rs
1use std::{path::PathBuf, time::Duration};
2
3use serde::{Deserialize, Serialize};
4
5use crate::StateStoreError;
6
7/// Embedded Lance state storage. Memory belongs to one connected store;
8/// directory connections share persisted state through native atomic commits.
9///
10/// # Examples
11///
12/// ```
13/// use storage_kit::state::config::LanceStateStoreLocation;
14/// let location = LanceStateStoreLocation::Memory;
15/// assert!(matches!(location, LanceStateStoreLocation::Memory));
16/// ```
17#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
18pub enum LanceStateStoreLocation {
19 /// Process-local storage, shared by clones of the connected handle.
20 Memory,
21 /// A local directory, which may also contain Lance Database collections.
22 Directory(PathBuf),
23}
24
25/// Configuration for the embedded Lance StateStore.
26///
27/// The timeout bounds admission and observation. An admitted commit retains
28/// authority after its observer times out or is dropped. Native filesystem
29/// calls may delay the final owner's joining cleanup beyond that timeout.
30///
31/// # Examples
32///
33/// ```
34/// use storage_kit::state::config::{LanceStateStoreConfig, StateStoreConfig};
35/// let config = StateStoreConfig::Lance(LanceStateStoreConfig::memory());
36/// config.validate()?;
37/// # Ok::<(), storage_kit::StateStoreError>(())
38/// ```
39#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
40pub struct LanceStateStoreConfig {
41 location: LanceStateStoreLocation,
42 operation_timeout_ms: u64,
43}
44
45impl LanceStateStoreConfig {
46 /// Selects process-local memory with a 30-second observation timeout.
47 ///
48 /// # Examples
49 ///
50 /// ```
51 /// use storage_kit::state::config::LanceStateStoreConfig;
52 /// assert_eq!(LanceStateStoreConfig::memory().operation_timeout(),
53 /// std::time::Duration::from_secs(30));
54 /// ```
55 #[must_use]
56 pub fn memory() -> Self {
57 Self {
58 location: LanceStateStoreLocation::Memory,
59 operation_timeout_ms: 30_000,
60 }
61 }
62
63 /// Selects a local directory. No filesystem I/O occurs until connection.
64 ///
65 /// # Examples
66 ///
67 /// ```
68 /// use storage_kit::state::config::LanceStateStoreConfig;
69 /// let config = LanceStateStoreConfig::directory("application-data");
70 /// config.validate()?;
71 /// # Ok::<(), storage_kit::StateStoreError>(())
72 /// ```
73 #[must_use]
74 pub fn directory(path: impl Into<PathBuf>) -> Self {
75 Self {
76 location: LanceStateStoreLocation::Directory(path.into()),
77 operation_timeout_ms: 30_000,
78 }
79 }
80
81 /// Sets the admission and observation timeout, without expiring transactions.
82 ///
83 /// # Errors
84 ///
85 /// Rejects sub-millisecond, zero, overflowing, or unrepresentable deadlines.
86 ///
87 /// # Examples
88 ///
89 /// ```
90 /// use storage_kit::state::config::LanceStateStoreConfig;
91 /// let config = LanceStateStoreConfig::memory()
92 /// .with_operation_timeout(std::time::Duration::from_secs(2))?;
93 /// assert_eq!(config.operation_timeout(), std::time::Duration::from_secs(2));
94 /// # Ok::<(), storage_kit::StateStoreError>(())
95 /// ```
96 pub fn with_operation_timeout(mut self, timeout: Duration) -> Result<Self, StateStoreError> {
97 self.operation_timeout_ms = u64::try_from(timeout.as_millis()).map_err(|_| {
98 StateStoreError::invalid_config("Lance state operation timeout exceeds milliseconds")
99 })?;
100 self.validate()?;
101 Ok(self)
102 }
103
104 /// Borrows the selected storage location.
105 ///
106 /// # Examples
107 ///
108 /// ```
109 /// use storage_kit::state::config::{LanceStateStoreConfig, LanceStateStoreLocation};
110 /// assert_eq!(LanceStateStoreConfig::memory().location(), &LanceStateStoreLocation::Memory);
111 /// ```
112 #[must_use]
113 pub fn location(&self) -> &LanceStateStoreLocation {
114 &self.location
115 }
116
117 /// Returns the admission and observation timeout.
118 ///
119 /// # Examples
120 ///
121 /// ```
122 /// use storage_kit::state::config::LanceStateStoreConfig;
123 /// assert_eq!(LanceStateStoreConfig::memory().operation_timeout().as_secs(), 30);
124 /// ```
125 #[must_use]
126 pub fn operation_timeout(&self) -> Duration {
127 Duration::from_millis(self.operation_timeout_ms)
128 }
129
130 /// Checks local configuration without opening storage.
131 ///
132 /// # Errors
133 ///
134 /// Rejects empty, NUL-containing, non-UTF-8, or URI directory paths, and
135 /// zero or unrepresentable observation timeouts. External URIs are outside
136 /// this embedded backend's supported scope.
137 ///
138 /// # Examples
139 ///
140 /// ```
141 /// use storage_kit::state::config::LanceStateStoreConfig;
142 /// assert!(LanceStateStoreConfig::directory("").validate().is_err());
143 /// LanceStateStoreConfig::memory().validate()?;
144 /// # Ok::<(), storage_kit::StateStoreError>(())
145 /// ```
146 pub fn validate(&self) -> Result<(), StateStoreError> {
147 if self.operation_timeout_ms == 0
148 || std::time::Instant::now()
149 .checked_add(self.operation_timeout())
150 .is_none()
151 {
152 return Err(StateStoreError::invalid_config(
153 "Lance state operation timeout must be positive and fit the clock range",
154 ));
155 }
156 if let LanceStateStoreLocation::Directory(path) = &self.location {
157 let value = path.to_str().ok_or_else(|| {
158 StateStoreError::invalid_config("Lance state directory must be valid UTF-8")
159 })?;
160 if value.is_empty() || value.contains('\0') || value.contains("://") {
161 return Err(StateStoreError::invalid_config(
162 "Lance state directory must be a nonempty local filesystem path without NUL",
163 ));
164 }
165 }
166 Ok(())
167 }
168}