gglib_core/ports/
process_runner.rs

1//! Process runner trait definition.
2//!
3//! This port defines the interface for managing model server processes.
4//! Implementations handle all process lifecycle details internally.
5
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9
10use super::ProcessError;
11
12/// Configuration for starting a model server.
13///
14/// This is an intent-based configuration — it expresses what the caller
15/// wants, not how the server should be started.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct ServerConfig {
18    /// Database ID of the model to serve.
19    pub model_id: i64,
20    /// Human-readable model name.
21    pub model_name: String,
22    /// Path to the model file.
23    pub model_path: PathBuf,
24    /// Port to listen on (if None, a free port will be assigned).
25    pub port: Option<u16>,
26    /// Base port for allocation when port is None.
27    pub base_port: u16,
28    /// Context size to use (if None, use model default).
29    pub context_size: Option<u64>,
30    /// Number of GPU layers to offload (if None, use default).
31    pub gpu_layers: Option<i32>,
32    /// Additional server-specific options.
33    pub extra_args: Vec<String>,
34}
35
36impl ServerConfig {
37    /// Create a new server configuration with required fields.
38    #[must_use]
39    pub const fn new(
40        model_id: i64,
41        model_name: String,
42        model_path: PathBuf,
43        base_port: u16,
44    ) -> Self {
45        Self {
46            model_id,
47            model_name,
48            model_path,
49            port: None,
50            base_port,
51            context_size: None,
52            gpu_layers: None,
53            extra_args: Vec::new(),
54        }
55    }
56
57    /// Set the port to listen on.
58    #[must_use]
59    pub const fn with_port(mut self, port: u16) -> Self {
60        self.port = Some(port);
61        self
62    }
63
64    /// Set the context size.
65    #[must_use]
66    pub const fn with_context_size(mut self, size: u64) -> Self {
67        self.context_size = Some(size);
68        self
69    }
70
71    /// Set the number of GPU layers.
72    #[must_use]
73    pub const fn with_gpu_layers(mut self, layers: i32) -> Self {
74        self.gpu_layers = Some(layers);
75        self
76    }
77
78    /// Add extra arguments to pass to the server.
79    #[must_use]
80    pub fn with_extra_args(mut self, args: Vec<String>) -> Self {
81        self.extra_args = args;
82        self
83    }
84}
85
86/// Handle to a running server process.
87///
88/// This is an opaque handle that implementations use to track processes.
89/// It contains enough information to identify and manage the process.
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct ProcessHandle {
92    /// Database ID of the model being served.
93    pub model_id: i64,
94    /// Human-readable model name.
95    pub model_name: String,
96    /// Process ID (if running on local system).
97    pub pid: Option<u32>,
98    /// Port the server is listening on.
99    pub port: u16,
100    /// Unix timestamp (seconds) when the server was started.
101    pub started_at: u64,
102}
103
104impl ProcessHandle {
105    /// Create a new process handle.
106    #[must_use]
107    pub const fn new(
108        model_id: i64,
109        model_name: String,
110        pid: Option<u32>,
111        port: u16,
112        started_at: u64,
113    ) -> Self {
114        Self {
115            model_id,
116            model_name,
117            pid,
118            port,
119            started_at,
120        }
121    }
122}
123
124/// Health status of a running server.
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct ServerHealth {
127    /// Whether the server is responding to health checks.
128    pub healthy: bool,
129    /// Unix timestamp (seconds) of the last successful health check.
130    pub last_check: Option<u64>,
131    /// Context size being used by the server.
132    pub context_size: Option<u64>,
133    /// Optional status message.
134    pub message: Option<String>,
135}
136
137impl ServerHealth {
138    /// Get the current Unix timestamp in seconds.
139    fn now_secs() -> u64 {
140        std::time::SystemTime::now()
141            .duration_since(std::time::UNIX_EPOCH)
142            .unwrap()
143            .as_secs()
144    }
145
146    /// Create a healthy server status.
147    #[must_use]
148    pub fn healthy() -> Self {
149        Self {
150            healthy: true,
151            last_check: Some(Self::now_secs()),
152            context_size: None,
153            message: None,
154        }
155    }
156
157    /// Create an unhealthy server status with a message.
158    pub fn unhealthy(message: impl Into<String>) -> Self {
159        Self {
160            healthy: false,
161            last_check: Some(Self::now_secs()),
162            context_size: None,
163            message: Some(message.into()),
164        }
165    }
166
167    /// Set the context size.
168    #[must_use]
169    pub const fn with_context_size(mut self, size: u64) -> Self {
170        self.context_size = Some(size);
171        self
172    }
173}
174
175/// Process runner for managing model server processes.
176///
177/// This trait abstracts process management for testability and
178/// potential alternative backends (local, remote, containerized).
179///
180/// # Design Rules
181///
182/// - Express **intent**, not implementation detail
183/// - No CLI/Tauri/Axum concerns in signatures
184/// - Must support: mock runner, remote runner, alternative inference backends
185#[async_trait]
186pub trait ProcessRunner: Send + Sync {
187    /// Start a model server with the given configuration.
188    ///
189    /// Returns a handle that can be used to manage the process.
190    async fn start(&self, config: ServerConfig) -> Result<ProcessHandle, ProcessError>;
191
192    /// Stop a running server.
193    ///
194    /// Returns `Err(ProcessError::NotRunning)` if the process isn't running.
195    async fn stop(&self, handle: &ProcessHandle) -> Result<(), ProcessError>;
196
197    /// Check if a server is still running.
198    async fn is_running(&self, handle: &ProcessHandle) -> bool;
199
200    /// Get the health status of a running server.
201    ///
202    /// Returns `Err(ProcessError::NotRunning)` if the process isn't running.
203    async fn health(&self, handle: &ProcessHandle) -> Result<ServerHealth, ProcessError>;
204
205    /// List all currently running server processes.
206    ///
207    /// This is needed for snapshot behavior (e.g., `server:snapshot` events).
208    async fn list_running(&self) -> Result<Vec<ProcessHandle>, ProcessError>;
209}