use std::collections::BTreeMap;
use std::fmt;
use std::num::NonZeroU32;
use haematite_query::handle::{Error, StartupStage};
use haematite_query::{Ready, Refusal};
use super::{Settings, start};
use crate::serve::storage::Failure;
#[derive(Debug)]
pub struct Prepared {
settings: Settings,
outcome: Result<Ready, Failure>,
}
impl Prepared {
#[must_use]
pub const fn settings(&self) -> &Settings {
&self.settings
}
pub const fn ready(&self) -> Result<&Ready, &Failure> {
self.outcome.as_ref()
}
}
#[derive(Debug)]
pub struct ProbeError {
pub failure: Failure,
pub ending: Option<Box<Error>>,
}
impl fmt::Display for ProbeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.failure, formatter)
}
}
impl std::error::Error for ProbeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.ending
.as_deref()
.map(|error| error as &dyn std::error::Error)
}
}
pub(super) fn classified_name(error: &Error) -> &'static str {
match error.refusal.code {
Refusal::QueryTimeLimit => return "query_worker_start_timeout",
Refusal::QueryMemoryLimit => return "query_worker_start_memory_limit",
_ => {}
}
let Some(startup) = error.startup else {
return "query_worker_provenance_missing";
};
match startup.stage {
StartupStage::Spawn => "query_worker_not_executable",
StartupStage::Identity => "query_worker_identity_mismatch",
StartupStage::Stopping => "query_worker_stop_failed",
StartupStage::UnexpectedFrame
| StartupStage::Failed(_)
| StartupStage::UnofferedReading => "query_worker_not_ready",
StartupStage::Owner => "query_worker_owner_failed",
StartupStage::SendStart | StartupStage::ReadReady => {
if error.refusal.code == Refusal::QueryProtocol {
"query_worker_not_ready"
} else if error.status.is_some() {
"query_worker_exited_before_ready"
} else {
"query_worker_owner_failed"
}
}
}
}
fn classify(error: Error, settings: &Settings) -> ProbeError {
let code = classified_name(&error);
let mut message = format!("worker {}: {error}", settings.worker.display());
if let Some(startup) = error.startup
&& let StartupStage::Failed(code) = startup.stage
{
match serde_json::to_string(&code) {
Ok(code) => message = format!("{message}; Failed code {code}"),
Err(cause) => {
return ProbeError {
failure: Failure::new(
"query_worker_diagnostic_failed",
format!("cannot encode worker Failed code: {cause}; {message}"),
),
ending: Some(Box::new(error)),
};
}
}
}
if error.refusal.code == Refusal::QueryTimeLimit {
message = format!(
"{message}; startup allowance is query_time_ms={} ms",
settings.time_ms
);
}
if let Some(cause) = &error.stderr_error {
message = format!("{message}; stderr reader: {cause}");
}
ProbeError {
failure: Failure::new(code, message),
ending: Some(Box::new(error)),
}
}
pub fn run(settings: &Settings, frame_bytes: NonZeroU32) -> Result<Ready, ProbeError> {
let boundary = |failure| ProbeError {
failure,
ending: None,
};
let deadline = start::deadline(settings).map_err(boundary)?;
start::check(settings, deadline).map_err(boundary)?;
let request = start::request(settings, frame_bytes, ".".to_owned(), None, BTreeMap::new());
let (mut worker, ready) =
start::connect(settings, &request, deadline).map_err(|error| classify(error, settings))?;
let pid = worker.pid();
worker
.stop()
.map_err(|error| classify(error.at_start(StartupStage::Stopping, Some(pid)), settings))?;
Ok(ready)
}
#[must_use]
pub fn prepare(settings: Settings, frame_bytes: NonZeroU32) -> Prepared {
let outcome = run(&settings, frame_bytes).map_err(|error| error.failure);
Prepared { settings, outcome }
}
#[cfg(all(test, unix))]
#[path = "probe_tests.rs"]
mod tests;