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("Unregister finalization outcome is unknown: {reason}")]
47 UnregisterFinalizationOutcomeUnknown { reason: String },
48
49 #[error("Unregister teardown is still in progress for runtime {runtime_id}")]
53 UnregisterInProgress { runtime_id: LogicalRuntimeId },
54
55 #[error("Runtime stop cleanup is still in progress for runtime {runtime_id}")]
59 RuntimeStopInProgress { runtime_id: LogicalRuntimeId },
60
61 #[error("Internal error: {0}")]
63 Internal(String),
64}
65
66#[derive(Debug, Clone, thiserror::Error)]
68#[non_exhaustive]
69pub enum RuntimeControlPlaneError {
70 #[error("Runtime not found: {0}")]
72 NotFound(LogicalRuntimeId),
73
74 #[error("Invalid state for operation: {state}")]
76 InvalidState { state: RuntimeState },
77
78 #[error("Store error: {0}")]
80 StoreError(String),
81
82 #[error("Internal error: {0}")]
84 Internal(String),
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct RecoveryReport {
90 pub inputs_recovered: usize,
92 pub inputs_abandoned: usize,
94 pub inputs_requeued: usize,
96 #[serde(default, skip_serializing_if = "Vec::is_empty")]
98 pub details: Vec<String>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct RetireReport {
104 pub inputs_abandoned: usize,
106 #[serde(default)]
108 pub inputs_pending_drain: usize,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct ResetReport {
114 pub inputs_abandoned: usize,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct RecycleReport {
121 pub inputs_transferred: usize,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct DestroyReport {
128 pub inputs_abandoned: usize,
130}
131
132#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
137#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
138pub trait RuntimeDriver: Send + Sync {
139 async fn accept_input(&mut self, input: Input) -> Result<AcceptOutcome, RuntimeDriverError>;
141
142 async fn on_runtime_event(
144 &mut self,
145 event: RuntimeEventEnvelope,
146 ) -> Result<(), RuntimeDriverError>;
147
148 async fn recover(&mut self) -> Result<RecoveryReport, RuntimeDriverError>;
150
151 fn runtime_state(&self) -> RuntimeState;
153
154 fn input_state(&self, input_id: &InputId) -> Option<&InputState>;
156
157 fn input_phase(&self, input_id: &InputId) -> Option<InputLifecycleState>;
159
160 fn input_last_run_id(&self, input_id: &InputId) -> Option<RunId>;
162
163 fn input_last_boundary_sequence(&self, input_id: &InputId) -> Option<u64>;
165
166 fn stored_input_state(&self, input_id: &InputId) -> Option<StoredInputState>;
168
169 fn stored_input_states_snapshot(&self) -> Result<Vec<StoredInputState>, RuntimeDriverError>;
175
176 fn input_id_for_idempotency_key(&self, idempotency_key: &str) -> Option<InputId>;
182
183 fn active_input_ids(&self) -> Vec<InputId>;
185}
186
187#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
189#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
190pub trait RuntimeControlPlane: Send + Sync {
191 async fn ingest(
193 &self,
194 runtime_id: &LogicalRuntimeId,
195 input: Input,
196 ) -> Result<AcceptOutcome, RuntimeControlPlaneError>;
197
198 async fn publish_event(
200 &self,
201 event: RuntimeEventEnvelope,
202 ) -> Result<(), RuntimeControlPlaneError>;
203
204 async fn retire(
206 &self,
207 runtime_id: &LogicalRuntimeId,
208 ) -> Result<RetireReport, RuntimeControlPlaneError>;
209
210 async fn recycle(
212 &self,
213 runtime_id: &LogicalRuntimeId,
214 ) -> Result<RecycleReport, RuntimeControlPlaneError>;
215
216 async fn reset(
218 &self,
219 runtime_id: &LogicalRuntimeId,
220 ) -> Result<ResetReport, RuntimeControlPlaneError>;
221
222 async fn recover(
224 &self,
225 runtime_id: &LogicalRuntimeId,
226 ) -> Result<RecoveryReport, RuntimeControlPlaneError>;
227
228 async fn runtime_state(
230 &self,
231 runtime_id: &LogicalRuntimeId,
232 ) -> Result<RuntimeState, RuntimeControlPlaneError>;
233
234 async fn destroy(
236 &self,
237 runtime_id: &LogicalRuntimeId,
238 ) -> Result<DestroyReport, RuntimeControlPlaneError>;
239
240 async fn load_boundary_receipt(
242 &self,
243 runtime_id: &LogicalRuntimeId,
244 run_id: &RunId,
245 sequence: u64,
246 ) -> Result<Option<meerkat_core::lifecycle::RunBoundaryReceipt>, RuntimeControlPlaneError>;
247}
248
249#[cfg(test)]
250#[allow(clippy::unwrap_used)]
251mod tests {
252 use super::*;
253
254 fn _assert_driver_object_safe(_: &dyn RuntimeDriver) {}
256 fn _assert_control_plane_object_safe(_: &dyn RuntimeControlPlane) {}
257
258 #[test]
259 fn runtime_driver_error_display() {
260 let err = RuntimeDriverError::NotReady {
261 state: RuntimeState::Initializing,
262 };
263 assert!(err.to_string().contains("initializing"));
264
265 let err = RuntimeDriverError::ValidationFailed {
266 reason: "bad input".into(),
267 };
268 assert!(err.to_string().contains("bad input"));
269 }
270
271 #[test]
272 fn runtime_control_plane_error_display() {
273 let err = RuntimeControlPlaneError::NotFound(LogicalRuntimeId::new("missing"));
274 assert!(err.to_string().contains("missing"));
275 }
276
277 #[test]
278 fn recovery_report_serde() {
279 let report = RecoveryReport {
280 inputs_recovered: 5,
281 inputs_abandoned: 1,
282 inputs_requeued: 3,
283 details: vec!["requeued 3 staged inputs".into()],
284 };
285 let json = serde_json::to_value(&report).unwrap();
286 let parsed: RecoveryReport = serde_json::from_value(json).unwrap();
287 assert_eq!(parsed.inputs_recovered, 5);
288 }
289}