1use std::path::PathBuf;
4use std::sync::Arc;
5use std::time::Duration;
6
7use haematite::store::NodeCacheBudget;
8
9use super::{
10 embed,
11 query::{Setting, probe},
12 service::Service,
13 storage::{Storage, UndatedGeneration},
14 transport::{Limits, Listener},
15};
16
17pub fn name_is_live(default_endpoint: &std::path::Path, expected_uid: u32) -> Result<bool, String> {
27 super::socket_owner::name_is_live(default_endpoint, expected_uid).map_err(|error| {
28 format!(
29 "name_owner_probe_failed: {}: {error}",
30 default_endpoint.display()
31 )
32 })
33}
34
35const USAGE: &str = "usage: haematite serve --config <file.json> [--create] [--long-wait-interval <duration>] [--exit-on-stdin-close]\n\
36--create requires a new database directory; omit it when restarting.\n\
37An explicit socket's parent directory must already exist with mode 0700.\n\
38An omitted socket uses the private OS runtime namespace. No auto-launch is installed.";
39
40const DEFAULT_LONG_WAIT_INTERVAL: Duration = Duration::from_secs(30);
41
42struct ServeOptions {
43 path: PathBuf,
44 create: bool,
45 exit_on_stdin_close: bool,
46 long_wait_interval: Duration,
47}
48
49fn long_wait_help() -> String {
50 format!(
51 "--long-wait-interval <duration> is the interval between long-wait warnings, not a deadline. \
52 default: {}s; use an integer followed by ns, us, ms, s, m or h.",
53 DEFAULT_LONG_WAIT_INTERVAL.as_secs()
54 )
55}
56
57#[path = "cli_config.rs"]
58mod config;
59#[cfg(test)]
60use config::lys_gate;
61pub use config::{Config, Creation, LysConfig};
62use config::{creation_config, lys_parts, served_through_lys};
63
64fn claim_name(
65 config: &Config,
66 endpoint: &super::runtime::Endpoint,
67 create: bool,
68) -> Result<std::fs::File, String> {
69 let name_endpoint = if config.socket.is_some() {
70 #[cfg(test)]
71 let resolved = match endpoint_tests::input() {
72 Some((root, uid)) => super::runtime::resolve_input(
73 &config.data_dir,
74 Some(&endpoint.name),
75 None,
76 create,
77 &root,
78 uid,
79 ),
80 None => super::runtime::resolve(&config.data_dir, Some(&endpoint.name), None, create),
81 };
82 #[cfg(not(test))]
83 let resolved =
84 super::runtime::resolve(&config.data_dir, Some(&endpoint.name), None, create);
85 Some(resolved.map_err(|error| format!("bind service socket: {error}"))?)
86 } else {
87 None
88 };
89 let name_socket = name_endpoint
90 .as_ref()
91 .map_or(&endpoint.socket, |named| &named.socket);
92 let namespace = name_socket
93 .parent()
94 .ok_or("bind service socket: name endpoint needs a parent")?;
95 let name_uid = {
96 use std::os::unix::fs::MetadataExt;
97 std::fs::symlink_metadata(namespace)
98 .map_err(|error| format!("bind service socket: {error}"))?
99 .uid()
100 };
101 super::socket_owner::acquire_name(name_socket, name_uid).map_err(|error| {
102 if error.kind() == std::io::ErrorKind::WouldBlock {
105 format!(
106 "bind service socket: the store name \"{}\" is already served by another running haematite serve ({}); give this data folder another name",
107 endpoint.name,
108 name_socket.display()
109 )
110 } else {
111 format!("bind service socket: {error}")
112 }
113 })
114}
115
116fn check_paths_and_timeout(config: &Config) -> Result<Duration, String> {
120 if !config.data_dir.is_absolute()
121 || config
122 .socket
123 .as_ref()
124 .is_some_and(|socket| !socket.is_absolute())
125 {
126 return Err("data_dir and socket must be absolute paths".to_owned());
127 }
128 let timeout = Duration::from_millis(config.timeout_ms.get());
129 if std::time::Instant::now().checked_add(timeout).is_none() {
130 return Err("timeout cannot be represented by the monotonic clock".to_owned());
131 }
132 if let Some(socket) = config.socket.as_ref() {
133 super::socket_owner::check_parent(socket)
134 .map_err(|error| format!("bind service socket: {error}"))?;
135 }
136 Ok(timeout)
137}
138
139pub fn run(args: impl Iterator<Item = String>) -> Result<(), String> {
143 let Some(options) = parse(args)? else {
144 println!("{USAGE}\n{}", long_wait_help());
145 return Ok(());
146 };
147 let config = read_config(&options.path)?;
148 let retention_limits = config.append_prune_limits()?;
150 let query_settings = config.query_settings()?;
151 let changes_settings = config.changes_settings()?;
152 let release = config.retention.settings()?;
153 let embedding = config.embed_settings()?;
155 let (gate, placer) = lys_parts(config.lys.as_ref())?;
156 let restart = embed::settings::Restart::of(
157 config.embed_restart_ms,
158 config.embed_restart_most_ms,
159 embedding
160 .as_ref()
161 .map_or(Duration::ZERO, |settings| settings.ready),
162 )?;
163 let timeout = check_paths_and_timeout(&config)?;
164 let endpoint = config
165 .endpoint(options.create)
166 .map_err(|error| error.to_string())?;
167 let name_ownership = claim_name(&config, &endpoint, options.create)?;
168 let query = query_settings.map(|settings| probe::prepare(settings, config.max_frame));
171 if let Some(Err(refusal)) = query.as_ref().map(probe::Prepared::ready) {
172 eprintln!("haem/1 query unavailable: {refusal}");
173 }
174 if let Err(error @ haematite::DatabaseError::LeafLogStore { .. }) =
177 haematite::Database::check_ordinary_store(&config.data_dir)
178 {
179 return Err(error.to_string());
180 }
181 let branch_cache = NodeCacheBudget::Bytes(config.branch_cache_bytes_per_shard);
182 let kept = config.kept_limit()?;
183 let storage = if options.create {
184 let creation = config
185 .create
186 .ok_or("--create needs a create section in the configuration")?;
187 Storage::create_kept(
188 creation_config(config.data_dir, &creation),
189 branch_cache,
190 kept,
191 options.long_wait_interval,
192 )
193 } else {
194 Storage::open_kept(
195 &config.data_dir,
196 branch_cache,
197 kept,
198 options.long_wait_interval,
199 )
200 }
201 .map_err(|error| error.to_string())?;
202 if options.create {
203 endpoint
204 .check_created()
205 .map_err(|error| error.to_string())?;
206 }
207 let mut service =
208 Service::with_query(storage, config.max_page, query).map_err(|error| error.to_string())?;
209 service.changes = changes_settings;
210 service.append_prune_limits = retention_limits;
211 service = served_through_lys(service, gate, placer)?;
212 service
213 .start_release(release)
214 .map_err(|error| error.to_string())?;
215 if let Some(settings) = embedding {
218 let bounds = super::embedding::bounds(&settings);
219 let writer_wait = settings.writer_wait;
220 let apart = !matches!(config.embed_question_worker, Setting::Present(false));
221 let questions = apart.then(|| super::embedding::embedder(settings.clone()));
222 service
223 .start_embedding_apart(
224 super::embedding::embedder(settings),
225 questions,
226 bounds,
227 writer_wait,
228 restart,
229 )
230 .map_err(|error| error.to_string())?;
231 }
232 let service = Arc::new(service);
233 service
234 .activate_reclaim()
235 .map_err(|error| format!("reclaim activation: {error}"))?;
236 let served = serve_listener(
237 &endpoint,
238 &service,
239 Limits {
240 max_frame: config.max_frame,
241 max_clients: config.max_clients,
242 frame_timeout: timeout,
243 },
244 options.exit_on_stdin_close,
245 );
246 drop(name_ownership);
247 served
248}
249
250fn serve_listener(
251 endpoint: &super::runtime::Endpoint,
252 service: &Arc<Service>,
253 limits: Limits,
254 exit_on_stdin_close: bool,
255) -> Result<(), String> {
256 let mut listener = Listener::bind(&endpoint.socket)
257 .map_err(|error| format!("bind service socket: {error}"))?;
258 listener
259 .publish(endpoint)
260 .map_err(|error| error.to_string())?;
261 let watch = exit_on_stdin_close
262 .then(super::stdin_close::Watch::start)
263 .transpose()
264 .map_err(|error| format!("stdin_watch_failed: {error}"))?;
265 eprintln!("haem/1 ready on {}", endpoint.socket.display());
266 let served = watch
267 .map_or_else(
268 || listener.run(service, limits),
269 |watch| listener.run_until(service, limits, watch),
270 )
271 .map_err(|error| format!("serve: {error}"));
272 let cleanup = listener.finish().map_err(|error| error.to_string());
273 drop(listener);
275 match (served, cleanup) {
276 (Ok(()), Ok(())) => Ok(()),
277 (Err(error), Ok(())) | (Ok(()), Err(error)) => Err(error),
278 (Err(served), Err(cleanup)) => Err(format!("{served}; {cleanup}")),
279 }
280}
281
282const RELEASE_USAGE: &str = "usage: haematite release date --config <file.json> --branch <name> \
283--generation <created> --at <nanoseconds> [--long-wait-interval <duration>]\n\
284Dates a generation discarded before its marker was written, as discarded at --at, so release \
285takes its rows by keep.days from then. --generation is the record's creation time its rows are \
286keyed by. Offline: the service must be stopped, as the store has one writer.";
287
288struct DateOptions {
289 branch: String,
290 generation: u64,
291 at: u64,
292 long_wait_interval: Duration,
293}
294
295#[path = "cli/branch_rename.rs"]
296mod branch_rename;
297pub use branch_rename::{BRANCH_RENAME_USAGE, branch_rename};
298
299pub fn release(mut args: impl Iterator<Item = String>) -> Result<(), String> {
310 if args.next().as_deref() != Some("date") {
311 return Err(RELEASE_USAGE.to_owned());
312 }
313 let mut args = args.peekable();
314 if args.peek().is_some_and(|arg| arg == "--help") {
315 if args.count() != 1 {
316 return Err(RELEASE_USAGE.to_owned());
317 }
318 println!("{RELEASE_USAGE}\n{}", long_wait_help());
319 return Ok(());
320 }
321 let (path, options) = parse_date(args)?;
322 let config = read_config(&path)?;
323 let branch_cache = NodeCacheBudget::Bytes(config.branch_cache_bytes_per_shard);
324 let mut storage = Storage::open_kept(
325 &config.data_dir,
326 branch_cache,
327 config.kept_limit()?,
328 options.long_wait_interval,
329 )
330 .map_err(|error| error.to_string())?;
331 let request = UndatedGeneration {
332 branch: options.branch,
333 generation: options.generation,
334 at: options.at,
335 };
336 let last = storage
337 .date_undated_generation(&request)
338 .map_err(|failure| failure.to_string())?;
339 println!("{}.{}", last.generation, last.sequence);
340 Ok(())
341}
342
343fn parse_date(mut args: impl Iterator<Item = String>) -> Result<(PathBuf, DateOptions), String> {
346 let (mut path, mut branch, mut generation, mut at) = (None, None, None, None);
347 let mut long_wait_interval = None;
348 while let Some(arg) = args.next() {
349 let value = args.next().ok_or(RELEASE_USAGE)?;
350 match arg.as_str() {
351 "--config" if path.is_none() => path = Some(PathBuf::from(value)),
352 "--branch" if branch.is_none() => branch = Some(value),
353 "--generation" if generation.is_none() => generation = Some(decimal(&arg, &value)?),
354 "--at" if at.is_none() => at = Some(decimal(&arg, &value)?),
355 "--long-wait-interval" if long_wait_interval.is_none() => {
356 long_wait_interval = Some(parse_long_wait_interval(&value)?);
357 }
358 _ => {
359 return Err(format!(
360 "unknown or repeated argument {arg:?}\n{RELEASE_USAGE}"
361 ));
362 }
363 }
364 }
365 let request = DateOptions {
366 long_wait_interval: long_wait_interval.unwrap_or(DEFAULT_LONG_WAIT_INTERVAL),
367 branch: branch.ok_or(RELEASE_USAGE)?,
368 generation: generation.ok_or(RELEASE_USAGE)?,
369 at: at.ok_or(RELEASE_USAGE)?,
370 };
371 Ok((path.ok_or(RELEASE_USAGE)?, request))
372}
373
374fn decimal(arg: &str, value: &str) -> Result<u64, String> {
375 value
376 .parse()
377 .map_err(|error| format!("{arg} {value:?}: {error}\n{RELEASE_USAGE}"))
378}
379
380fn read_config(path: &std::path::Path) -> Result<Config, String> {
381 let bytes = std::fs::read(path).map_err(|error| format!("read service config: {error}"))?;
382 serde_json::from_slice(&bytes).map_err(|error| format!("service config: {error}"))
383}
384
385fn parse_long_wait_interval(value: &str) -> Result<Duration, String> {
386 let invalid = || {
387 format!(
388 "invalid --long-wait-interval {value:?}: {}",
389 long_wait_help()
390 )
391 };
392 let end = value
393 .find(|character: char| !character.is_ascii_digit())
394 .ok_or_else(invalid)?;
395 let (number, unit) = value.split_at(end);
396 let amount: u64 = number.parse().map_err(|_| invalid())?;
397 let interval = match unit {
398 "ns" => Duration::from_nanos(amount),
399 "us" => Duration::from_micros(amount),
400 "ms" => Duration::from_millis(amount),
401 "s" => Duration::from_secs(amount),
402 "m" => Duration::from_secs(amount.checked_mul(60).ok_or_else(invalid)?),
403 "h" => Duration::from_secs(amount.checked_mul(3_600).ok_or_else(invalid)?),
404 _ => return Err(invalid()),
405 };
406 super::storage::runtime_options(interval).map_err(|error| error.to_string())?;
407 Ok(interval)
408}
409
410fn parse(args: impl Iterator<Item = String>) -> Result<Option<ServeOptions>, String> {
411 let mut args = args.peekable();
412 if args.peek().is_some_and(|arg| arg == "--help") {
413 return if args.count() == 1 {
414 Ok(None)
415 } else {
416 Err(USAGE.to_owned())
417 };
418 }
419 let mut path = None;
420 let mut create = false;
421 let mut exit_on_stdin_close = false;
422 let mut long_wait_interval = None;
423 while let Some(arg) = args.next() {
424 match arg.as_str() {
425 "--config" if path.is_none() => path = Some(PathBuf::from(args.next().ok_or(USAGE)?)),
426 "--create" if !create => create = true,
427 "--exit-on-stdin-close" if !exit_on_stdin_close => exit_on_stdin_close = true,
428 "--long-wait-interval" if long_wait_interval.is_none() => {
429 long_wait_interval = Some(parse_long_wait_interval(&args.next().ok_or(USAGE)?)?);
430 }
431 _ => return Err(format!("unknown or repeated argument {arg:?}\n{USAGE}")),
432 }
433 }
434 Ok(Some(ServeOptions {
435 path: path.ok_or(USAGE)?,
436 create,
437 exit_on_stdin_close,
438 long_wait_interval: long_wait_interval.unwrap_or(DEFAULT_LONG_WAIT_INTERVAL),
439 }))
440}
441
442#[cfg(test)]
443#[path = "cli_tests.rs"]
444mod tests;
445
446#[cfg(test)]
447#[path = "cli_lys_tests.rs"]
448mod lys_tests;
449
450#[cfg(test)]
451#[path = "cli_long_wait_tests.rs"]
452mod long_wait_tests;
453
454#[cfg(test)]
455#[path = "cli_endpoint_tests.rs"]
456pub(crate) mod endpoint_tests;