mecha10-core 0.8.0

Mecha10 core runtime and foundational types
Documentation
//! Service Pattern for Infrastructure Components
//!
//! This module defines the Service trait for infrastructure and singleton components
//! that complement the Node pattern. While Nodes are designed for data processing
//! via pub/sub messaging, Services provide capabilities like HTTP servers, database
//! connections, job processors, and other infrastructure components.
//!
//! # Node vs Service
//!
//! | Aspect | Node | Service |
//! |--------|------|---------|
//! | **Purpose** | Data processing via pub/sub | Infrastructure/capabilities |
//! | **Lifecycle** | `init → run (loop) → shutdown` | `init → start → stop` |
//! | **Concurrency** | Single run loop | Multiple background tasks |
//! | **Instances** | Can have multiple (e.g., camera/left, camera/right) | Typically singleton |
//! | **Context** | Has Context for pub/sub | May use Context internally |
//! | **Examples** | camera_fake, lidar_filter, task_planner | HTTP API, database pool, job queue |
//!
//! # Example
//!
//! Note: this example models a job-queue worker rather than an HTTP server, since
//! `axum` is not a dependency of `mecha10-core` (and so isn't available to this
//! crate's doctests) — the `Service` lifecycle shown below applies equally to an
//! HTTP API service built on whatever web framework a consuming crate chooses.
//!
//! ```rust
//! use mecha10_core::prelude::*;
//! use mecha10_core::service::Service;
//! use mecha10_core::service_manager::ServiceManager;
//!
//! #[derive(Debug)]
//! pub struct JobQueueService {
//!     config: JobQueueConfig,
//!     worker_handle: Option<tokio::task::JoinHandle<()>>,
//!     shutdown_tx: Option<tokio::sync::oneshot::Sender<()>>,
//! }
//!
//! #[derive(Debug, serde::Deserialize)]
//! pub struct JobQueueConfig {
//!     pub poll_interval_ms: u64,
//! }
//!
//! #[async_trait::async_trait]
//! impl Service for JobQueueService {
//!     type Config = JobQueueConfig;
//!
//!     async fn init(config: Self::Config) -> Result<Self> {
//!         Ok(Self {
//!             config,
//!             worker_handle: None,
//!             shutdown_tx: None,
//!         })
//!     }
//!
//!     async fn start(&mut self) -> Result<()> {
//!         let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel();
//!         self.shutdown_tx = Some(shutdown_tx);
//!
//!         let poll_interval = tokio::time::Duration::from_millis(self.config.poll_interval_ms);
//!         let worker_handle = tokio::spawn(async move {
//!             loop {
//!                 tokio::select! {
//!                     _ = tokio::time::sleep(poll_interval) => {
//!                         // Poll for and process the next job here.
//!                     }
//!                     _ = &mut shutdown_rx => break,
//!                 }
//!             }
//!         });
//!
//!         self.worker_handle = Some(worker_handle);
//!         info!("Job queue worker started");
//!         Ok(())
//!     }
//!
//!     async fn stop(&mut self) -> Result<()> {
//!         if let Some(tx) = self.shutdown_tx.take() {
//!             let _ = tx.send(());
//!         }
//!
//!         if let Some(handle) = self.worker_handle.take() {
//!             handle.await.map_err(|e| Mecha10Error::Other(e.to_string()))?;
//!         }
//!
//!         info!("Job queue worker stopped");
//!         Ok(())
//!     }
//!
//!     fn name(&self) -> &str {
//!         "job_queue"
//!     }
//! }
//!
//! # async fn example() -> Result<()> {
//! let mut manager = ServiceManager::new();
//! manager
//!     .register::<JobQueueService>(JobQueueConfig { poll_interval_ms: 1000 })
//!     .await?;
//! manager.start_all().await?;
//! manager.stop_all().await?;
//! # Ok(())
//! # }
//! ```

use crate::{HealthStatus, Result};
use async_trait::async_trait;
use serde::de::DeserializeOwned;
use std::fmt::Debug;

