bamboo_engine/runtime/execution/
runner_lifecycle.rs1use std::collections::HashMap;
9use std::sync::Arc;
10
11use chrono::Utc;
12use tokio::sync::{broadcast, RwLock};
13use tokio_util::sync::CancellationToken;
14
15use bamboo_agent_core::{AgentError, AgentEvent};
16
17use super::runner_state::{AgentRunner, AgentStatus};
18
19#[derive(Debug, Clone)]
21pub struct RunnerReservation {
22 pub cancel_token: CancellationToken,
23 pub run_id: String,
24 pub(super) physical_admission: Option<super::root_execution_lifetime::RootRunAdmission>,
25}
26
27#[derive(Debug, Clone)]
29pub enum ReserveOutcome {
30 Reserved(RunnerReservation),
32 AlreadyRunning(String),
35}
36
37pub async fn reserve_runner_core(
60 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
61 senders: &Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
62 session_id: &str,
63 event_sender: &broadcast::Sender<AgentEvent>,
64) -> ReserveOutcome {
65 reserve_runner_inner(runners, senders, session_id, event_sender, None).await
66}
67
68pub(super) async fn reserve_runner_for_registration(
71 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
72 senders: &Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
73 session_id: &str,
74 event_sender: &broadcast::Sender<AgentEvent>,
75 identity: RunnerReservation,
76) -> ReserveOutcome {
77 reserve_runner_inner(runners, senders, session_id, event_sender, Some(identity)).await
78}
79
80async fn reserve_runner_inner(
81 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
82 senders: &Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
83 session_id: &str,
84 event_sender: &broadcast::Sender<AgentEvent>,
85 identity: Option<RunnerReservation>,
86) -> ReserveOutcome {
87 let mut runners_guard = runners.write().await;
88 if let Some(runner) = runners_guard.get(session_id) {
89 if matches!(runner.status, AgentStatus::Running) {
90 return ReserveOutcome::AlreadyRunning(runner.run_id.clone());
91 }
92 }
93
94 let mut senders_guard = senders.write().await;
98 let mut runner = AgentRunner::new();
99 if let Some(identity) = identity {
100 runner.run_id = identity.run_id;
101 runner.cancel_token = identity.cancel_token;
102 }
103 if let Some(previous) = runners_guard.get(session_id) {
104 runner.root_execution = previous.root_execution.clone();
105 }
106 let physical_admission = match runner.root_execution.admit(&runner.run_id) {
110 Ok(admission) => admission,
111 Err(_) => {
112 return ReserveOutcome::AlreadyRunning(
113 runners_guard
114 .get(session_id)
115 .map_or_else(|| runner.run_id.clone(), |previous| previous.run_id.clone()),
116 )
117 }
118 };
119 if let Some(previous) = runners_guard.get(session_id) {
120 previous.event_publication.retire().await;
121 }
122 runner.transfer_root_slot(runners_guard.get_mut(session_id));
123 runners_guard.remove(session_id);
124
125 runner.status = AgentStatus::Running;
126 runner.event_sender = event_sender.clone();
127 let reservation = RunnerReservation {
128 cancel_token: runner.cancel_token.clone(),
129 run_id: runner.run_id.clone(),
130 physical_admission: Some(physical_admission),
131 };
132 runners_guard.insert(session_id.to_string(), runner);
133
134 senders_guard
139 .entry(session_id.to_string())
140 .or_insert_with(|| event_sender.clone());
141
142 ReserveOutcome::Reserved(reservation)
143}
144
145pub async fn try_reserve_runner(
158 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
159 senders: &Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
160 session_id: &str,
161 event_sender: &broadcast::Sender<AgentEvent>,
162) -> Option<RunnerReservation> {
163 match reserve_runner_core(runners, senders, session_id, event_sender).await {
164 ReserveOutcome::Reserved(reservation) => Some(reservation),
165 ReserveOutcome::AlreadyRunning(_) => {
166 tracing::debug!("[{}] Runner already running, skipping", session_id);
167 None
168 }
169 }
170}
171
172pub async fn remove_runner_entry(
176 runners: &mut HashMap<String, AgentRunner>,
177 session_id: &str,
178) -> Option<AgentRunner> {
179 if let Some(runner) = runners.get(session_id) {
180 if runner.root_reset_admission_reserved() {
181 return None;
182 }
183 runner.cancel_token.cancel();
187 runner.event_publication.retire().await;
188 }
189 runners.remove(session_id)
190}
191
192pub fn status_from_execution_result(result: &Result<(), AgentError>) -> AgentStatus {
194 match result {
195 Ok(_) => AgentStatus::Completed,
196 Err(error) if error.is_cancelled() => AgentStatus::Cancelled,
197 Err(error) => AgentStatus::Error(error.to_string()),
198 }
199}
200
201pub async fn finalize_runner(
203 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
204 session_id: &str,
205 result: &Result<(), AgentError>,
206) {
207 let mut guard = runners.write().await;
208 if let Some(runner) = guard.get_mut(session_id) {
209 runner.status = status_from_execution_result(result);
210 runner.completed_at = Some(Utc::now());
211 }
212}
213
214pub async fn finalize_runner_exact(
220 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
221 session_id: &str,
222 run_id: &str,
223 result: &Result<(), AgentError>,
224) -> bool {
225 let mut guard = runners.write().await;
226 let Some(runner) = guard.get_mut(session_id) else {
227 return false;
228 };
229 if runner.run_id != run_id {
230 return false;
231 }
232 runner.status = status_from_execution_result(result);
233 runner.completed_at = Some(Utc::now());
234 true
235}
236
237pub async fn finalize_rejected_runner_if_distinct(
243 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
244 session_id: &str,
245 existing_owner_run_id: &str,
246 attempted_run_id: &str,
247 result: &Result<(), AgentError>,
248) -> bool {
249 if existing_owner_run_id == attempted_run_id {
250 return false;
251 }
252 finalize_runner_exact(runners, session_id, attempted_run_id, result).await
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[tokio::test]
260 async fn cancelling_removal_keeps_the_predecessor_fence_discoverable() {
261 use std::sync::Barrier;
262 let mut runner = AgentRunner::new();
263 runner.status = AgentStatus::Running;
264 let cancel = runner.cancel_token.clone();
265 let publication = runner.event_publication.clone();
266 let entered = Arc::new(Barrier::new(2));
267 let release = Arc::new(Barrier::new(2));
268 let publisher = {
269 let publication = publication.clone();
270 let entered = entered.clone();
271 let release = release.clone();
272 std::thread::spawn(move || {
273 publication.publish(|| {
274 entered.wait();
275 release.wait();
276 })
277 })
278 };
279 entered.wait();
280 let mut runners = HashMap::from([("child".to_string(), runner)]);
281 let mut removing = Box::pin(remove_runner_entry(&mut runners, "child"));
282 assert!(futures::poll!(removing.as_mut()).is_pending());
283 drop(removing);
284 assert!(
285 cancel.is_cancelled(),
286 "retired execution must be told to stop even when removal is cancelled"
287 );
288 assert!(
289 runners.contains_key("child"),
290 "a successor must still find the old fence after cancellation"
291 );
292 assert!(!publication.publish(|| panic!("retired run must reject late frames")));
293 release.wait();
294 publisher.join().unwrap();
295 assert!(remove_runner_entry(&mut runners, "child").await.is_some());
296 assert!(runners.is_empty());
297 }
298
299 fn new_runners() -> Arc<RwLock<HashMap<String, AgentRunner>>> {
300 Arc::new(RwLock::new(HashMap::new()))
301 }
302
303 fn new_senders() -> Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>> {
304 Arc::new(RwLock::new(HashMap::new()))
305 }
306
307 fn new_broadcaster() -> broadcast::Sender<AgentEvent> {
308 broadcast::channel(100).0
309 }
310
311 #[tokio::test]
312 async fn try_reserve_runner_creates_runner_with_running_status() {
313 let runners = new_runners();
314 let senders = new_senders();
315 let tx = new_broadcaster();
316 let token = try_reserve_runner(&runners, &senders, "s1", &tx).await;
317 assert!(token.is_some());
318
319 let guard = runners.read().await;
320 let runner = guard.get("s1").unwrap();
321 assert!(matches!(runner.status, AgentStatus::Running));
322 }
323
324 #[tokio::test]
325 async fn try_reserve_runner_returns_none_when_already_running() {
326 let runners = new_runners();
327 let senders = new_senders();
328 let tx = new_broadcaster();
329 let _ = try_reserve_runner(&runners, &senders, "s1", &tx).await;
330 let second = try_reserve_runner(&runners, &senders, "s1", &tx).await;
331 assert!(second.is_none());
332 }
333
334 #[tokio::test]
335 async fn try_reserve_runner_replaces_completed_runner() {
336 let runners = new_runners();
337 let senders = new_senders();
338 let tx = new_broadcaster();
339 let _ = try_reserve_runner(&runners, &senders, "s1", &tx).await;
340
341 {
342 let mut guard = runners.write().await;
343 let runner = guard.get_mut("s1").unwrap();
344 runner.status = AgentStatus::Completed;
345 }
346
347 let second = try_reserve_runner(&runners, &senders, "s1", &tx).await;
348 assert!(second.is_some());
349 }
350
351 #[tokio::test]
352 async fn rejected_duplicate_does_not_terminalize_shared_owner_slot() {
353 let runners = new_runners();
354 let senders = new_senders();
355 let tx = new_broadcaster();
356 let reservation = try_reserve_runner(&runners, &senders, "s1", &tx)
357 .await
358 .unwrap();
359 let rejected = Err(AgentError::Cancelled);
360
361 assert!(
362 !finalize_rejected_runner_if_distinct(
363 &runners,
364 "s1",
365 &reservation.run_id,
366 &reservation.run_id,
367 &rejected,
368 )
369 .await
370 );
371 assert!(matches!(
372 runners.read().await.get("s1").map(|runner| &runner.status),
373 Some(AgentStatus::Running)
374 ));
375 }
376
377 #[tokio::test]
378 async fn rejected_distinct_runner_releases_only_its_exact_slot() {
379 let runners = new_runners();
380 let senders = new_senders();
381 let tx = new_broadcaster();
382 let reservation = try_reserve_runner(&runners, &senders, "s1", &tx)
383 .await
384 .unwrap();
385 let rejected = Err(AgentError::Cancelled);
386
387 assert!(
388 finalize_rejected_runner_if_distinct(
389 &runners,
390 "s1",
391 "different-live-owner",
392 &reservation.run_id,
393 &rejected,
394 )
395 .await
396 );
397 assert!(matches!(
398 runners.read().await.get("s1").map(|runner| &runner.status),
399 Some(AgentStatus::Cancelled)
400 ));
401 }
402
403 #[tokio::test]
404 async fn try_reserve_runner_reasserts_evicted_sender_so_late_subscriber_receives() {
405 use super::super::session_events::get_or_create_event_sender;
411 use bamboo_agent_core::AgentEvent;
412
413 let runners = new_runners();
414 let senders = new_senders();
415
416 let session_tx = get_or_create_event_sender(&senders, "s1").await;
418 senders.write().await.remove("s1");
420 assert!(senders.read().await.get("s1").is_none());
421
422 let reservation = try_reserve_runner(&runners, &senders, "s1", &session_tx).await;
424 assert!(reservation.is_some(), "reservation must succeed");
425
426 assert!(
429 senders.read().await.get("s1").is_some(),
430 "reservation must re-assert the evicted session sender into the map"
431 );
432
433 let subscriber_tx = get_or_create_event_sender(&senders, "s1").await;
436 let mut rx = subscriber_tx.subscribe();
437
438 let _ = session_tx.send(AgentEvent::SessionDeleted {
440 session_id: "s1".to_string(),
441 });
442
443 let received = tokio::time::timeout(std::time::Duration::from_millis(500), rx.recv()).await;
444 assert!(
445 matches!(received, Ok(Ok(_))),
446 "late subscriber must receive events from the resumed run; without the \
447 re-assert, get_or_create mints a fresh channel and the event is lost"
448 );
449 }
450
451 #[tokio::test]
452 async fn cancellation_before_atomic_registry_commit_leaves_no_zombie_runner() {
453 let runners = new_runners();
454 let senders = new_senders();
455 let tx = new_broadcaster();
456 let held_senders = senders.write().await;
457
458 let task = {
459 let runners = runners.clone();
460 let senders = senders.clone();
461 let tx = tx.clone();
462 tokio::spawn(
463 async move { reserve_runner_core(&runners, &senders, "cancelled", &tx).await },
464 )
465 };
466
467 for _ in 0..100 {
471 if runners.try_write().is_err() {
472 break;
473 }
474 tokio::task::yield_now().await;
475 }
476 assert!(
477 runners.try_write().is_err(),
478 "reservation never reached the sender-lock barrier"
479 );
480 task.abort();
481 assert!(task.await.unwrap_err().is_cancelled());
482 drop(held_senders);
483
484 assert!(
485 runners.read().await.get("cancelled").is_none(),
486 "a cancelled reservation must not leave a Running slot without a task"
487 );
488 assert!(matches!(
489 reserve_runner_core(&runners, &senders, "cancelled", &tx).await,
490 ReserveOutcome::Reserved(_)
491 ));
492 }
493
494 #[test]
495 fn status_from_execution_result_maps_correctly() {
496 let ok_result: Result<(), AgentError> = Ok(());
497 assert!(matches!(
498 status_from_execution_result(&ok_result),
499 AgentStatus::Completed
500 ));
501
502 let cancelled: Result<(), AgentError> = Err(AgentError::Cancelled);
507 assert!(matches!(
508 status_from_execution_result(&cancelled),
509 AgentStatus::Cancelled
510 ));
511
512 let failed: Result<(), AgentError> = Err(AgentError::LLM("network error".to_string()));
513 match status_from_execution_result(&failed) {
514 AgentStatus::Error(message) => assert!(message.contains("network error")),
515 other => panic!("unexpected status: {other:?}"),
516 }
517 }
518}