use serde::{Deserialize, Serialize};
use crate::diag::{Diagnostic, SgCode};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Liveness {
pub pid: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Outcome {
Up(Liveness),
Completed,
Failed(Diagnostic),
}
impl Outcome {
pub fn succeeded(&self) -> bool {
!matches!(self, Outcome::Failed(_))
}
pub fn diagnostic(&self) -> Option<&Diagnostic> {
match self {
Outcome::Failed(diag) => Some(diag),
_ => None,
}
}
}
pub fn immediate_exit(service: &str, exit_code: Option<i32>) -> Diagnostic {
let detail = match exit_code {
Some(code) => format!("it exited with code {code} before it could come up"),
None => "it exited before it could come up".to_string(),
};
Diagnostic::error(
SgCode::UnitImmediateExit,
format!("service `{service}` exited immediately at start"),
)
.note(detail)
.help_cmd("view logs", format!("sysg logs -s {service}"))
.help_docs()
}
pub fn pre_start_failed(service: &str, exit_code: Option<i32>) -> Diagnostic {
let detail = match exit_code {
Some(code) => format!("the pre_start command exited with code {code}"),
None => "the pre_start command failed".to_string(),
};
Diagnostic::error(
SgCode::PreStartFailed,
format!("pre_start for `{service}` failed, so it was not started"),
)
.note(detail)
.help_cmd("view logs", format!("sysg logs -s {service}"))
.help_docs()
}
pub fn health_unmet(service: &str, attempts: u32) -> Diagnostic {
Diagnostic::error(
SgCode::HealthUnmet,
format!("service `{service}` never passed its health check"),
)
.note(format!(
"the process is running but {attempts} health checks did not pass"
))
.help_cmd("view logs", format!("sysg logs -s {service}"))
.help_docs()
}
pub fn dependency_unavailable(
service: &str,
dependency: &str,
reason: impl Into<String>,
) -> Diagnostic {
Diagnostic::error(
SgCode::DependencyUnavailable,
format!(
"service `{service}` could not start because `{dependency}` was unavailable"
),
)
.note(reason)
.help_cmd("inspect dependency", format!("sysg inspect {dependency}"))
.help_docs()
}
pub fn project_services_not_up(project: &str, services: &[String]) -> Diagnostic {
Diagnostic::error(
SgCode::ProjectServicesNotUp,
format!("project `{project}` did not bring every service up"),
)
.note(format!("did not start: {}", services.join(", ")))
.help_cmd("check status", format!("sysg status -p {project}"))
.help_docs()
}
pub fn unit_start_failed(service: &str, reason: impl Into<String>) -> Diagnostic {
Diagnostic::error(
SgCode::UnitStartFailed,
format!("service `{service}` failed to start"),
)
.note(reason)
.help_cmd("view logs", format!("sysg logs -s {service}"))
.help_docs()
}
pub fn ambiguous_service(service: &str, projects: &[String]) -> Diagnostic {
Diagnostic::error(
SgCode::TargetScopeAmbiguous,
format!("service `{service}` exists in more than one project"),
)
.note(format!("it is declared in: {}", projects.join(", ")))
.note("pass -p/--project to choose which one to target")
.help_docs()
}
pub fn project_mismatch(requested: &str, actual: &str) -> Diagnostic {
Diagnostic::error(
SgCode::TargetConfigMismatch,
format!("project `{requested}` does not match the resolved project `{actual}`"),
)
.note("the -p flag, any service selector prefix, and the config must agree")
.help_docs()
}
pub fn outcome_of(
service: &str,
result: Result<crate::daemon::ServiceReadyState, crate::error::ProcessManagerError>,
pid: Option<u32>,
) -> Outcome {
use crate::{daemon::ServiceReadyState, error::ProcessManagerError};
match result {
Ok(ServiceReadyState::Running) => match pid {
Some(pid) => Outcome::Up(Liveness { pid }),
None => Outcome::Failed(unit_start_failed(
service,
"the service reported running but no PID was recorded",
)),
},
Ok(ServiceReadyState::CompletedSuccess) => Outcome::Completed,
Err(ProcessManagerError::Diag(diag)) => Outcome::Failed(*diag),
Err(err) => Outcome::Failed(unit_start_failed(service, err.to_string())),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn up_and_completed_succeed_failed_does_not() {
assert!(Outcome::Up(Liveness { pid: 42 }).succeeded());
assert!(Outcome::Completed.succeeded());
assert!(!Outcome::Failed(immediate_exit("web", Some(1))).succeeded());
}
#[test]
fn failed_carries_its_diagnostic_code() {
let out = Outcome::Failed(pre_start_failed("api", Some(3)));
assert_eq!(out.diagnostic().unwrap().code, SgCode::PreStartFailed);
}
#[test]
fn outcome_of_maps_each_result_variant() {
use crate::{daemon::ServiceReadyState, error::ProcessManagerError};
assert_eq!(
outcome_of("web", Ok(ServiceReadyState::Running), Some(9)),
Outcome::Up(Liveness { pid: 9 })
);
assert_eq!(
outcome_of("job", Ok(ServiceReadyState::CompletedSuccess), None),
Outcome::Completed
);
let running_without_pid = outcome_of("web", Ok(ServiceReadyState::Running), None);
assert_eq!(
running_without_pid.diagnostic().unwrap().code,
SgCode::UnitStartFailed
);
let diag = immediate_exit("web", Some(1));
let out = outcome_of("web", Err(ProcessManagerError::Diag(Box::new(diag))), None);
assert_eq!(out.diagnostic().unwrap().code, SgCode::UnitImmediateExit);
let generic = outcome_of(
"web",
Err(ProcessManagerError::SpawnLimitExceeded("x".into())),
None,
);
assert_eq!(generic.diagnostic().unwrap().code, SgCode::UnitStartFailed);
}
#[test]
fn outcome_round_trips_over_serde() {
let out = Outcome::Up(Liveness { pid: 7 });
let json = serde_json::to_string(&out).unwrap();
assert_eq!(serde_json::from_str::<Outcome>(&json).unwrap(), out);
let failed = Outcome::Failed(health_unmet("db", 5));
let json = serde_json::to_string(&failed).unwrap();
assert_eq!(serde_json::from_str::<Outcome>(&json).unwrap(), failed);
}
}