use std::collections::BTreeMap;
use schemars::JsonSchema;
use serde::Serialize;
#[derive(Debug, Clone)]
pub struct Freshness {
pub project_root: String,
pub project_hash: String,
pub imports: Vec<String>,
pub session_id: String,
pub lean_toolchain: String,
pub(crate) toolchain_advisories: Vec<String>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct FreshnessIdentity {
pub project_root: String,
pub session_id: String,
pub lean_toolchain: String,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct Telemetry {
pub project_hash: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub imports: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub runtime: Option<RuntimeFacts>,
}
#[derive(Debug, Clone, Default, Serialize, JsonSchema)]
pub struct RuntimeRestartEvent {
pub cause: String,
pub reason: String,
pub worker_generation: u64,
pub planned: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub rss_kib: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit_kib: Option<u64>,
}
#[derive(Debug, Clone, Default, Serialize, JsonSchema)]
pub struct RuntimeFacts {
pub worker_generation: u64,
pub worker_restarted: bool,
pub retry_count: u32,
pub admission_wait_millis: u64,
pub queue_wait_millis: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub call_restart: Option<RuntimeRestartEvent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_restart: Option<RuntimeRestartEvent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rss_kib: Option<u64>,
pub worker_lanes: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub import_profile: Option<String>,
pub profile_switch_count: u64,
pub restarts_total: u64,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub restarts_by_cause: BTreeMap<String, u64>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ResponseStatus {
Ok,
RuntimeUnavailable,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct RuntimeFailure {
pub reason: String,
pub retryable: bool,
pub project_root: String,
pub session_id: String,
pub worker_generation: u64,
pub worker_restarted: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub restart_cause: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rss_kib: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit_kib: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub retry_after_millis: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub restarts_in_window: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub window_millis: Option<u64>,
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct Response<T>
where
T: Serialize + JsonSchema,
{
pub status: ResponseStatus,
pub result: Option<T>,
pub runtime_error: Option<RuntimeFailure>,
pub freshness: FreshnessIdentity,
#[serde(skip_serializing_if = "Option::is_none")]
pub telemetry: Option<Telemetry>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub warnings: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub next_actions: Vec<String>,
#[serde(skip)]
#[schemars(skip)]
pub(crate) advisories: Vec<String>,
}
fn split_freshness(freshness: Freshness, runtime: Option<RuntimeFacts>) -> (FreshnessIdentity, Telemetry, Vec<String>) {
let Freshness {
project_root,
project_hash,
imports,
session_id,
lean_toolchain,
toolchain_advisories,
} = freshness;
(
FreshnessIdentity {
project_root,
session_id,
lean_toolchain,
},
Telemetry {
project_hash,
imports,
runtime,
},
toolchain_advisories,
)
}
impl<T> Response<T>
where
T: Serialize + JsonSchema,
{
pub fn ok(result: T, freshness: Freshness) -> Self {
let (freshness, telemetry, advisories) = split_freshness(freshness, None);
Self {
status: ResponseStatus::Ok,
result: Some(result),
runtime_error: None,
freshness,
telemetry: Some(telemetry),
warnings: Vec::new(),
next_actions: Vec::new(),
advisories,
}
}
pub fn runtime_unavailable(failure: RuntimeFailure, freshness: Freshness, runtime: RuntimeFacts) -> Self {
let (freshness, telemetry, advisories) = split_freshness(freshness, Some(runtime));
Self {
status: ResponseStatus::RuntimeUnavailable,
result: None,
runtime_error: Some(failure),
freshness,
telemetry: Some(telemetry),
warnings: Vec::new(),
next_actions: Vec::new(),
advisories,
}
}
pub fn result_ref(&self) -> Option<&T> {
self.result.as_ref()
}
pub fn runtime(&self) -> Option<&RuntimeFacts> {
self.telemetry.as_ref().and_then(|telemetry| telemetry.runtime.as_ref())
}
pub fn imports(&self) -> &[String] {
match &self.telemetry {
Some(telemetry) => &telemetry.imports,
None => &[],
}
}
#[must_use]
pub fn with_runtime(mut self, runtime: RuntimeFacts) -> Self {
if let Some(telemetry) = self.telemetry.as_mut() {
telemetry.runtime = Some(runtime);
}
self
}
pub fn drop_telemetry(&mut self) {
self.telemetry = None;
}
pub(crate) fn drain_advisories(&mut self) {
let advisories = std::mem::take(&mut self.advisories);
self.warnings.extend(advisories);
}
#[must_use]
pub fn warn(mut self, msg: impl Into<String>) -> Self {
self.warnings.push(msg.into());
self
}
#[must_use]
pub fn hint(mut self, msg: impl Into<String>) -> Self {
self.next_actions.push(msg.into());
self
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn runtime_facts_separate_call_restart_from_lifecycle_history() {
let facts = RuntimeFacts {
worker_generation: 4,
worker_restarted: false,
retry_count: 0,
admission_wait_millis: 0,
queue_wait_millis: 0,
call_restart: None,
last_restart: Some(RuntimeRestartEvent {
cause: "rss_post_job".to_owned(),
reason: "rss_post_job current_kib=5 limit_kib=4".to_owned(),
worker_generation: 3,
planned: true,
rss_kib: Some(5),
limit_kib: Some(4),
}),
rss_kib: Some(2),
worker_lanes: 1,
import_profile: Some("Init".to_owned()),
profile_switch_count: 1,
restarts_total: 1,
restarts_by_cause: BTreeMap::from([("rss_post_job".to_owned(), 1)]),
};
let json = serde_json::to_value(facts).unwrap();
assert!(json.pointer("/call_restart").is_none_or(serde_json::Value::is_null));
assert_eq!(
json.pointer("/last_restart/cause").and_then(serde_json::Value::as_str),
Some("rss_post_job")
);
assert_eq!(
json.pointer("/worker_restarted").and_then(serde_json::Value::as_bool),
Some(false)
);
}
}