#[async_trait]
pub trait Service: Send + Sync + Debug + 'static {
    type Config: DeserializeOwned + Send + Sync + Debug;

    /// Initialize the service with configuration.
    ///
    /// Called once at startup before `start()`. Use this to:
    /// - Load and validate configuration
    /// - Allocate resources
    /// - Prepare for startup (but don't start background tasks yet)
    ///
    /// # Arguments
    ///
    /// * `config` - Service configuration
    ///
    /// # Returns
    ///
    /// * `Ok(Self)` - Successfully initialized service
    /// * `Err(_)` - Initialization failed
    ///
    /// # Example
    ///
    /// ```rust
    /// # use mecha10_core::prelude::*;
    /// # use mecha10_core::service::Service;
    /// # #[derive(Debug)]
    /// # struct DatabaseService { pool: Option<()> }
    /// # #[derive(Debug, serde::Deserialize)]
    /// # struct DatabaseConfig { url: String, max_connections: u32 }
    /// # #[async_trait::async_trait]
    /// # impl Service for DatabaseService {
    /// # type Config = DatabaseConfig;
    /// async fn init(config: Self::Config) -> Result<Self> {
    ///     // Validate config
    ///     if config.max_connections == 0 {
    ///         return Err(anyhow::anyhow!("max_connections must be > 0").into());
    ///     }
    ///
    ///     Ok(Self {
    ///         pool: None,  // Will connect in start()
    ///     })
    /// }
    /// # async fn start(&mut self) -> Result<()> { Ok(()) }
    /// # async fn stop(&mut self) -> Result<()> { Ok(()) }
    /// # fn name(&self) -> &str { "database" }
    /// # }
    /// ```
    async fn init(config: Self::Config) -> Result<Self>
    where
        Self: Sized;

    /// Start the service.
    ///
    /// Called after all services are initialized. Use this to:
    /// - Spawn background tasks
    /// - Start listening on ports/sockets
    /// - Begin processing
    ///
    /// This method should return quickly - background work should be spawned
    /// as separate tasks.
    ///
    /// # Returns
    ///
    /// * `Ok(())` - Service started successfully
    /// * `Err(_)` - Startup failed
    ///
    /// # Example
    ///
    /// ```rust
    /// # use mecha10_core::prelude::*;
    /// # use mecha10_core::service::Service;
    /// # #[derive(Debug)]
    /// # struct JobProcessor {
    /// #     worker_handles: Vec<tokio::task::JoinHandle<()>>,
    /// #     job_tx: tokio::sync::mpsc::Sender<()>,
    /// # }
    /// # #[derive(Debug, serde::Deserialize)]
    /// # struct JobConfig { worker_count: usize }
    /// # #[async_trait::async_trait]
    /// # impl Service for JobProcessor {
    /// # type Config = JobConfig;
    /// # async fn init(_config: Self::Config) -> Result<Self> {
    /// #     let (job_tx, _) = tokio::sync::mpsc::channel(100);
    /// #     Ok(Self { worker_handles: Vec::new(), job_tx })
    /// # }
    /// async fn start(&mut self) -> Result<()> {
    ///     // Spawn worker tasks
    ///     for i in 0..4 {
    ///         let handle = tokio::spawn(async move {
    ///             info!("Worker {} started", i);
    ///             // Worker loop...
    ///         });
    ///         self.worker_handles.push(handle);
    ///     }
    ///
    ///     info!("Job processor started with {} workers", 4);
    ///     Ok(())
    /// }
    /// # async fn stop(&mut self) -> Result<()> { Ok(()) }
    /// # fn name(&self) -> &str { "job_processor" }
    /// # }
    /// ```
    async fn start(&mut self) -> Result<()>;

    /// Stop the service gracefully.
    ///
    /// Called during shutdown. Use this to:
    /// - Signal background tasks to stop
    /// - Wait for tasks to complete
    /// - Release resources (close connections, files, etc.)
    ///
    /// This method should block until all background work is complete.
    ///
    /// # Returns
    ///
    /// * `Ok(())` - Service stopped successfully
    /// * `Err(_)` - Shutdown failed (may leave resources in inconsistent state)
    ///
    /// # Example
    ///
    /// ```rust
    /// # use mecha10_core::prelude::*;
    /// # use mecha10_core::service::Service;
    /// # #[derive(Debug)]
    /// # struct DatabaseService { pool: Option<()> }
    /// # #[derive(Debug, serde::Deserialize)]
    /// # struct DatabaseConfig;
    /// # #[async_trait::async_trait]
    /// # impl Service for DatabaseService {
    /// # type Config = DatabaseConfig;
    /// # async fn init(_config: Self::Config) -> Result<Self> { Ok(Self { pool: None }) }
    /// # async fn start(&mut self) -> Result<()> { Ok(()) }
    /// async fn stop(&mut self) -> Result<()> {
    ///     if let Some(pool) = self.pool.take() {
    ///         // Close all connections
    ///         // pool.close().await;
    ///     }
    ///     info!("Database connection pool closed");
    ///     Ok(())
    /// }
    /// # fn name(&self) -> &str { "database" }
    /// # }
    /// ```
    async fn stop(&mut self) -> Result<()>;

    /// Check service health.
    ///
    /// Called periodically by health monitoring. Override this to provide
    /// meaningful health status.
    ///
    /// Default implementation returns healthy.
    ///
    /// # Returns
    ///
    /// * `HealthStatus` - Current health status
    ///
    /// # Example
    ///
    /// ```rust
    /// # use mecha10_core::prelude::*;
    /// # use mecha10_core::service::Service;
    /// # #[derive(Debug)]
    /// # struct HttpApiService { connected_clients: u32 }
    /// # #[derive(Debug, serde::Deserialize)]
    /// # struct HttpApiConfig;
    /// # #[async_trait::async_trait]
    /// # impl Service for HttpApiService {
    /// # type Config = HttpApiConfig;
    /// # async fn init(_config: Self::Config) -> Result<Self> {
    /// #     Ok(Self { connected_clients: 0 })
    /// # }
    /// # async fn start(&mut self) -> Result<()> { Ok(()) }
    /// # async fn stop(&mut self) -> Result<()> { Ok(()) }
    /// # fn name(&self) -> &str { "http_api" }
    /// async fn health_check(&self) -> HealthStatus {
    ///     let healthy = self.connected_clients < 1000;  // Check capacity
    ///
    ///     HealthStatus {
    ///         healthy,
    ///         last_check: now_micros(),
    ///         failure_count: if healthy { 0 } else { 1 },
    ///         message: Some(format!(
    ///             "Connected clients: {}/1000",
    ///             self.connected_clients
    ///         )),
    ///     }
    /// }
    /// # }
    /// ```
    async fn health_check(&self) -> HealthStatus {
        HealthStatus {
            healthy: true,
            last_check: crate::prelude::now_micros(),
            failure_count: 0,
            message: Some("Service operational".to_string()),
        }
    }

    /// Service name for logging and registration.
    ///
    /// This should be a unique identifier for the service type (not instance).
    ///
    /// # Returns
    ///
    /// * `&str` - Service name
    ///
    /// # Example
    ///
    /// ```rust
    /// # use mecha10_core::service::Service;
    /// # use mecha10_core::Result;
    /// # #[derive(Debug)]
    /// # struct HttpApiService;
    /// # #[derive(Debug, serde::Deserialize)]
    /// # struct HttpApiConfig;
    /// # #[async_trait::async_trait]
    /// # impl Service for HttpApiService {
    /// # type Config = HttpApiConfig;
    /// # async fn init(_config: Self::Config) -> Result<Self> {
    /// #     Ok(Self)
    /// # }
    /// # async fn start(&mut self) -> Result<()> { Ok(()) }
    /// # async fn stop(&mut self) -> Result<()> { Ok(()) }
    /// fn name(&self) -> &str {
    ///     "http_api"
    /// }
    /// # }
    /// ```
    fn name(&self) -> &str;
}