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}