mothership 0.0.100

Process supervisor with HTTP exposure - wrap, monitor, and expose your fleet
Documentation
//! Election backends for flagship coordination
//!
//! The `Election` trait defines the interface for leader election and signaling.
//! Implementations handle acquiring/releasing the flagship lock and coordinating
//! with escort instances.

use std::fmt;
use std::time::Duration;

/// Errors that can occur during election operations
#[derive(Debug)]
pub enum ElectionError {
    /// Lock acquisition failed (someone else is flagship)
    LockNotAcquired,
    /// Connection or I/O error
    Connection(String),
    /// Timeout waiting for signal
    Timeout,
    /// Flagship signaled abort
    Aborted,
    /// Configuration error
    Config(String),
    /// Circuit breaker open - connection attempts suspended
    CircuitBreakerOpen,
}

impl fmt::Display for ElectionError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ElectionError::LockNotAcquired => write!(f, "flagship lock not acquired"),
            ElectionError::Connection(msg) => write!(f, "connection error: {}", msg),
            ElectionError::Timeout => write!(f, "timeout waiting for flagship signal"),
            ElectionError::Aborted => write!(f, "flagship signaled abort"),
            ElectionError::Config(msg) => write!(f, "configuration error: {}", msg),
            ElectionError::CircuitBreakerOpen => write!(
                f,
                "PostgreSQL election circuit breaker open - connection attempts suspended"
            ),
        }
    }
}

impl std::error::Error for ElectionError {}

/// Signal status from flagship to escorts
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlagshipSignal {
    /// Flagship is running prelaunch
    Running,
    /// Flagship completed prelaunch successfully
    Ready,
    /// Flagship failed, abort deployment
    Abort,
}

impl fmt::Display for FlagshipSignal {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FlagshipSignal::Running => write!(f, "running"),
            FlagshipSignal::Ready => write!(f, "ready"),
            FlagshipSignal::Abort => write!(f, "abort"),
        }
    }
}

/// Election backend trait for flagship coordination
///
/// Implementations handle:
/// - Lock acquisition (who becomes flagship)
/// - Signal broadcasting (flagship → escorts)
/// - Signal waiting (escorts wait for flagship)
pub trait Election: Send + Sync {
    /// Attempt to acquire the flagship lock (non-blocking)
    ///
    /// Returns `Ok(true)` if this instance became the flagship,
    /// `Ok(false)` if another instance holds the lock.
    fn try_acquire(
        &self,
        app_name: &str,
    ) -> impl std::future::Future<Output = Result<bool, ElectionError>> + Send;

    /// Release the flagship lock
    fn release(
        &self,
        app_name: &str,
    ) -> impl std::future::Future<Output = Result<(), ElectionError>> + Send;

    /// Signal status to escorts (flagship only)
    fn signal(
        &self,
        app_name: &str,
        status: FlagshipSignal,
    ) -> impl std::future::Future<Output = Result<(), ElectionError>> + Send;

    /// Wait for flagship signal (escorts only)
    ///
    /// Returns when flagship signals Ready or Abort.
    /// Returns error on timeout or connection failure.
    fn wait_for_signal(
        &self,
        app_name: &str,
        timeout: Duration,
    ) -> impl std::future::Future<Output = Result<FlagshipSignal, ElectionError>> + Send;

    /// Get the current signal status (for late joiners)
    fn get_signal(
        &self,
        app_name: &str,
    ) -> impl std::future::Future<Output = Result<Option<FlagshipSignal>, ElectionError>> + Send;
}

/// Static election backend - no coordination, explicit designation
///
/// The flagship is determined by evaluating an environment variable or command
/// at startup. No inter-server coordination is performed.
///
/// Use this when:
/// - You have a single server or external orchestration (Kamal, etc.)
/// - You want explicit control over which instance runs prelaunch
pub struct StaticElection {
    is_flagship: bool,
}

impl StaticElection {
    /// Create a new static election backend
    ///
    /// `is_flagship` is determined by evaluating the `static_flagship` config
    /// (env var or command output) at startup.
    pub fn new(is_flagship: bool) -> Self {
        Self { is_flagship }
    }

    /// Check if this instance is configured as the flagship
    pub fn is_flagship(&self) -> bool {
        self.is_flagship
    }
}

impl Election for StaticElection {
    async fn try_acquire(&self, _app_name: &str) -> Result<bool, ElectionError> {
        // Static election: we're flagship if configured as such
        Ok(self.is_flagship)
    }

    async fn release(&self, _app_name: &str) -> Result<(), ElectionError> {
        // No lock to release in static mode
        Ok(())
    }

    async fn signal(&self, _app_name: &str, _status: FlagshipSignal) -> Result<(), ElectionError> {
        // No signaling in static mode - escorts don't exist or are externally coordinated
        Ok(())
    }

    async fn wait_for_signal(
        &self,
        _app_name: &str,
        _timeout: Duration,
    ) -> Result<FlagshipSignal, ElectionError> {
        // In static mode, non-flagship instances should not wait
        // They either skip prelaunch or rely on external orchestration
        if self.is_flagship {
            // Flagship doesn't wait for itself
            Ok(FlagshipSignal::Ready)
        } else {
            // Escort in static mode: assume flagship is ready (external coordination)
            Ok(FlagshipSignal::Ready)
        }
    }

    async fn get_signal(&self, _app_name: &str) -> Result<Option<FlagshipSignal>, ElectionError> {
        // No persistent signal in static mode
        Ok(None)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_static_election_flagship() {
        let election = StaticElection::new(true);
        assert!(election.is_flagship());
        assert!(election.try_acquire("test").await.unwrap());
    }

    #[tokio::test]
    async fn test_static_election_escort() {
        let election = StaticElection::new(false);
        assert!(!election.is_flagship());
        assert!(!election.try_acquire("test").await.unwrap());
    }

    #[tokio::test]
    async fn test_static_election_wait() {
        let election = StaticElection::new(false);
        let signal = election
            .wait_for_signal("test", Duration::from_secs(1))
            .await
            .unwrap();
        // Static mode assumes external coordination
        assert_eq!(signal, FlagshipSignal::Ready);
    }
}