1use crate::config::{ConfigError, VelesConfig};
6
7#[cfg(target_pointer_width = "64")]
23const MAX_VECTORS_PER_COLLECTION_CAP: usize = 10_000_000_000;
24#[cfg(not(target_pointer_width = "64"))]
25const MAX_VECTORS_PER_COLLECTION_CAP: usize = 4_000_000_000;
26const MAX_COLLECTIONS_CAP: usize = 1_000_000;
28const MAX_PAYLOAD_SIZE_CAP: usize = 1_073_741_824;
30const MAX_PERFECT_MODE_VECTORS_CAP: usize = 100_000_000;
32const QUERY_TIMEOUT_MS_CAP: u64 = 86_400_000;
37const MAX_LAYERS_CAP: usize = 64;
39const MMAP_CACHE_MB_CAP: usize = 1_048_576;
42const WORKERS_CAP: usize = 4_096;
45
46fn range_check_capacity(key: &str, value: usize, cap: usize) -> Result<(), ConfigError> {
48 if value == 0 || value > cap {
49 return Err(ConfigError::InvalidValue {
50 key: key.to_string(),
51 message: format!("value {value} is out of range [1, {cap}]"),
52 });
53 }
54 Ok(())
55}
56
57fn range_check_upper<T: PartialOrd + Copy + std::fmt::Display>(
61 key: &str,
62 value: T,
63 cap: T,
64) -> Result<(), ConfigError> {
65 if value > cap {
66 return Err(ConfigError::InvalidValue {
67 key: key.to_string(),
68 message: format!("value {value} is out of range [0, {cap}]"),
69 });
70 }
71 Ok(())
72}
73
74impl VelesConfig {
75 pub fn validate(&self) -> Result<(), ConfigError> {
81 self.validate_search()?;
82 self.validate_hnsw()?;
83 self.validate_limits()?;
84 self.validate_server()?;
85 self.validate_storage()?;
86 self.validate_logging()?;
87 self.warn_inert_wal_batch();
88 self.warn_inert_engine_sections();
89 Ok(())
90 }
91
92 fn warn_inert_engine_sections(&self) {
101 fn deviates<T: serde::Serialize>(actual: &T, default: &T) -> bool {
102 match (serde_json::to_value(actual), serde_json::to_value(default)) {
103 (Ok(a), Ok(d)) => a != d,
104 _ => false,
105 }
106 }
107
108 let mut inert: Vec<&str> = Vec::new();
109 if deviates(&self.search, &crate::config::SearchConfig::default()) {
110 inert.push("[search]");
111 }
112 if deviates(&self.hnsw, &crate::config::HnswConfig::default()) {
113 inert.push("[hnsw]");
114 }
115 if deviates(
116 &self.storage,
117 &crate::config::server::StorageConfig::default(),
118 ) {
119 inert.push("[storage]");
120 }
121 if deviates(
122 &self.quantization,
123 &crate::config_quantization::QuantizationConfig::default(),
124 ) {
125 inert.push("[quantization]");
126 }
127 if !inert.is_empty() {
128 tracing::warn!(
129 sections = inert.join(", "),
130 "these config sections are parsed and validated but not yet \
131 applied by the engine — only [limits] is; see issue #2087. \
132 Query-time WITH (...) overrides are unaffected."
133 );
134 }
135 }
136
137 fn warn_inert_wal_batch(&self) {
141 if self.wal_batch.enabled {
142 tracing::warn!(
143 "[wal_batch] enabled = true is parsed but not yet wired: no group \
144 commit occurs and every write keeps its own durability barrier \
145 (see issue #2078)"
146 );
147 }
148 }
149
150 fn validate_search(&self) -> Result<(), ConfigError> {
151 if let Some(ef) = self.search.ef_search {
152 if !(16..=4096).contains(&ef) {
153 return Err(ConfigError::InvalidValue {
154 key: "search.ef_search".to_string(),
155 message: format!("value {ef} is out of range [16, 4096]"),
156 });
157 }
158 }
159
160 if self.search.max_results == 0 || self.search.max_results > 10000 {
161 return Err(ConfigError::InvalidValue {
162 key: "search.max_results".to_string(),
163 message: format!(
164 "value {} is out of range [1, 10000]",
165 self.search.max_results
166 ),
167 });
168 }
169
170 range_check_upper(
173 "search.query_timeout_ms",
174 self.search.query_timeout_ms,
175 QUERY_TIMEOUT_MS_CAP,
176 )
177 }
178
179 fn validate_hnsw(&self) -> Result<(), ConfigError> {
180 if let Some(m) = self.hnsw.m {
181 if !(4..=128).contains(&m) {
182 return Err(ConfigError::InvalidValue {
183 key: "hnsw.m".to_string(),
184 message: format!("value {m} is out of range [4, 128]"),
185 });
186 }
187 }
188
189 if let Some(ef) = self.hnsw.ef_construction {
190 if !(100..=2000).contains(&ef) {
191 return Err(ConfigError::InvalidValue {
192 key: "hnsw.ef_construction".to_string(),
193 message: format!("value {ef} is out of range [100, 2000]"),
194 });
195 }
196 }
197
198 range_check_upper("hnsw.max_layers", self.hnsw.max_layers, MAX_LAYERS_CAP)
201 }
202
203 fn validate_limits(&self) -> Result<(), ConfigError> {
204 let limits = &self.limits;
205 range_check_capacity("limits.max_dimensions", limits.max_dimensions, 65536)?;
206 range_check_capacity(
207 "limits.max_vectors_per_collection",
208 limits.max_vectors_per_collection,
209 MAX_VECTORS_PER_COLLECTION_CAP,
210 )?;
211 range_check_capacity(
212 "limits.max_collections",
213 limits.max_collections,
214 MAX_COLLECTIONS_CAP,
215 )?;
216 range_check_capacity(
217 "limits.max_payload_size",
218 limits.max_payload_size,
219 MAX_PAYLOAD_SIZE_CAP,
220 )?;
221 range_check_capacity(
222 "limits.max_perfect_mode_vectors",
223 limits.max_perfect_mode_vectors,
224 MAX_PERFECT_MODE_VECTORS_CAP,
225 )
226 }
227
228 fn validate_server(&self) -> Result<(), ConfigError> {
229 if self.server.port < 1024 {
230 return Err(ConfigError::InvalidValue {
231 key: "server.port".to_string(),
232 message: format!("value {} must be >= 1024", self.server.port),
233 });
234 }
235
236 range_check_upper("server.workers", self.server.workers, WORKERS_CAP)
239 }
240
241 fn validate_storage(&self) -> Result<(), ConfigError> {
242 let valid_modes = ["mmap", "memory"];
243 if !valid_modes.contains(&self.storage.storage_mode.as_str()) {
244 return Err(ConfigError::InvalidValue {
245 key: "storage.storage_mode".to_string(),
246 message: format!(
247 "value '{}' is invalid, expected one of: {:?}",
248 self.storage.storage_mode, valid_modes
249 ),
250 });
251 }
252
253 range_check_capacity(
256 "storage.mmap_cache_mb",
257 self.storage.mmap_cache_mb,
258 MMAP_CACHE_MB_CAP,
259 )?;
260 Ok(())
261 }
262
263 fn validate_logging(&self) -> Result<(), ConfigError> {
264 let valid_levels = ["error", "warn", "info", "debug", "trace"];
265 if !valid_levels.contains(&self.logging.level.as_str()) {
266 return Err(ConfigError::InvalidValue {
267 key: "logging.level".to_string(),
268 message: format!(
269 "value '{}' is invalid, expected one of: {:?}",
270 self.logging.level, valid_levels
271 ),
272 });
273 }
274 Ok(())
275 }
276}