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 runner.execution_finalized = true;
235 true
236}
237
238pub async fn finalize_rejected_runner_if_distinct(
244 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
245 session_id: &str,
246 existing_owner_run_id: &str,
247 attempted_run_id: &str,
248 result: &Result<(), AgentError>,
249) -> bool {
250 if existing_owner_run_id == attempted_run_id {
251 return false;
252 }
253 finalize_runner_exact(runners, session_id, attempted_run_id, result).await
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[tokio::test]
261 async fn cancelling_removal_keeps_the_predecessor_fence_discoverable() {
262 use std::sync::Barrier;
263 let mut runner = AgentRunner::new();
264 runner.status = AgentStatus::Running;
265 let cancel = runner.cancel_token.clone();
266 let publication = runner.event_publication.clone();
267 let entered = Arc::new(Barrier::new(2));
268 let release = Arc::new(Barrier::new(2));
269 let publisher = {
270 let publication = publication.clone();
271 let entered = entered.clone();
272 let release = release.clone();
273 std::thread::spawn(move || {
274 publication.publish(|| {
275 entered.wait();
276 release.wait();
277 })
278 })
279 };
280 entered.wait();
281 let mut runners = HashMap::from([("child".to_string(), runner)]);
282 let mut removing = Box::pin(remove_runner_entry(&mut runners, "child"));
283 assert!(futures::poll!(removing.as_mut()).is_pending());
284 drop(removing);
285 assert!(
286 cancel.is_cancelled(),
287 "retired execution must be told to stop even when removal is cancelled"
288 );
289 assert!(
290 runners.contains_key("child"),
291 "a successor must still find the old fence after cancellation"
292 );
293 assert!(!publication.publish(|| panic!("retired run must reject late frames")));
294 release.wait();
295 publisher.join().unwrap();
296 assert!(remove_runner_entry(&mut runners, "child").await.is_some());
297 assert!(runners.is_empty());
298 }
299
300 fn new_runners() -> Arc<RwLock<HashMap<String, AgentRunner>>> {
301 Arc::new(RwLock::new(HashMap::new()))
302 }
303
304 fn new_senders() -> Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>> {
305 Arc::new(RwLock::new(HashMap::new()))
306 }
307
308 fn new_broadcaster() -> broadcast::Sender<AgentEvent> {
309 broadcast::channel(100).0
310 }
311
312 #[tokio::test]
313 async fn try_reserve_runner_creates_runner_with_running_status() {
314 let runners = new_runners();
315 let senders = new_senders();
316 let tx = new_broadcaster();
317 let token = try_reserve_runner(&runners, &senders, "s1", &tx).await;
318 assert!(token.is_some());
319
320 let guard = runners.read().await;
321 let runner = guard.get("s1").unwrap();
322 assert!(matches!(runner.status, AgentStatus::Running));
323 }
324
325 #[tokio::test]
326 async fn try_reserve_runner_returns_none_when_already_running() {
327 let runners = new_runners();
328 let senders = new_senders();
329 let tx = new_broadcaster();
330 let _ = try_reserve_runner(&runners, &senders, "s1", &tx).await;
331 let second = try_reserve_runner(&runners, &senders, "s1", &tx).await;
332 assert!(second.is_none());
333 }
334
335 #[tokio::test]
336 async fn try_reserve_runner_replaces_completed_runner() {
337 let runners = new_runners();
338 let senders = new_senders();
339 let tx = new_broadcaster();
340 let _ = try_reserve_runner(&runners, &senders, "s1", &tx).await;
341
342 {
343 let mut guard = runners.write().await;
344 let runner = guard.get_mut("s1").unwrap();
345 runner.status = AgentStatus::Completed;
346 }
347
348 let second = try_reserve_runner(&runners, &senders, "s1", &tx).await;
349 assert!(second.is_some());
350 }
351
352 #[tokio::test]
353 async fn rejected_duplicate_does_not_terminalize_shared_owner_slot() {
354 let runners = new_runners();
355 let senders = new_senders();
356 let tx = new_broadcaster();
357 let reservation = try_reserve_runner(&runners, &senders, "s1", &tx)
358 .await
359 .unwrap();
360 let rejected = Err(AgentError::Cancelled);
361
362 assert!(
363 !finalize_rejected_runner_if_distinct(
364 &runners,
365 "s1",
366 &reservation.run_id,
367 &reservation.run_id,
368 &rejected,
369 )
370 .await
371 );
372 assert!(matches!(
373 runners.read().await.get("s1").map(|runner| &runner.status),
374 Some(AgentStatus::Running)
375 ));
376 }
377
378 #[tokio::test]
379 async fn rejected_distinct_runner_releases_only_its_exact_slot() {
380 let runners = new_runners();
381 let senders = new_senders();
382 let tx = new_broadcaster();
383 let reservation = try_reserve_runner(&runners, &senders, "s1", &tx)
384 .await
385 .unwrap();
386 let rejected = Err(AgentError::Cancelled);
387
388 assert!(
389 finalize_rejected_runner_if_distinct(
390 &runners,
391 "s1",
392 "different-live-owner",
393 &reservation.run_id,
394 &rejected,
395 )
396 .await
397 );
398 assert!(matches!(
399 runners.read().await.get("s1").map(|runner| &runner.status),
400 Some(AgentStatus::Cancelled)
401 ));
402 }
403
404 #[tokio::test]
405 async fn try_reserve_runner_reasserts_evicted_sender_so_late_subscriber_receives() {
406 use super::super::session_events::get_or_create_event_sender;
412 use bamboo_agent_core::AgentEvent;
413
414 let runners = new_runners();
415 let senders = new_senders();
416
417 let session_tx = get_or_create_event_sender(&senders, "s1").await;
419 senders.write().await.remove("s1");
421 assert!(senders.read().await.get("s1").is_none());
422
423 let reservation = try_reserve_runner(&runners, &senders, "s1", &session_tx).await;
425 assert!(reservation.is_some(), "reservation must succeed");
426
427 assert!(
430 senders.read().await.get("s1").is_some(),
431 "reservation must re-assert the evicted session sender into the map"
432 );
433
434 let subscriber_tx = get_or_create_event_sender(&senders, "s1").await;
437 let mut rx = subscriber_tx.subscribe();
438
439 let _ = session_tx.send(AgentEvent::SessionDeleted {
441 session_id: "s1".to_string(),
442 });
443
444 let received = tokio::time::timeout(std::time::Duration::from_millis(500), rx.recv()).await;
445 assert!(
446 matches!(received, Ok(Ok(_))),
447 "late subscriber must receive events from the resumed run; without the \
448 re-assert, get_or_create mints a fresh channel and the event is lost"
449 );
450 }
451
452 #[tokio::test]
453 async fn cancellation_before_atomic_registry_commit_leaves_no_zombie_runner() {
454 let runners = new_runners();
455 let senders = new_senders();
456 let tx = new_broadcaster();
457 let held_senders = senders.write().await;
458
459 let task = {
460 let runners = runners.clone();
461 let senders = senders.clone();
462 let tx = tx.clone();
463 tokio::spawn(
464 async move { reserve_runner_core(&runners, &senders, "cancelled", &tx).await },
465 )
466 };
467
468 for _ in 0..100 {
472 if runners.try_write().is_err() {
473 break;
474 }
475 tokio::task::yield_now().await;
476 }
477 assert!(
478 runners.try_write().is_err(),
479 "reservation never reached the sender-lock barrier"
480 );
481 task.abort();
482 assert!(task.await.unwrap_err().is_cancelled());
483 drop(held_senders);
484
485 assert!(
486 runners.read().await.get("cancelled").is_none(),
487 "a cancelled reservation must not leave a Running slot without a task"
488 );
489 assert!(matches!(
490 reserve_runner_core(&runners, &senders, "cancelled", &tx).await,
491 ReserveOutcome::Reserved(_)
492 ));
493 }
494
495 #[test]
496 fn status_from_execution_result_maps_correctly() {
497 let ok_result: Result<(), AgentError> = Ok(());
498 assert!(matches!(
499 status_from_execution_result(&ok_result),
500 AgentStatus::Completed
501 ));
502
503 let cancelled: Result<(), AgentError> = Err(AgentError::Cancelled);
508 assert!(matches!(
509 status_from_execution_result(&cancelled),
510 AgentStatus::Cancelled
511 ));
512
513 let failed: Result<(), AgentError> = Err(AgentError::LLM("network error".to_string()));
514 match status_from_execution_result(&failed) {
515 AgentStatus::Error(message) => assert!(message.contains("network error")),
516 other => panic!("unexpected status: {other:?}"),
517 }
518 }
519}