1use super::super::{
2 changes, embed,
3 live::LysLive,
4 pass::Gate,
5 placement,
6 query::{Options, Setting, Settings},
7 releaser,
8 storage::{KEPT_VERSIONS_DEFAULT, KEPT_VERSIONS_MOST},
9};
10#[cfg(test)]
11use super::endpoint_tests;
12use crate::serve::service::Service;
13use haematite::branch::KeptLimit;
14use haematite::db::DatabaseConfig;
15use haematite::store::NodeCacheBudget;
16use serde::Deserialize;
17use std::num::{NonZeroU32, NonZeroU64, NonZeroUsize};
18use std::path::PathBuf;
19use std::sync::Arc;
20#[derive(Debug, Deserialize)]
21#[serde(deny_unknown_fields)]
22pub struct Config {
23 pub mcp_http: Option<crate::mcp::http::Config>,
24 pub data_dir: PathBuf,
25 pub socket: Option<PathBuf>,
26 pub name: Option<String>,
27 pub branch_cache_bytes_per_shard: NonZeroUsize,
28 pub max_frame: NonZeroU32,
29 pub max_clients: NonZeroUsize,
30 pub max_page: NonZeroU32,
31 #[serde(default = "append_prune_keep_ceiling")]
32 pub append_prune_keep_ceiling: u32,
33 #[serde(default = "append_prune_removal_cap")]
34 pub append_prune_removal_cap: u32,
35 pub timeout_ms: NonZeroU64,
36 pub create: Option<Creation>,
37 #[serde(default = "changes_scan_records")]
39 pub changes_scan_records: NonZeroU64,
40 #[serde(default = "changes_node_budget")]
41 pub changes_node_budget: NonZeroUsize,
42 #[serde(default = "changes_max_page")]
43 pub changes_max_page: NonZeroU32,
44 #[serde(default, deserialize_with = "super::super::query::settings::time_ms")]
45 pub query_time_ms: Setting<u64>,
46 #[serde(
47 default,
48 deserialize_with = "super::super::query::settings::memory_bytes"
49 )]
50 pub query_memory_bytes: Setting<u64>,
51 #[serde(
52 default,
53 deserialize_with = "super::super::query::settings::outputs_per_record"
54 )]
55 pub query_outputs_per_record: Setting<u64>,
56 #[serde(
57 default,
58 deserialize_with = "super::super::query::settings::scan_records"
59 )]
60 pub query_scan_records: Setting<u64>,
61 #[serde(
62 default,
63 deserialize_with = "super::super::query::settings::max_running"
64 )]
65 pub query_max_running: Setting<u64>,
66 #[serde(default, deserialize_with = "super::super::query::settings::worker")]
67 pub query_worker: Setting<PathBuf>,
68 #[serde(default)]
69 pub embed_worker: Setting<PathBuf>,
70 #[serde(default)]
71 pub embed_model_dir: Setting<PathBuf>,
72 #[serde(default)]
73 pub embed_model_sha256: Setting<String>,
74 #[serde(default)]
75 pub embed_memory_bytes: Setting<u64>,
76 #[serde(default)]
77 pub embed_threads: Setting<u64>,
78 #[serde(default)]
79 pub embed_batch_items: Setting<u64>,
80 #[serde(default)]
81 pub embed_input_bytes: Setting<u64>,
82 #[serde(default)]
83 pub embed_tokens: Setting<u64>,
84 #[serde(default)]
85 pub embed_frame_bytes: Setting<u32>,
86 #[serde(default)]
87 pub embed_ready_ms: Setting<u64>,
88 #[serde(default)]
89 pub embed_call_ms: Setting<u64>,
90 #[serde(default)]
91 pub embed_waiting: Setting<u64>,
92 #[serde(default)]
93 pub embed_request_items: Setting<u64>,
94 #[serde(default)]
95 pub embed_page_records: Setting<u64>,
96 #[serde(default)]
97 pub embed_writer_wait_ms: Setting<u64>,
98 #[serde(default)]
100 pub embed_windows: Setting<bool>,
101 #[serde(default)]
103 pub embed_max_windows: Setting<u64>,
104 #[serde(default)]
106 pub embed_window_overlap_tokens: Setting<u64>,
107 #[serde(default)]
110 pub embed_restart_ms: Setting<u64>,
111 #[serde(default)]
114 pub embed_restart_most_ms: Setting<u64>,
115 #[serde(default)]
120 pub embed_question_worker: Setting<bool>,
121 #[serde(default = "kept_versions", deserialize_with = "kept_versions_within")]
124 pub kept_versions: usize,
125 #[serde(default)]
127 pub retention: releaser::Config,
128 #[serde(default)]
132 pub lys: Option<LysConfig>,
133 #[serde(default)]
136 pub network: Option<crate::serve::transport::network::NetworkConfig>,
137}
138
139#[derive(Debug, Deserialize)]
142#[serde(deny_unknown_fields)]
143pub struct LysConfig {
144 pub issuer: String,
147 pub database: String,
149 #[serde(default)]
156 pub placement_credential_file: Option<PathBuf>,
157}
158
159pub(super) fn lys_parts(
165 lys: Option<&LysConfig>,
166) -> Result<(Option<Gate>, Option<placement::Placer>), String> {
167 let gate = lys.map(lys_gate).transpose()?;
168 let placer = lys
169 .and_then(|lys| {
170 lys.placement_credential_file.as_deref().map(|file| {
171 placement::Lys::new(&lys.issuer, &lys.database, file)
172 .and_then(placement::Placer::start)
173 })
174 })
175 .transpose()?;
176 Ok((gate, placer))
177}
178
179pub(super) fn served_through_lys(
182 mut service: Service,
183 gate: Option<Gate>,
184 placer: Option<placement::Placer>,
185) -> Result<Service, String> {
186 if let Some(gate) = gate {
187 service = service.with_lys(gate);
188 }
189 if let Some(placer) = placer {
190 service = service
191 .with_placer(placer)
192 .map_err(|error| error.to_string())?;
193 }
194 Ok(service)
195}
196
197pub(super) fn lys_gate(config: &LysConfig) -> Result<Gate, String> {
204 if config.database.is_empty()
205 || !config
206 .database
207 .bytes()
208 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-')
209 {
210 return Err(format!(
211 "lys database id {:?} must be letters, digits, _ or - only",
212 config.database
213 ));
214 }
215 let mut live = LysLive::new(&config.issuer)
216 .map_err(|error| format!("{}: the lys issuer {}", error.name(), config.issuer))?;
217 if let Some(file) = config.placement_credential_file.as_deref() {
218 live = live.asking_as(super::super::lys_schema::read_credential(file)?);
219 }
220 let live = Arc::new(live);
221 let asking = Arc::clone(&live);
222 Gate::fetching(
223 move || asking.keys(),
224 &config.issuer,
225 &config.database,
226 live,
227 )
228 .map_err(|error| format!("the thread reading Lys's keys could not start: {error}"))
229}
230
231const fn kept_versions() -> usize {
232 KEPT_VERSIONS_DEFAULT
233}
234
235fn kept_versions_within<'de, D>(deserializer: D) -> Result<usize, D::Error>
237where
238 D: serde::Deserializer<'de>,
239{
240 let value = u64::deserialize(deserializer)?;
241 usize::try_from(value)
242 .ok()
243 .filter(|kept| (1..=KEPT_VERSIONS_MOST).contains(kept))
244 .ok_or_else(|| {
245 serde::de::Error::custom(format!(
246 "kept_versions must be a whole number from 1 to {KEPT_VERSIONS_MOST}, not {value}"
247 ))
248 })
249}
250
251const fn append_prune_keep_ceiling() -> u32 {
252 4_096
253}
254
255const fn append_prune_removal_cap() -> u32 {
256 64
257}
258
259fn changes_scan_records() -> NonZeroU64 {
260 changes::Settings::default().scan_records
261}
262
263fn changes_node_budget() -> NonZeroUsize {
264 changes::Settings::default().node_budget
265}
266
267fn changes_max_page() -> NonZeroU32 {
268 changes::Settings::default().max_page
269}
270
271impl Config {
272 pub fn endpoint(
277 &self,
278 create: bool,
279 ) -> Result<super::super::runtime::Endpoint, super::super::runtime::Refusal> {
280 #[cfg(test)]
281 if let Some((root, uid)) = endpoint_tests::input() {
282 return super::super::runtime::resolve_input(
283 &self.data_dir,
284 self.name.as_deref(),
285 self.socket.as_deref(),
286 create,
287 &root,
288 uid,
289 );
290 }
291 super::super::runtime::resolve(
292 &self.data_dir,
293 self.name.as_deref(),
294 self.socket.as_deref(),
295 create,
296 )
297 }
298
299 pub fn kept_limit(&self) -> Result<KeptLimit, String> {
304 KeptLimit::new(self.kept_versions).map_err(|error| error.to_string())
305 }
306
307 pub fn append_prune_limits(&self) -> Result<super::super::storage::AppendPruneLimits, String> {
310 super::super::storage::AppendPruneLimits::new(
311 self.append_prune_keep_ceiling,
312 self.append_prune_removal_cap,
313 )
314 .map_err(|failure| failure.to_string())
315 }
316
317 pub fn changes_settings(&self) -> Result<changes::Settings, String> {
322 if self.changes_scan_records.get() < 2 {
323 return Err("changes_scan_records must be at least 2".to_owned());
324 }
325 Ok(changes::Settings {
326 scan_records: self.changes_scan_records,
327 node_budget: self.changes_node_budget,
328 max_page: self.changes_max_page,
329 })
330 }
331
332 pub fn query_settings(&self) -> Result<Option<Settings>, String> {
337 Options {
338 query_time_ms: self.query_time_ms,
339 query_memory_bytes: self.query_memory_bytes,
340 query_outputs_per_record: self.query_outputs_per_record,
341 query_scan_records: self.query_scan_records,
342 query_max_running: self.query_max_running,
343 query_worker: self.query_worker.clone(),
344 }
345 .resolve()
346 }
347
348 pub fn embed_settings(&self) -> Result<Option<embed::Settings>, String> {
354 embed::Options {
355 worker: self.embed_worker.clone(),
356 model_dir: self.embed_model_dir.clone(),
357 model_sha256: self.embed_model_sha256.clone(),
358 memory_bytes: self.embed_memory_bytes,
359 threads: self.embed_threads,
360 batch_items: self.embed_batch_items,
361 input_bytes: self.embed_input_bytes,
362 tokens: self.embed_tokens,
363 frame_bytes: self.embed_frame_bytes,
364 ready_ms: self.embed_ready_ms,
365 call_ms: self.embed_call_ms,
366 waiting: self.embed_waiting,
367 request_items: self.embed_request_items,
368 page_records: self.embed_page_records,
369 writer_wait_ms: self.embed_writer_wait_ms,
370 windows: self.embed_windows,
371 max_windows: self.embed_max_windows,
372 overlap_tokens: self.embed_window_overlap_tokens,
373 }
374 .resolve()
375 }
376}
377
378#[derive(Debug, Deserialize)]
379#[serde(deny_unknown_fields)]
380pub struct Creation {
381 pub shards: NonZeroUsize,
382 pub cache_bytes_total: NonZeroUsize,
383 pub executor_threads: NonZeroUsize,
384}
385
386pub(super) const fn creation_config(data_dir: PathBuf, creation: &Creation) -> DatabaseConfig {
390 DatabaseConfig {
391 data_dir,
392 shard_count: creation.shards.get(),
393 distributed: None,
394 executor_threads: Some(creation.executor_threads.get()),
395 node_cache_budget: Some(NodeCacheBudget::Bytes(creation.cache_bytes_total)),
396 retention: haematite::Retention::reclaim(0),
397 }
398}