1use meerkat_core::lifecycle::{InputId, RunId};
6use serde::{Deserialize, Serialize};
7
8use crate::accept::AcceptOutcome;
9use crate::identifiers::LogicalRuntimeId;
10use crate::input::Input;
11use crate::input_state::{InputLifecycleState, InputState, StoredInputState};
12use crate::runtime_event::RuntimeEventEnvelope;
13use crate::runtime_state::RuntimeState;
14
15#[derive(Debug, Clone, thiserror::Error)]
17#[non_exhaustive]
18pub enum RuntimeDriverError {
19 #[error("Runtime not ready: {state}")]
21 NotReady { state: RuntimeState },
22
23 #[error("Runtime not found: {runtime_id}")]
30 NotFound { runtime_id: LogicalRuntimeId },
31
32 #[error("Input validation failed: {reason}")]
34 ValidationFailed { reason: String },
35
36 #[error("Runtime destroyed")]
38 Destroyed,
39
40 #[error("Recovery corruption: {reason}")]
42 RecoveryCorruption { reason: String },
43
44 #[error("Runtime recovery should back off: {reason}")]
46 RecoveryBackoff { reason: String },
47
48 #[error("Runtime recovery is repair-blocked: {reason}")]
50 RecoveryRepairBlocked {
51 evidence_digest: Option<String>,
52 reason: String,
53 },
54
55 #[error("Unregister finalization outcome is unknown: {reason}")]
58 UnregisterFinalizationOutcomeUnknown { reason: String },
59
60 #[error("Unregister teardown is still in progress for runtime {runtime_id}")]
64 UnregisterInProgress { runtime_id: LogicalRuntimeId },
65
66 #[error("Runtime stop cleanup is still in progress for runtime {runtime_id}")]
70 RuntimeStopInProgress { runtime_id: LogicalRuntimeId },
71
72 #[error("Stale runtime authority: {reason}")]
75 StaleAuthority { reason: String },
76
77 #[error("Internal error: {0}")]
79 Internal(String),
80}
81
82#[derive(Debug, Clone, thiserror::Error)]
84#[non_exhaustive]
85pub enum RuntimeControlPlaneError {
86 #[error("Runtime not found: {0}")]
88 NotFound(LogicalRuntimeId),
89
90 #[error("Invalid state for operation: {state}")]
92 InvalidState { state: RuntimeState },
93
94 #[error("Store error: {0}")]
96 StoreError(String),
97
98 #[error("Internal error: {0}")]
100 Internal(String),
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct RecoveryReport {
106 pub inputs_recovered: usize,
108 pub inputs_abandoned: usize,
110 pub inputs_requeued: usize,
112 #[serde(default, skip_serializing_if = "Vec::is_empty")]
114 pub details: Vec<String>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct RetireReport {
120 pub inputs_abandoned: usize,
122 #[serde(default)]
124 pub inputs_pending_drain: usize,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct ResetReport {
130 pub inputs_abandoned: usize,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct RecycleReport {
137 pub inputs_transferred: usize,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct DestroyReport {
144 pub inputs_abandoned: usize,
146}
147
148#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
153#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
154pub trait RuntimeDriver: Send + Sync {
155 async fn accept_input(&mut self, input: Input) -> Result<AcceptOutcome, RuntimeDriverError>;
157
158 async fn on_runtime_event(
160 &mut self,
161 event: RuntimeEventEnvelope,
162 ) -> Result<(), RuntimeDriverError>;
163
164 async fn recover(&mut self) -> Result<RecoveryReport, RuntimeDriverError>;
166
167 fn runtime_state(&self) -> RuntimeState;
169
170 fn input_state(&self, input_id: &InputId) -> Option<&InputState>;
172
173 fn input_phase(&self, input_id: &InputId) -> Option<InputLifecycleState>;
175
176 fn input_last_run_id(&self, input_id: &InputId) -> Option<RunId>;
178
179 fn input_last_boundary_sequence(&self, input_id: &InputId) -> Option<u64>;
181
182 fn stored_input_state(&self, input_id: &InputId) -> Option<StoredInputState>;
184
185 fn stored_input_states_snapshot(&self) -> Result<Vec<StoredInputState>, RuntimeDriverError>;
191
192 fn input_id_for_idempotency_key(&self, idempotency_key: &str) -> Option<InputId>;
198
199 fn active_input_ids(&self) -> Vec<InputId>;
201}
202
203#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
205#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
206pub trait RuntimeControlPlane: Send + Sync {
207 async fn ingest(
209 &self,
210 runtime_id: &LogicalRuntimeId,
211 input: Input,
212 ) -> Result<AcceptOutcome, RuntimeControlPlaneError>;
213
214 async fn publish_event(
222 &self,
223 event: RuntimeEventEnvelope,
224 ) -> Result<(), RuntimeControlPlaneError>;
225
226 async fn retire(
228 &self,
229 runtime_id: &LogicalRuntimeId,
230 ) -> Result<RetireReport, RuntimeControlPlaneError>;
231
232 async fn recycle(
234 &self,
235 runtime_id: &LogicalRuntimeId,
236 ) -> Result<RecycleReport, RuntimeControlPlaneError>;
237
238 async fn reset(
240 &self,
241 runtime_id: &LogicalRuntimeId,
242 ) -> Result<ResetReport, RuntimeControlPlaneError>;
243
244 async fn recover(
246 &self,
247 runtime_id: &LogicalRuntimeId,
248 ) -> Result<RecoveryReport, RuntimeControlPlaneError>;
249
250 async fn runtime_state(
252 &self,
253 runtime_id: &LogicalRuntimeId,
254 ) -> Result<RuntimeState, RuntimeControlPlaneError>;
255
256 async fn destroy(
258 &self,
259 runtime_id: &LogicalRuntimeId,
260 ) -> Result<DestroyReport, RuntimeControlPlaneError>;
261
262 async fn load_boundary_receipt(
264 &self,
265 runtime_id: &LogicalRuntimeId,
266 run_id: &RunId,
267 sequence: u64,
268 ) -> Result<Option<meerkat_core::lifecycle::RunBoundaryReceipt>, RuntimeControlPlaneError>;
269}
270
271#[cfg(test)]
272#[allow(clippy::unwrap_used)]
273mod tests {
274 use super::*;
275
276 fn _assert_driver_object_safe(_: &dyn RuntimeDriver) {}
278 fn _assert_control_plane_object_safe(_: &dyn RuntimeControlPlane) {}
279
280 #[test]
281 fn runtime_driver_error_display() {
282 let err = RuntimeDriverError::NotReady {
283 state: RuntimeState::Initializing,
284 };
285 assert!(err.to_string().contains("initializing"));
286
287 let err = RuntimeDriverError::ValidationFailed {
288 reason: "bad input".into(),
289 };
290 assert!(err.to_string().contains("bad input"));
291 }
292
293 #[test]
294 fn runtime_control_plane_error_display() {
295 let err = RuntimeControlPlaneError::NotFound(LogicalRuntimeId::new("missing"));
296 assert!(err.to_string().contains("missing"));
297 }
298
299 #[test]
300 fn recovery_report_serde() {
301 let report = RecoveryReport {
302 inputs_recovered: 5,
303 inputs_abandoned: 1,
304 inputs_requeued: 3,
305 details: vec!["requeued 3 staged inputs".into()],
306 };
307 let json = serde_json::to_value(&report).unwrap();
308 let parsed: RecoveryReport = serde_json::from_value(json).unwrap();
309 assert_eq!(parsed.inputs_recovered, 5);
310 }
311}