use super::*;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ClusterLifecycleStatus {
#[default]
Idle,
Running,
Stopping,
Stopped,
Failed,
}
impl ClusterLifecycleStatus {
pub fn is_running(self) -> bool {
self == Self::Running
}
pub fn is_stopping(self) -> bool {
self == Self::Stopping
}
pub fn is_stopped(self) -> bool {
self == Self::Stopped
}
pub fn has_failed(self) -> bool {
self == Self::Failed
}
pub fn is_terminal(self) -> bool {
matches!(self, Self::Stopped | Self::Failed)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClusterLifecycleDiagnostics {
pub component: String,
pub status: ClusterLifecycleStatus,
pub start_count: u64,
pub stop_count: u64,
pub shutdown_requested: bool,
pub last_error: Option<String>,
}
impl ClusterLifecycleDiagnostics {
pub fn idle(component: impl Into<String>) -> Self {
Self {
component: component.into(),
status: ClusterLifecycleStatus::Idle,
start_count: 0,
stop_count: 0,
shutdown_requested: false,
last_error: None,
}
}
pub fn running(component: impl Into<String>) -> Self {
let mut lifecycle = Self::idle(component);
lifecycle.record_start();
lifecycle
}
pub fn record_start(&mut self) {
self.status = ClusterLifecycleStatus::Running;
self.start_count = self.start_count.saturating_add(1);
self.shutdown_requested = false;
self.last_error = None;
}
pub fn record_shutdown_requested(&mut self) {
self.shutdown_requested = true;
if !self.status.is_terminal() {
self.status = ClusterLifecycleStatus::Stopping;
}
}
pub fn record_graceful_stop(&mut self) {
self.status = ClusterLifecycleStatus::Stopped;
self.stop_count = self.stop_count.saturating_add(1);
self.shutdown_requested = true;
}
pub fn record_failure(&mut self, error: impl Into<String>) {
self.status = ClusterLifecycleStatus::Failed;
self.last_error = Some(error.into());
}
pub fn is_running(&self) -> bool {
self.status.is_running()
}
pub fn is_stopping(&self) -> bool {
self.status.is_stopping()
}
pub fn is_stopped(&self) -> bool {
self.status.is_stopped()
}
pub fn has_failed(&self) -> bool {
self.status.has_failed()
}
pub fn is_terminal(&self) -> bool {
self.status.is_terminal()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClusterComponentError {
component: &'static str,
message: String,
}
impl ClusterComponentError {
pub fn new(component: &'static str, message: impl Into<String>) -> Self {
Self {
component,
message: message.into(),
}
}
pub fn component(&self) -> &'static str {
self.component
}
pub fn message(&self) -> &str {
&self.message
}
}
impl fmt::Display for ClusterComponentError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"cluster component '{}' failed: {}",
self.component, self.message
)
}
}
impl std::error::Error for ClusterComponentError {}
#[async_trait::async_trait]
pub trait ClusterComponent: Send + Sync {
fn name(&self) -> &'static str;
async fn start(&self) -> std::result::Result<(), ClusterComponentError>;
async fn stop(&self) -> std::result::Result<(), ClusterComponentError>;
fn diagnostics(&self) -> ClusterLifecycleDiagnostics;
fn last_error(&self) -> Option<String>;
}
#[derive(Debug, Clone)]
pub struct ClusterLifecycleComponent {
name: &'static str,
state: Arc<Mutex<ClusterLifecycleDiagnostics>>,
}
impl ClusterLifecycleComponent {
pub fn new(name: &'static str) -> Self {
Self {
name,
state: Arc::new(Mutex::new(ClusterLifecycleDiagnostics::idle(name))),
}
}
pub fn fail(&self, message: impl Into<String>) {
self.state
.lock()
.expect("cluster component lifecycle poisoned")
.record_failure(message);
}
}
#[async_trait::async_trait]
impl ClusterComponent for ClusterLifecycleComponent {
fn name(&self) -> &'static str {
self.name
}
async fn start(&self) -> std::result::Result<(), ClusterComponentError> {
let mut diagnostics = self
.state
.lock()
.expect("cluster component lifecycle poisoned");
if !diagnostics.is_running() {
diagnostics.record_start();
}
Ok(())
}
async fn stop(&self) -> std::result::Result<(), ClusterComponentError> {
let mut diagnostics = self
.state
.lock()
.expect("cluster component lifecycle poisoned");
if !diagnostics.is_stopped() {
diagnostics.record_shutdown_requested();
diagnostics.record_graceful_stop();
}
Ok(())
}
fn diagnostics(&self) -> ClusterLifecycleDiagnostics {
self.state
.lock()
.expect("cluster component lifecycle poisoned")
.clone()
}
fn last_error(&self) -> Option<String> {
self.diagnostics().last_error
}
}