haematite_cli/serve/query/
start.rs1use std::collections::BTreeMap;
2use std::num::NonZeroU32;
3use std::time::{Duration, Instant};
4
5use haematite_query::handle::{Error, Handle, StartupStage};
6use haematite_query::{Limits, Ready, Refusal, Start};
7use serde_json::value::RawValue;
8
9use super::{Settings, probe};
10use crate::serve::storage::Failure;
11
12#[must_use]
13pub fn request(
14 settings: &Settings,
15 frame_bytes: NonZeroU32,
16 filter: String,
17 alternate: Option<String>,
18 parameters: BTreeMap<String, Box<RawValue>>,
19) -> Start {
20 Start {
21 protocol: haematite_query::PROTOCOL,
22 filter,
23 alternate,
24 limits: Limits {
25 time_ms: settings.time_ms,
26 memory_bytes: settings.memory_bytes.get(),
27 outputs_per_input: settings.outputs_per_record.get(),
28 frame_bytes,
29 },
30 parameters,
31 binding_names: haematite_query::page::BINDING_NAMES
32 .iter()
33 .map(|name| (*name).to_owned())
34 .collect(),
35 }
36}
37
38pub(super) fn deadline(settings: &Settings) -> Result<Instant, Failure> {
39 Instant::now()
40 .checked_add(Duration::from_millis(settings.time_ms.get()))
41 .ok_or_else(|| {
42 Failure::new(
43 "query_worker_start_timeout",
44 "query_time_ms cannot be represented by the monotonic clock",
45 )
46 })
47}
48
49pub(super) fn check(settings: &Settings, deadline: Instant) -> Result<(), Failure> {
50 if !settings.worker.is_absolute() {
51 return Err(Failure::new(
52 "query_worker_path_invalid",
53 "query_worker must be an absolute path",
54 ));
55 }
56 let metadata = std::fs::metadata(&settings.worker).map_err(|error| {
57 let code = if error.kind() == std::io::ErrorKind::NotFound {
58 "query_worker_missing"
59 } else {
60 "query_worker_path_unreadable"
61 };
62 Failure::new(
63 code,
64 format!("query_worker {}: {error}", settings.worker.display()),
65 )
66 })?;
67 if !metadata.is_file() {
68 return Err(Failure::new(
69 "query_worker_not_file",
70 format!(
71 "query_worker {} is not a regular file",
72 settings.worker.display()
73 ),
74 ));
75 }
76 if Instant::now() >= deadline {
77 return Err(Failure::new(
78 "query_worker_start_timeout",
79 format!(
80 "startup allowance query_time_ms={} ms expired before spawn",
81 settings.time_ms
82 ),
83 ));
84 }
85 Ok(())
86}
87
88fn boundary_error(failure: &Failure) -> Error {
89 let code = if failure.code == "query_worker_start_timeout" {
90 Refusal::QueryTimeLimit
91 } else {
92 Refusal::QueryEngineUnavailable
93 };
94 Error::new(code, failure.to_string()).at_start(StartupStage::Spawn, None)
95}
96
97pub(super) fn connect(
98 settings: &Settings,
99 request: &Start,
100 deadline: Instant,
101) -> Result<(Handle, Ready), Error> {
102 Handle::start(
103 &settings.worker,
104 request,
105 env!("CARGO_PKG_VERSION"),
106 deadline,
107 )
108}
109
110pub fn open(
115 settings: &Settings,
116 frame_bytes: NonZeroU32,
117 filter: String,
118 alternate: Option<String>,
119 parameters: BTreeMap<String, Box<RawValue>>,
120) -> Result<(Handle, Ready), Error> {
121 let deadline = deadline(settings).map_err(|failure| boundary_error(&failure))?;
122 check(settings, deadline).map_err(|failure| boundary_error(&failure))?;
123 let request = request(settings, frame_bytes, filter, alternate, parameters);
124 connect(settings, &request, deadline)
125}
126
127#[must_use]
129pub const fn code_name(code: Refusal) -> &'static str {
130 match code {
131 Refusal::QueryCompile => "query_compile",
132 Refusal::QueryPattern => "query_pattern",
133 Refusal::QueryFlag => "query_flag",
134 Refusal::QueryRuntime => "query_runtime",
135 Refusal::QueryOutputLimit => "query_output_limit",
136 Refusal::QueryValueTooLarge => "query_value_too_large",
137 Refusal::QueryFunctionRefused => "query_function_refused",
138 Refusal::QueryProtocol => "query_protocol",
139 Refusal::QueryEngineMismatch => "query_engine_mismatch",
140 Refusal::QueryEngineUnavailable => "query_engine_unavailable",
141 Refusal::QueryTimeLimit => "query_time_limit",
142 Refusal::QueryMemoryLimit => "query_memory_limit",
143 Refusal::QueryStopped => "query_stopped",
144 Refusal::QueryScanLimit => "query_scan_limit",
145 Refusal::QueryPercentileRange => "query_percentile_range",
146 Refusal::QueryMomentGone => "query_moment_gone",
147 Refusal::QueryReadFailed => "query_read_failed",
148 }
149}
150
151#[must_use]
153pub fn caller_failure(error: &Error) -> Failure {
154 let code = match error.refusal.code {
155 Refusal::QueryTimeLimit | Refusal::QueryMemoryLimit => error.refusal.code,
156 _ => match error.startup.map(|startup| startup.stage) {
157 Some(StartupStage::Failed(code)) => code,
158 Some(StartupStage::Identity) => Refusal::QueryEngineMismatch,
159 Some(StartupStage::UnofferedReading) => Refusal::QueryProtocol,
160 _ => Refusal::QueryEngineUnavailable,
161 },
162 };
163 let mut message = if code == Refusal::QueryEngineUnavailable {
164 format!("{}: {error}", probe::classified_name(error))
165 } else {
166 error.to_string()
167 };
168 if let Some(status) = &error.status {
169 message = format!("{message}; worker status: {status}");
170 }
171 if let Some(cause) = &error.stderr_error {
172 message = format!("{message}; stderr reader: {cause}");
173 }
174 Failure::new(code_name(code), message)
175}
176
177#[cfg(test)]
178#[path = "start_tests.rs"]
179mod tests;