1use std::fmt::{Display, Formatter};
4
5use serde::{Deserialize, Serialize};
6
7use crate::mob_handle_runtime::MobRuntimeError;
8use crate::runtime::{
9 NormalizationError, RuntimeRouteMutationError, RuntimeShutdownReport, ScheduleValidationError,
10 SubscribeError,
11};
12
13use super::edge_types::{DesiredPeerEdge, EdgeReconcileFailure};
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
20pub struct UnifiedRuntimeReconcileEdgesReport {
21 pub desired_edges: Vec<DesiredPeerEdge>,
22 pub wired_edges: Vec<DesiredPeerEdge>,
23 pub unwired_edges: Vec<DesiredPeerEdge>,
24 pub retained_edges: Vec<DesiredPeerEdge>,
25 pub preexisting_edges: Vec<DesiredPeerEdge>,
26 pub skipped_missing_members: Vec<DesiredPeerEdge>,
27 pub pruned_stale_managed_edges: Vec<DesiredPeerEdge>,
28 #[serde(default)]
29 pub failures: Vec<EdgeReconcileFailure>,
30}
31
32impl UnifiedRuntimeReconcileEdgesReport {
33 pub fn is_complete(&self) -> bool {
35 self.failures.is_empty() && self.skipped_missing_members.is_empty()
36 }
37}
38
39#[derive(Debug)]
40pub enum UnifiedRuntimeBootstrapError {
41 Mob(MobRuntimeError),
42 Module(crate::runtime::MobkitRuntimeError),
43 ModuleStartupThreadPanicked,
44 ModuleStartupRollbackFailed {
45 startup_error: Box<UnifiedRuntimeBootstrapError>,
46 rollback_error: MobRuntimeError,
47 },
48 PreSpawnHook(String),
49 IdentityFirst(String),
50 Topology(String),
51}
52
53impl Display for UnifiedRuntimeBootstrapError {
54 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
55 match self {
56 Self::Mob(err) => write!(f, "failed to bootstrap mob runtime: {err}"),
57 Self::Module(err) => write!(f, "failed to bootstrap module runtime: {err:?}"),
58 Self::ModuleStartupThreadPanicked => {
59 write!(
60 f,
61 "failed to bootstrap module runtime: startup thread panicked"
62 )
63 }
64 Self::PreSpawnHook(err) => {
65 write!(f, "pre-spawn hook failed: {err}")
66 }
67 Self::IdentityFirst(err) => {
68 write!(f, "identity-first bootstrap failed: {err}")
69 }
70 Self::Topology(err) => write!(f, "topology-control bootstrap failed: {err}"),
71 Self::ModuleStartupRollbackFailed {
72 startup_error,
73 rollback_error,
74 } => {
75 write!(
76 f,
77 "failed to bootstrap unified runtime: startup error ({startup_error}) and rollback failed: {rollback_error}"
78 )
79 }
80 }
81 }
82}
83
84impl std::error::Error for UnifiedRuntimeBootstrapError {}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum UnifiedRuntimeBuilderField {
88 MobSpec,
89 ModuleConfig,
90 Timeout,
91}
92
93#[derive(Debug)]
94pub enum UnifiedRuntimeBuilderError {
95 MissingRequiredField(UnifiedRuntimeBuilderField),
96 Bootstrap(UnifiedRuntimeBootstrapError),
97 Io(String),
99 DefinitionLoad(String),
101 ConflictingConfiguration(String),
103 StorageLayout(crate::storage_layout::StorageLayoutError),
105 StorageProvider(crate::storage_provider::MobKitStorageProviderError),
108}
109
110impl From<crate::storage_layout::StorageLayoutError> for UnifiedRuntimeBuilderError {
111 fn from(error: crate::storage_layout::StorageLayoutError) -> Self {
112 Self::StorageLayout(error)
113 }
114}
115
116impl From<crate::storage_provider::MobKitStorageProviderError> for UnifiedRuntimeBuilderError {
117 fn from(error: crate::storage_provider::MobKitStorageProviderError) -> Self {
118 Self::StorageProvider(error)
119 }
120}
121
122impl Display for UnifiedRuntimeBuilderError {
123 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
124 match self {
125 Self::MissingRequiredField(UnifiedRuntimeBuilderField::MobSpec) => {
126 write!(f, "missing required builder field: mob_spec or definition")
127 }
128 Self::MissingRequiredField(UnifiedRuntimeBuilderField::ModuleConfig) => {
129 write!(f, "missing required builder field: module_config")
130 }
131 Self::MissingRequiredField(UnifiedRuntimeBuilderField::Timeout) => {
132 write!(f, "missing required builder field: timeout")
133 }
134 Self::Bootstrap(err) => write!(f, "{err}"),
135 Self::Io(msg) => write!(f, "{msg}"),
136 Self::DefinitionLoad(msg) => write!(f, "{msg}"),
137 Self::ConflictingConfiguration(msg) => write!(f, "conflicting configuration: {msg}"),
138 Self::StorageLayout(err) => write!(f, "{err}"),
139 Self::StorageProvider(err) => write!(f, "{err}"),
140 }
141 }
142}
143
144impl std::error::Error for UnifiedRuntimeBuilderError {}
145
146#[derive(Debug)]
147pub enum UnifiedRuntimeError {
148 Normalize(NormalizationError),
149 Subscribe(SubscribeError),
150 ScheduleValidation(ScheduleValidationError),
151 RuntimeShuttingDown,
152 ScheduleDispatchThreadPanicked,
153}
154
155impl Display for UnifiedRuntimeError {
156 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
157 match self {
158 Self::Normalize(err) => write!(f, "failed to normalize unified event: {err:?}"),
159 Self::Subscribe(err) => write!(f, "failed to subscribe to unified events: {err:?}"),
160 Self::ScheduleValidation(err) => {
161 write!(f, "failed to dispatch schedule tick: {err:?}")
162 }
163 Self::RuntimeShuttingDown => {
164 write!(
165 f,
166 "failed to dispatch schedule tick: unified runtime is shutting down"
167 )
168 }
169 Self::ScheduleDispatchThreadPanicked => {
170 write!(
171 f,
172 "failed to dispatch schedule tick: dispatch thread panicked"
173 )
174 }
175 }
176 }
177}
178
179impl std::error::Error for UnifiedRuntimeError {}
180
181impl From<NormalizationError> for UnifiedRuntimeError {
182 fn from(value: NormalizationError) -> Self {
183 Self::Normalize(value)
184 }
185}
186
187impl From<SubscribeError> for UnifiedRuntimeError {
188 fn from(value: SubscribeError) -> Self {
189 Self::Subscribe(value)
190 }
191}
192
193impl From<ScheduleValidationError> for UnifiedRuntimeError {
194 fn from(value: ScheduleValidationError) -> Self {
195 Self::ScheduleValidation(value)
196 }
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
201pub enum IdentityAuthorityReleaseOutcome {
202 NotConfigured,
204 Released { grant_count: usize },
206 Failed { error: String },
208 SkippedResetCleanupFailed { error: String },
211 SkippedMobStopFailed,
213}
214
215#[derive(Debug)]
216pub struct UnifiedRuntimeShutdownReport {
217 pub drain: ShutdownDrainReport,
218 pub module_shutdown: RuntimeShutdownReport,
219 pub mob_stop: Result<(), MobRuntimeError>,
220 pub identity_authority_release: IdentityAuthorityReleaseOutcome,
221}
222
223impl UnifiedRuntimeShutdownReport {
224 pub fn cleanup_completed(&self) -> bool {
227 !self.drain.timed_out
228 && self.mob_stop.is_ok()
229 && matches!(
230 &self.identity_authority_release,
231 IdentityAuthorityReleaseOutcome::NotConfigured
232 | IdentityAuthorityReleaseOutcome::Released { .. }
233 )
234 && self.module_shutdown.orphan_processes == 0
235 }
236}
237
238#[derive(Debug)]
239pub struct UnifiedRuntimeRunReport {
240 pub serve_result: std::io::Result<()>,
241 pub shutdown: UnifiedRuntimeShutdownReport,
242}
243
244#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
246pub struct RediscoverReport {
247 pub spawned: Vec<String>,
249 pub edges: UnifiedRuntimeReconcileEdgesReport,
251}
252
253#[derive(Debug, Clone, PartialEq, Eq)]
254pub struct UnifiedRuntimeReconcileRoutingReport {
255 pub router_module_loaded: bool,
256 pub active_members: Vec<String>,
257 pub added_route_keys: Vec<String>,
258 pub removed_route_keys: Vec<String>,
259}
260
261pub use meerkat_contracts::MobReconcileFailureWire as MobReconcileFailure;
265
266pub use meerkat_contracts::MobReconcileReportWire as MobReconcileReport;
271
272pub fn meerkat_reconcile_report_to_wire(
280 mob_id: &str,
281 report: meerkat_mob::runtime::reconcile::ReconcileReport,
282) -> MobReconcileReport {
283 use meerkat_contracts::{MobSpawnReceiptWire, WireMemberRef};
284 let alias_of =
285 |id: &str| -> String { crate::member_comms_id::runtime_alias_str(id).into_owned() };
286 MobReconcileReport {
287 desired: report
288 .desired
289 .into_iter()
290 .map(|id| alias_of(id.as_str()))
291 .collect(),
292 retained: report
293 .retained
294 .into_iter()
295 .map(|id| alias_of(id.as_str()))
296 .collect(),
297 spawned: report
298 .spawned
299 .into_iter()
300 .map(|receipt| {
301 let identity_str = alias_of(receipt.agent_identity.as_str());
302 MobSpawnReceiptWire {
303 member_ref: WireMemberRef::encode(mob_id, &identity_str),
304 agent_identity: identity_str,
305 }
306 })
307 .collect(),
308 retired: report
309 .retired
310 .into_iter()
311 .map(|id| alias_of(id.as_str()))
312 .collect(),
313 failures: report
314 .failures
315 .into_iter()
316 .map(|failure| MobReconcileFailure {
317 agent_identity: alias_of(failure.agent_identity.as_str()),
318 stage: match failure.stage {
319 meerkat_mob::runtime::reconcile::ReconcileStage::Spawn => {
320 meerkat_contracts::WireMobReconcileStage::Spawn
321 }
322 meerkat_mob::runtime::reconcile::ReconcileStage::Retire => {
323 meerkat_contracts::WireMobReconcileStage::Retire
324 }
325 },
326 error: meerkat_contracts::WireMobError {
327 code: meerkat_mob::mob_error_wire_code(&failure.error),
328 message: failure.error.to_string(),
329 },
330 })
331 .collect(),
332 }
333}
334
335#[derive(Debug, Clone, PartialEq)]
338pub struct UnifiedRuntimeReconcileReport {
339 pub mob: MobReconcileReport,
340 pub edges: UnifiedRuntimeReconcileEdgesReport,
341 pub routing: UnifiedRuntimeReconcileRoutingReport,
342}
343
344#[derive(Debug)]
345pub enum UnifiedRuntimeReconcileError {
346 Mob(MobRuntimeError),
347 RouteMutation(RuntimeRouteMutationError),
348 PartialFailure(Box<UnifiedRuntimeReconcileReport>),
354}
355
356impl Display for UnifiedRuntimeReconcileError {
357 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
358 match self {
359 Self::Mob(err) => write!(f, "failed to reconcile mob roster: {err}"),
360 Self::RouteMutation(err) => {
361 write!(f, "failed to reconcile routing wiring: {err:?}")
362 }
363 Self::PartialFailure(report) => {
364 write!(
365 f,
366 "reconcile completed with {} per-identity failure(s): {:?}",
367 report.mob.failures.len(),
368 report.mob.failures
369 )
370 }
371 }
372 }
373}
374
375impl std::error::Error for UnifiedRuntimeReconcileError {}
376
377#[derive(Debug)]
378pub struct ShutdownDrainReport {
379 pub drained_count: usize,
380 pub timed_out: bool,
381 pub drain_duration_ms: u64,
382}
383
384#[non_exhaustive]
400#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
401#[serde(tag = "category", rename_all = "snake_case")]
402pub enum ErrorEvent {
403 SpawnFailure {
404 member_id: String,
405 profile: String,
406 error: String,
407 },
408 ReconcileIncomplete {
409 failures: usize,
410 skipped: usize,
411 },
412 CheckpointFailure {
413 session_id: String,
414 error: String,
415 },
416 HostLoopCrash {
417 member_id: String,
418 error: String,
419 },
420 RediscoverFailure {
421 error: String,
422 },
423 EventLogFlushFailure {
424 error: String,
425 },
426 IdentityMaterializationFailure {
427 identity: String,
428 initiator: Option<String>,
429 operation: String,
430 error: String,
431 },
432}
433
434impl Display for ErrorEvent {
435 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
436 match self {
437 Self::SpawnFailure {
438 member_id, error, ..
439 } => {
440 write!(f, "spawn_failure: {member_id}: {error}")
441 }
442 Self::ReconcileIncomplete { failures, skipped } => {
443 write!(
444 f,
445 "reconcile_incomplete: {failures} failures, {skipped} skipped"
446 )
447 }
448 Self::CheckpointFailure { session_id, error } => {
449 write!(f, "checkpoint_failure: {session_id}: {error}")
450 }
451 Self::HostLoopCrash { member_id, error } => {
452 write!(f, "host_loop_crash: {member_id}: {error}")
453 }
454 Self::RediscoverFailure { error } => {
455 write!(f, "rediscover_failure: {error}")
456 }
457 Self::EventLogFlushFailure { error } => {
458 write!(f, "event_log_flush_failure: {error}")
459 }
460 Self::IdentityMaterializationFailure {
461 identity,
462 initiator,
463 operation,
464 error,
465 } => {
466 if let Some(initiator) = initiator {
467 write!(
468 f,
469 "identity_materialization_failure: {identity} for {initiator} during {operation}: {error}"
470 )
471 } else {
472 write!(
473 f,
474 "identity_materialization_failure: {identity} during {operation}: {error}"
475 )
476 }
477 }
478 }
479 }
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485
486 fn completed_shutdown_report() -> UnifiedRuntimeShutdownReport {
487 UnifiedRuntimeShutdownReport {
488 drain: ShutdownDrainReport {
489 drained_count: 1,
490 timed_out: false,
491 drain_duration_ms: 2,
492 },
493 module_shutdown: RuntimeShutdownReport {
494 terminated_modules: vec!["router".to_string()],
495 orphan_processes: 0,
496 },
497 mob_stop: Ok(()),
498 identity_authority_release: IdentityAuthorityReleaseOutcome::NotConfigured,
499 }
500 }
501
502 #[test]
503 fn shutdown_cleanup_attestation_requires_every_authority_boundary() {
504 let mut report = completed_shutdown_report();
505 assert!(report.cleanup_completed());
506
507 report.identity_authority_release =
508 IdentityAuthorityReleaseOutcome::Released { grant_count: 1 };
509 assert!(report.cleanup_completed());
510
511 let mut report = completed_shutdown_report();
512 report.drain.timed_out = true;
513 assert!(!report.cleanup_completed());
514
515 let mut report = completed_shutdown_report();
516 report.mob_stop = Err(MobRuntimeError::InvalidConfig(
517 "mob stop failed".to_string(),
518 ));
519 assert!(!report.cleanup_completed());
520
521 let mut report = completed_shutdown_report();
522 report.identity_authority_release = IdentityAuthorityReleaseOutcome::Failed {
523 error: "provider release failed".to_string(),
524 };
525 assert!(!report.cleanup_completed());
526
527 let mut report = completed_shutdown_report();
528 report.identity_authority_release = IdentityAuthorityReleaseOutcome::SkippedMobStopFailed;
529 assert!(!report.cleanup_completed());
530
531 let mut report = completed_shutdown_report();
532 report.identity_authority_release =
533 IdentityAuthorityReleaseOutcome::SkippedResetCleanupFailed {
534 error: "superseded member retained".to_string(),
535 };
536 assert!(!report.cleanup_completed());
537
538 let mut report = completed_shutdown_report();
539 report.module_shutdown.orphan_processes = 1;
540 assert!(!report.cleanup_completed());
541 }
542}