use std::sync::Mutex;
use std::time::{Duration, Instant};
use serde::Serialize;
use crate::client::GatewayApi;
use crate::error::CoreError;
use crate::poll::{self, PollConfig, PollState};
pub const RESTART_FLOOR: Duration = Duration::from_secs(5);
pub const DEFAULT_INTERVAL: Duration = Duration::from_secs(2);
pub const RESTART_TIMEOUT: Duration = Duration::from_secs(300);
pub const READINESS_TIMEOUT: Duration = Duration::from_secs(120);
const RUNNING: &str = "RUNNING";
const ACTIVE: &str = "ACTIVE";
#[derive(Debug, Serialize)]
pub struct RestartResult {
pub restarted: bool,
}
#[derive(Debug, Serialize)]
pub struct RestartWaitResult {
pub restarted: bool,
pub state: String,
pub elapsed_secs: u64,
}
#[derive(Debug, Serialize)]
pub struct WaitResult {
pub target: String,
pub state: String,
pub elapsed_secs: u64,
}
pub async fn restart(api: &dyn GatewayApi) -> Result<RestartResult, CoreError> {
api.restart().await?;
Ok(RestartResult { restarted: true })
}
pub async fn restart_and_wait(
api: &dyn GatewayApi,
interval: Duration,
timeout: Duration,
floor: Duration,
) -> Result<RestartWaitResult, CoreError> {
api.restart().await?;
let started = Instant::now();
tokio::time::sleep(floor).await;
let state = wait_state_running(
api,
"restart completion (GET /StatusPing)".to_string(),
interval,
timeout,
)
.await?;
Ok(RestartWaitResult {
restarted: true,
state,
elapsed_secs: started.elapsed().as_secs(),
})
}
pub async fn wait_gateway(
api: &dyn GatewayApi,
interval: Duration,
timeout: Duration,
) -> Result<WaitResult, CoreError> {
let started = Instant::now();
let state = wait_state_running(
api,
"gateway readiness (GET /StatusPing)".to_string(),
interval,
timeout,
)
.await?;
Ok(WaitResult {
target: "gateway".to_string(),
state,
elapsed_secs: started.elapsed().as_secs(),
})
}
pub async fn wait_restart(
api: &dyn GatewayApi,
interval: Duration,
timeout: Duration,
floor: Duration,
) -> Result<WaitResult, CoreError> {
let started = Instant::now();
let cfg = PollConfig {
subject: "restart completion (GET /StatusPing)".to_string(),
interval,
deadline: timeout,
..PollConfig::default()
};
struct Witness<'a> {
seen_non_running: bool,
final_state: &'a mut Mutex<String>,
}
let mut final_state = Mutex::new(String::new());
poll::poll(
cfg,
Witness {
seen_non_running: false,
final_state: &mut final_state,
},
|witness| {
Box::pin(async {
let ping = api.status_ping().await?;
if ping.state == RUNNING {
if witness.seen_non_running || started.elapsed() >= floor {
witness
.final_state
.get_mut()
.expect("terminal state")
.clone_from(&ping.state);
Ok(PollState::<()>::Done(()))
} else {
Ok(PollState::<()>::Pending(Some(format!(
"{RUNNING} (all-RUNNING inside the {floor:?} restart grace floor)"
))))
}
} else {
witness.seen_non_running = true;
Ok(PollState::<()>::Pending(Some(ping.state)))
}
})
},
)
.await?;
Ok(WaitResult {
target: "restart".to_string(),
state: std::mem::take(&mut *final_state.lock().expect("terminal state")),
elapsed_secs: started.elapsed().as_secs(),
})
}
pub async fn wait_module(
api: &dyn GatewayApi,
module_id: &str,
interval: Duration,
timeout: Duration,
) -> Result<WaitResult, CoreError> {
let started = Instant::now();
let cfg = PollConfig {
subject: format!("module {module_id} ACTIVE (GET /data/api/v1/modules/healthy)"),
interval,
deadline: timeout,
..PollConfig::default()
};
let mut final_state = Mutex::new(String::new());
poll::poll(cfg, &mut final_state, |final_state| {
Box::pin(async {
let query = crate::client::query::ListQuery {
search: Some(module_id.to_string()),
..Default::default()
};
let modules = api.modules(false, &query).await?;
if let Some(module) = modules.items.iter().find(|m| m.id == module_id) {
if module.state.as_deref() == Some(ACTIVE) {
final_state
.get_mut()
.expect("terminal state")
.push_str(ACTIVE);
Ok(PollState::<()>::Done(()))
} else {
Ok(PollState::<()>::Pending(Some(format!(
"{} state {}",
module.id,
module.state.as_deref().unwrap_or("-")
))))
}
} else {
Ok(PollState::<()>::Pending(Some(format!(
"{module_id} not present in the healthy module list"
))))
}
})
})
.await?;
Ok(WaitResult {
target: format!("module {module_id}"),
state: std::mem::take(&mut *final_state.lock().expect("terminal state")),
elapsed_secs: started.elapsed().as_secs(),
})
}
async fn wait_state_running(
api: &dyn GatewayApi,
subject: String,
interval: Duration,
timeout: Duration,
) -> Result<String, CoreError> {
let cfg = PollConfig {
subject,
interval,
deadline: timeout,
..PollConfig::default()
};
let mut final_state = Mutex::new(String::new());
poll::poll(cfg, &mut final_state, |final_state| {
Box::pin(async {
let ping = api.status_ping().await?;
if ping.state == RUNNING {
final_state
.get_mut()
.expect("terminal state")
.clone_from(&ping.state);
Ok(PollState::<()>::Done(()))
} else {
Ok(PollState::<()>::Pending(Some(ping.state)))
}
})
})
.await?;
Ok(std::mem::take(
&mut *final_state.lock().expect("terminal state"),
))
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::RESTART_FLOOR;
#[test]
fn restart_floor_is_five_seconds() {
assert_eq!(RESTART_FLOOR, Duration::from_secs(5));
}
}