meerkat_mobkit/unified_runtime/
lifecycle.rs1use std::future::Future;
4use std::future::IntoFuture;
5use std::sync::atomic::Ordering;
6use std::time::Duration;
7
8use meerkat_mob::SpawnMemberSpec;
9use serde_json::json;
10use tokio::runtime::RuntimeFlavor;
11use tokio::sync::mpsc::error::TryRecvError;
12
13use crate::mob_handle_runtime::{MobRuntimeError, send_message_on_mob};
14use crate::runtime::{
15 MobkitRuntimeHandle, RuntimeDecisionState, ScheduleDefinition, ScheduleDispatchReport,
16 ScheduleValidationError,
17};
18use crate::types::{EventEnvelope, ModuleEvent, UnifiedEvent};
19
20use super::types::{
21 RediscoverReport, ShutdownDrainReport, UnifiedRuntimeError, UnifiedRuntimeRunReport,
22 UnifiedRuntimeShutdownReport,
23};
24use super::{MobEventIngress, UnifiedRuntime, discovery_spec_to_spawn_spec};
25
26impl UnifiedRuntime {
27 pub async fn rediscover(&self) -> Result<Option<RediscoverReport>, MobRuntimeError> {
40 match self.rediscover_inner().await {
41 Ok(report) => Ok(report),
42 Err(err) => {
43 self.fire_error(super::types::ErrorEvent::RediscoverFailure {
44 error: format!("{err}"),
45 });
46 Err(err)
47 }
48 }
49 }
50
51 async fn rediscover_inner(&self) -> Result<Option<RediscoverReport>, MobRuntimeError> {
52 let discovery = match &self.discovery {
53 Some(d) => d,
54 None => return Ok(None),
55 };
56
57 self.mob_runtime
59 .handle()
60 .reset()
61 .await
62 .map_err(MobRuntimeError::Mob)?;
63
64 let specs = discovery.discover(serde_json::Value::Null).await;
66 let spawn_specs: Vec<SpawnMemberSpec> =
67 specs.iter().map(discovery_spec_to_spawn_spec).collect();
68 let spawned: Vec<String> = spawn_specs.iter().map(|s| s.identity.to_string()).collect();
69
70 self.spawn_many(spawn_specs).await?;
72
73 self.managed_dynamic_edges.write().await.clear();
75
76 let edges = self.reconcile_edges().await;
78
79 Ok(Some(RediscoverReport { spawned, edges }))
80 }
81
82 pub async fn run<F>(
83 &self,
84 listener: tokio::net::TcpListener,
85 decisions: RuntimeDecisionState,
86 shutdown_signal: F,
87 ) -> UnifiedRuntimeRunReport
88 where
89 F: Future<Output = ()> + Send + 'static,
90 {
91 let app = self.build_reference_app_router(decisions);
92 let serve = axum::serve(listener, app)
93 .with_graceful_shutdown(shutdown_signal)
94 .into_future();
95 tokio::pin!(serve);
96 let serve_result = loop {
97 tokio::select! {
98 result = &mut serve => break result,
99 () = tokio::time::sleep(Duration::from_millis(25)) => {
100 let _ = self.drain_mob_agent_events().await;
101 }
102 }
103 };
104 let shutdown = self.shutdown().await;
105 UnifiedRuntimeRunReport {
106 serve_result,
107 shutdown,
108 }
109 }
110
111 pub async fn serve(
112 &self,
113 listener: tokio::net::TcpListener,
114 decisions: RuntimeDecisionState,
115 ) -> std::io::Result<()> {
116 let app = self.build_reference_app_router(decisions);
117 let serve = axum::serve(listener, app).into_future();
118 tokio::pin!(serve);
119 loop {
120 tokio::select! {
121 result = &mut serve => break result,
122 () = tokio::time::sleep(Duration::from_millis(25)) => {
123 let _ = self.drain_mob_agent_events().await;
124 }
125 }
126 }
127 }
128
129 pub fn spawn_event_drain_task(self: std::sync::Arc<Self>) -> tokio::task::JoinHandle<()> {
139 tokio::spawn(async move {
140 loop {
141 tokio::time::sleep(Duration::from_millis(25)).await;
142 if self.shutting_down.load(Ordering::SeqCst) {
143 break;
144 }
145 if let Err(err) = self.drain_mob_agent_events().await {
146 if matches!(err, UnifiedRuntimeError::RuntimeShuttingDown) {
147 break;
148 }
149 tracing::warn!(error = %err, "mob agent event drain tick failed");
152 }
153 }
154 })
155 }
156
157 pub async fn shutdown(&self) -> UnifiedRuntimeShutdownReport {
158 self.shutting_down.store(true, Ordering::SeqCst);
159 if let Some(task) = self.implicit_delegate_retirement_task.lock().await.take() {
160 task.abort();
161 }
162 if let Some(task) = self.identity_lease_renewal_task.lock().await.take() {
163 task.abort();
164 }
165
166 let drain_start = std::time::Instant::now();
168 let mut drained_count = 0_usize;
169 let drain_result = tokio::time::timeout(self.drain_timeout, async {
170 loop {
171 if self.drain_mob_agent_events().await.is_err() {
172 break;
173 }
174 let ingress = self.mob_event_ingress.lock().await;
175 if ingress.is_none() {
176 break;
177 }
178 drop(ingress);
179 drained_count += 1;
180 tokio::time::sleep(Duration::from_millis(50)).await;
181 if drained_count > 1 {
182 break;
183 }
184 }
185 })
186 .await;
187 let drain = ShutdownDrainReport {
188 drained_count,
189 timed_out: drain_result.is_err(),
190 drain_duration_ms: drain_start.elapsed().as_millis() as u64,
191 };
192
193 let mob_stop = self
197 .mob_handle()
198 .stop()
199 .await
200 .map_err(MobRuntimeError::from);
201
202 self.close_event_router().await;
204
205 let module_shutdown = self.module_runtime.lock().await.shutdown();
207 UnifiedRuntimeShutdownReport {
208 drain,
209 module_shutdown,
210 mob_stop,
211 }
212 }
213
214 pub async fn drain_mob_agent_events(&self) -> Result<(), UnifiedRuntimeError> {
221 let mut disconnected = false;
222 let mut ingress_guard = match self.mob_event_ingress.try_lock() {
223 Ok(guard) => guard,
224 Err(_) => {
225 return Ok(());
229 }
230 };
231 let ingress = match ingress_guard.as_mut() {
232 Some(i) => i,
233 None => return Ok(()),
234 };
235
236 loop {
237 match Self::try_recv_ingress_event(ingress) {
238 Some(Ok(unified_event)) => {
239 if let crate::types::UnifiedEvent::Agent {
241 ref agent_id,
242 ref event_type,
243 ..
244 } = unified_event.event
245 && event_type == "run_failed"
246 {
247 self.fire_error(super::types::ErrorEvent::HostLoopCrash {
248 member_id: agent_id.clone(),
249 error: format!(
250 "agent run failed (event_id: {})",
251 unified_event.event_id
252 ),
253 });
254 }
255 self.ingest_event(&unified_event);
257 self.project_console_event_from_unified(&unified_event)
258 .await;
259 self.module_runtime
260 .lock()
261 .await
262 .append_normalized_event(unified_event)?;
263 }
264 Some(Err(TryRecvError::Empty)) => break,
265 Some(Err(TryRecvError::Disconnected)) => {
266 disconnected = true;
267 break;
268 }
269 None => break,
270 }
271 }
272
273 if disconnected {
274 *ingress_guard = None;
275 }
276
277 Ok(())
278 }
279
280 pub(super) async fn close_event_router(&self) {
281 let ingress = self.mob_event_ingress.lock().await.take();
282 match ingress {
283 Some(MobEventIngress::Forwarder(forwarder)) => {
284 let task = forwarder.task;
285 task.abort();
286 let _ = task.await;
287 }
288 None => {}
289 }
290
291 if let Some(task) = self.mob_events_subscriber_task.lock().await.take() {
293 task.abort();
294 let _ = task.await;
295 }
296 }
297
298 fn try_recv_ingress_event(
299 ingress: &mut MobEventIngress,
300 ) -> Option<Result<EventEnvelope<UnifiedEvent>, TryRecvError>> {
301 Some(match ingress {
302 MobEventIngress::Forwarder(forwarder) => forwarder.event_rx.try_recv(),
303 })
304 }
305
306 pub async fn dispatch_schedule_tick(
307 &self,
308 schedules: &[ScheduleDefinition],
309 tick_ms: u64,
310 ) -> Result<ScheduleDispatchReport, UnifiedRuntimeError> {
311 if self.shutting_down.load(Ordering::SeqCst) {
312 return Err(UnifiedRuntimeError::RuntimeShuttingDown);
313 }
314 let mut dispatch_report = self
315 .dispatch_schedule_tick_blocking(schedules, tick_ms)
316 .await?;
317
318 for dispatch in &mut dispatch_report.dispatched {
319 let Some(runtime_injection) = dispatch.runtime_injection.clone() else {
320 continue;
321 };
322
323 let injection_result = send_message_on_mob(
324 &self.mob_handle(),
325 &runtime_injection.member_id,
326 runtime_injection.message.clone(),
327 )
328 .await;
329
330 match injection_result {
331 Ok(session_id) => {
332 self.module_runtime
333 .lock()
334 .await
335 .append_normalized_event(EventEnvelope {
336 event_id: format!("{}-executed", runtime_injection.injection_event_id),
337 source: "module".to_string(),
338 timestamp_ms: dispatch.tick_ms,
339 event: UnifiedEvent::Module(ModuleEvent {
340 module: "runtime".to_string(),
341 event_type: "runtime.injection.executed".to_string(),
342 payload: json!({
343 "schedule_id": dispatch.schedule_id.clone(),
344 "claim_key": dispatch.claim_key.clone(),
345 "member_id": runtime_injection.member_id,
346 "message": runtime_injection.message,
347 "session_id": session_id,
348 }),
349 }),
350 })?;
351 }
352 Err(error) => {
353 dispatch.runtime_injection_error =
354 Some(format!("mob injection failed: {error}"));
355 self.module_runtime
356 .lock()
357 .await
358 .append_normalized_event(EventEnvelope {
359 event_id: format!("{}-failed", runtime_injection.injection_event_id),
360 source: "module".to_string(),
361 timestamp_ms: dispatch.tick_ms,
362 event: UnifiedEvent::Module(ModuleEvent {
363 module: "runtime".to_string(),
364 event_type: "runtime.injection.failed".to_string(),
365 payload: json!({
366 "schedule_id": dispatch.schedule_id.clone(),
367 "claim_key": dispatch.claim_key.clone(),
368 "member_id": runtime_injection.member_id,
369 "message": runtime_injection.message,
370 "error_kind": "mob_runtime",
371 "error": format!("mob injection failed: {error}"),
372 }),
373 }),
374 })?;
375 }
376 }
377 }
378
379 self.drain_mob_agent_events().await?;
380 Ok(dispatch_report)
381 }
382
383 async fn dispatch_schedule_tick_blocking(
384 &self,
385 schedules: &[ScheduleDefinition],
386 tick_ms: u64,
387 ) -> Result<ScheduleDispatchReport, UnifiedRuntimeError> {
388 let mut rt = self.module_runtime.lock().await;
389
390 let dispatch_result = if tokio::runtime::Handle::try_current()
391 .is_ok_and(|handle| handle.runtime_flavor() == RuntimeFlavor::MultiThread)
392 {
393 tokio::task::block_in_place(|| {
394 Self::dispatch_schedule_tick_in_joined_thread(&mut rt, schedules, tick_ms)
395 })
396 } else {
397 Self::dispatch_schedule_tick_in_joined_thread(&mut rt, schedules, tick_ms)
398 };
399
400 dispatch_result
401 .map_err(|_| UnifiedRuntimeError::ScheduleDispatchThreadPanicked)?
402 .map_err(UnifiedRuntimeError::ScheduleValidation)
403 }
404
405 fn dispatch_schedule_tick_in_joined_thread(
406 module_runtime: &mut MobkitRuntimeHandle,
407 schedules: &[ScheduleDefinition],
408 tick_ms: u64,
409 ) -> std::thread::Result<Result<ScheduleDispatchReport, ScheduleValidationError>> {
410 std::thread::scope(|scope| {
411 scope
412 .spawn(move || module_runtime.dispatch_schedule_tick(schedules, tick_ms))
413 .join()
414 })
415 }
416}