1#[cfg(not(test))]
4use std::collections::BTreeMap;
5use std::collections::{HashMap, HashSet, VecDeque};
6use std::sync::atomic::AtomicU64;
7use std::sync::{Arc, Condvar, Mutex, OnceLock};
8use std::thread;
9#[cfg(not(test))]
10use std::time::{Duration, Instant};
11
12#[cfg(not(test))]
13use agentos_bridge::queue_tracker::{warn_limit_exhausted, TrackedLimit};
14use agentos_bridge::{bridge_contract, BridgeCallConvention};
15use crossbeam_channel::{Receiver, Sender};
16
17use crate::execution;
18#[cfg(not(test))]
19use crate::host_call::{BridgeCallContext, ChannelRuntimeEventSender};
20use crate::host_call::{CallIdRouter, SharedCallIdCounter};
21use crate::ipc::ExecutionError;
22#[cfg(not(test))]
23use crate::ipc_binary::ExecutionErrorBin;
24use crate::runtime_protocol::{
25 BridgeResponse, RuntimeEvent, SessionMessage, StreamEvent, WarmSessionHint,
26};
27use crate::snapshot::{snapshot_cache_key, SnapshotCache, SnapshotCacheKey};
28#[cfg(not(test))]
29use crate::{bridge, isolate, snapshot};
30
31pub enum SessionCommand {
33 Shutdown,
35 Message(SessionMessage),
37 SetModuleReader(Box<dyn crate::execution::GuestModuleReader>),
42}
43
44#[cfg(not(test))]
45type SharedIsolateHandle = Arc<Mutex<Option<v8::IsolateHandle>>>;
46#[cfg(test)]
47type SharedIsolateHandle = Arc<Mutex<Option<()>>>;
48
49pub type RuntimeEventSender = crossbeam_channel::Sender<RuntimeEventEnvelope>;
51
52#[derive(Debug, Clone, PartialEq)]
53pub struct RuntimeEventEnvelope {
54 pub output_generation: Option<u64>,
55 pub event: RuntimeEvent,
56}
57
58const LATE_TERMINATE_EXECUTION_ERROR_CODE: &str = "ERR_LATE_TERMINATE_EXECUTION";
59const LATE_STREAM_EVENT_ERROR_CODE: &str = "ERR_LATE_STREAM_EVENT";
60const LATE_BRIDGE_RESPONSE_ERROR_CODE: &str = "ERR_LATE_BRIDGE_RESPONSE";
61const DEFERRED_COMMAND_LIMIT_ERROR_CODE: &str = "ERR_SESSION_DEFERRED_COMMAND_LIMIT";
62const SESSION_COMMAND_CHANNEL_CAPACITY: usize = 256;
63const MAX_DEFERRED_SESSION_COMMANDS: usize = SESSION_COMMAND_CHANNEL_CAPACITY;
64const MAX_DEFERRED_SYNC_MESSAGES: usize = SESSION_COMMAND_CHANNEL_CAPACITY;
65
66#[derive(Clone, Debug, Eq, Hash, PartialEq)]
67struct WarmPoolKey {
68 snapshot_key_digest: SnapshotCacheKey,
69 heap_limit_mb: u32,
70}
71
72struct ParkedWorker {
73 assignment_tx: Sender<SessionAssignment>,
74 join_handle: thread::JoinHandle<()>,
75}
76
77#[derive(Default)]
78struct WarmWorkerPoolState {
79 workers: HashMap<WarmPoolKey, Vec<ParkedWorker>>,
80 refilling: HashSet<WarmPoolKey>,
81}
82
83#[derive(Default)]
84struct WarmWorkerPool {
85 state: Mutex<WarmWorkerPoolState>,
86}
87
88struct SessionAssignment {
89 heap_limit_mb: Option<u32>,
90 cpu_time_limit_ms: Option<u32>,
91 wall_clock_limit_ms: Option<u32>,
92 rx: Receiver<SessionCommand>,
93 slot_control: SlotControl,
94 max_concurrency: usize,
95 event_tx: RuntimeEventSender,
96 call_id_router: CallIdRouter,
97 shared_call_id: SharedCallIdCounter,
98 snapshot_cache: Arc<SnapshotCache>,
99 isolate_handle: SharedIsolateHandle,
100 execution_abort: SharedExecutionAbort,
101 session_id: String,
102 output_generation: Option<u64>,
103}
104
105#[cfg(not(test))]
106struct PrecreatedIsolate {
107 isolate: v8::OwnedIsolate,
108 context: v8::Global<v8::Context>,
109 bridge_code: String,
110 userland_code: String,
111}
112
113#[cfg(test)]
114struct PrecreatedIsolate;
115
116#[cfg(not(test))]
117#[derive(Default)]
118struct V8SessionPhaseStats {
119 calls: u64,
120 total_ns: u128,
121 max_ns: u128,
122}
123
124#[cfg(not(test))]
125static V8_SESSION_PHASES: OnceLock<Mutex<BTreeMap<String, V8SessionPhaseStats>>> = OnceLock::new();
126
127#[cfg(not(test))]
128fn v8_session_phases_enabled() -> bool {
129 std::env::var("AGENTOS_V8_SESSION_PHASES").as_deref() == Ok("1")
130}
131
132#[cfg(not(test))]
133fn record_v8_session_phase(stage: &str, elapsed: Duration) {
134 if !v8_session_phases_enabled() {
135 return;
136 }
137 let phases = V8_SESSION_PHASES.get_or_init(|| Mutex::new(BTreeMap::new()));
138 let Ok(mut phases) = phases.lock() else {
139 return;
140 };
141 let stats = phases.entry(stage.to_string()).or_default();
142 stats.calls += 1;
143 let elapsed_ns = elapsed.as_nanos();
144 stats.total_ns += elapsed_ns;
145 stats.max_ns = stats.max_ns.max(elapsed_ns);
146
147 let Some(path) = std::env::var_os("AGENTOS_V8_SESSION_PHASES_FILE") else {
148 return;
149 };
150 let mut output = String::new();
151 for (stage, stats) in phases.iter() {
152 let total_us = stats.total_ns / 1_000;
153 let avg_us = if stats.calls == 0 {
154 0
155 } else {
156 total_us / u128::from(stats.calls)
157 };
158 let max_us = stats.max_ns / 1_000;
159 output.push_str(&format!(
160 "stage={stage} calls={} total_us={total_us} avg_us={avg_us} max_us={max_us}\n",
161 stats.calls
162 ));
163 }
164 let _ = std::fs::write(path, output);
165}
166
167#[cfg(not(test))]
168fn record_warm_worker_hit() {
169 record_v8_session_phase("warm_worker_hit", Duration::ZERO);
170}
171
172#[cfg(test)]
173fn record_warm_worker_hit() {}
174
175#[cfg(not(test))]
176fn record_warm_worker_miss() {
177 record_v8_session_phase("warm_worker_miss", Duration::ZERO);
178}
179
180#[cfg(test)]
181fn record_warm_worker_miss() {}
182
183fn warm_worker_capacity_per_key() -> usize {
184 std::env::var("AGENTOS_V8_WARM_ISOLATES")
185 .ok()
186 .and_then(|value| value.parse::<usize>().ok())
187 .unwrap_or(2)
188}
189
190fn effective_heap_limit_mb(heap_limit_mb: Option<u32>) -> u32 {
191 heap_limit_mb.unwrap_or(crate::isolate::DEFAULT_HEAP_LIMIT_MB)
192}
193
194fn warm_pool_key(
195 bridge_code: &str,
196 userland_code: &str,
197 heap_limit_mb: Option<u32>,
198) -> WarmPoolKey {
199 WarmPoolKey {
200 snapshot_key_digest: snapshot_cache_key(
201 bridge_code,
202 (!userland_code.is_empty()).then_some(userland_code),
203 ),
204 heap_limit_mb: effective_heap_limit_mb(heap_limit_mb),
205 }
206}
207
208fn warm_key_prefix(key: &WarmPoolKey) -> String {
209 key.snapshot_key_digest[..4]
210 .iter()
211 .map(|byte| format!("{byte:02x}"))
212 .collect()
213}
214
215impl WarmWorkerPool {
216 fn claim(&self, key: &WarmPoolKey) -> Option<ParkedWorker> {
217 let mut state = self.state.lock().expect("warm worker pool lock poisoned");
218 state.workers.get_mut(key).and_then(Vec::pop)
219 }
220
221 fn shutdown_handles(&self) -> Vec<thread::JoinHandle<()>> {
222 let mut state = self.state.lock().expect("warm worker pool lock poisoned");
223 state.refilling.clear();
224 state
225 .workers
226 .drain()
227 .flat_map(|(_, workers)| workers)
228 .map(|worker| {
229 drop(worker.assignment_tx);
230 worker.join_handle
231 })
232 .collect()
233 }
234
235 fn ensure_count(
236 self: &Arc<Self>,
237 snapshot_cache: Arc<SnapshotCache>,
238 slot_control: SlotControl,
239 bridge_code: String,
240 userland_code: String,
241 heap_limit_mb: Option<u32>,
242 requested_count: usize,
243 ) {
244 let capacity = warm_worker_capacity_per_key();
245 if capacity == 0 || requested_count == 0 {
246 return;
247 }
248
249 let target_count = requested_count.min(capacity);
250 let key = warm_pool_key(&bridge_code, &userland_code, heap_limit_mb);
251 {
252 let mut state = self.state.lock().expect("warm worker pool lock poisoned");
253 let current = state.workers.get(&key).map_or(0, Vec::len);
254 if current >= target_count || state.refilling.contains(&key) {
255 return;
256 }
257 state.refilling.insert(key.clone());
258 }
259
260 let pool = Arc::clone(self);
261 let spawn_key = key.clone();
262 let _ = thread::Builder::new()
263 .name(String::from("secure-exec-v8-warm-refill"))
264 .spawn(move || {
265 pool.refill_until(
266 snapshot_cache,
267 slot_control,
268 spawn_key,
269 bridge_code,
270 userland_code,
271 heap_limit_mb,
272 target_count,
273 );
274 })
275 .map_err(|error| {
276 eprintln!("agentos-v8-runtime: warm worker refill spawn failed: {error}");
277 self.state
278 .lock()
279 .expect("warm worker pool lock poisoned")
280 .refilling
281 .remove(&key);
282 });
283 }
284
285 #[allow(clippy::too_many_arguments)]
287 fn refill_until(
288 &self,
289 snapshot_cache: Arc<SnapshotCache>,
290 slot_control: SlotControl,
291 key: WarmPoolKey,
292 bridge_code: String,
293 userland_code: String,
294 heap_limit_mb: Option<u32>,
295 target_count: usize,
296 ) {
297 loop {
298 let capacity = warm_worker_capacity_per_key();
299 if capacity == 0 {
300 break;
301 }
302 let desired = target_count.min(capacity);
303 {
304 let state = self.state.lock().expect("warm worker pool lock poisoned");
305 if state.workers.get(&key).map_or(0, Vec::len) >= desired {
306 break;
307 }
308 }
309
310 let Some(worker) = spawn_warm_worker(
311 Arc::clone(&snapshot_cache),
312 Arc::clone(&slot_control),
313 key.clone(),
314 bridge_code.clone(),
315 userland_code.clone(),
316 heap_limit_mb,
317 ) else {
318 break;
319 };
320
321 let mut state = self.state.lock().expect("warm worker pool lock poisoned");
322 let workers = state.workers.entry(key.clone()).or_default();
323 if workers.len() >= desired {
324 drop(worker.assignment_tx);
325 let _ = worker.join_handle.join();
326 break;
327 }
328 workers.push(worker);
329 eprintln!(
330 "agentos-v8-runtime: warm worker refilled key={} heap={} pool_size={}",
331 warm_key_prefix(&key),
332 key.heap_limit_mb,
333 workers.len()
334 );
335 }
336
337 self.state
338 .lock()
339 .expect("warm worker pool lock poisoned")
340 .refilling
341 .remove(&key);
342 }
343}
344
345#[cfg(not(test))]
346fn spawn_warm_worker(
347 snapshot_cache: Arc<SnapshotCache>,
348 slot_control: SlotControl,
349 key: WarmPoolKey,
350 bridge_code: String,
351 userland_code: String,
352 heap_limit_mb: Option<u32>,
353) -> Option<ParkedWorker> {
354 let (assignment_tx, assignment_rx) = crossbeam_channel::bounded::<SessionAssignment>(1);
355 let (ready_tx, ready_rx) = crossbeam_channel::bounded::<Result<(), String>>(1);
356 let worker_bridge_code = bridge_code.clone();
357 let worker_userland_code = userland_code.clone();
358 let join_handle = match thread::Builder::new()
359 .name(String::from("secure-exec-v8-warm-worker"))
360 .spawn(move || {
361 let precreated = precreate_warm_isolate(
362 snapshot_cache,
363 slot_control,
364 worker_bridge_code,
365 worker_userland_code,
366 heap_limit_mb,
367 );
368 match precreated {
369 Ok(precreated) => {
370 let _ = ready_tx.send(Ok(()));
371 if let Ok(assignment) = assignment_rx.recv() {
372 session_thread(assignment, Some(precreated));
373 }
374 }
375 Err(error) => {
376 let _ = ready_tx.send(Err(error));
377 }
378 }
379 }) {
380 Ok(handle) => handle,
381 Err(error) => {
382 eprintln!("agentos-v8-runtime: warm worker spawn failed: {error}");
383 return None;
384 }
385 };
386
387 match ready_rx.recv() {
388 Ok(Ok(())) => Some(ParkedWorker {
389 assignment_tx,
390 join_handle,
391 }),
392 Ok(Err(error)) => {
393 eprintln!(
394 "agentos-v8-runtime: warm worker refill failed key={} heap={}: {error}",
395 warm_key_prefix(&key),
396 key.heap_limit_mb
397 );
398 let _ = join_handle.join();
399 None
400 }
401 Err(error) => {
402 eprintln!(
403 "agentos-v8-runtime: warm worker refill failed key={} heap={}: {error}",
404 warm_key_prefix(&key),
405 key.heap_limit_mb
406 );
407 let _ = join_handle.join();
408 None
409 }
410 }
411}
412
413#[cfg(test)]
414fn spawn_warm_worker(
415 _snapshot_cache: Arc<SnapshotCache>,
416 _slot_control: SlotControl,
417 _key: WarmPoolKey,
418 _bridge_code: String,
419 _userland_code: String,
420 _heap_limit_mb: Option<u32>,
421) -> Option<ParkedWorker> {
422 None
423}
424
425#[cfg(not(test))]
426fn precreate_warm_isolate(
427 snapshot_cache: Arc<SnapshotCache>,
428 slot_control: SlotControl,
429 bridge_code: String,
430 userland_code: String,
431 heap_limit_mb: Option<u32>,
432) -> Result<PrecreatedIsolate, String> {
433 isolate::init_v8_platform();
434 let snapshot_blob = snapshot_cache.get_or_create_with_userland(
435 &bridge_code,
436 (!userland_code.is_empty()).then_some(userland_code.as_str()),
437 )?;
438 let snapshot_blob = (*snapshot_blob).clone();
439 let _idle_slots = lock_idle_slots(&slot_control);
440 let mut isolate = snapshot::create_isolate_from_snapshot(snapshot_blob, heap_limit_mb);
441 isolate.set_host_import_module_dynamically_callback(execution::dynamic_import_callback);
442 isolate.set_host_initialize_import_meta_object_callback(execution::import_meta_object_callback);
443 let context = isolate::create_context(&mut isolate);
444 Ok(PrecreatedIsolate {
445 isolate,
446 context,
447 bridge_code,
448 userland_code,
449 })
450}
451
452#[cfg(not(test))]
453fn lock_idle_slots(slot_control: &SlotControl) -> std::sync::MutexGuard<'_, usize> {
454 let (lock, cvar) = &**slot_control;
455 let mut count = lock.lock().unwrap();
456 while *count != 0 {
457 count = cvar.wait(count).unwrap();
458 }
459 count
460}
461
462fn normalize_cpu_time_limit_ms(cpu_time_limit_ms: Option<u32>) -> Option<u32> {
468 cpu_time_limit_ms.filter(|budget_ms| *budget_ms > 0)
469}
470
471fn normalize_wall_clock_limit_ms(wall_clock_limit_ms: Option<u32>) -> Option<u32> {
477 wall_clock_limit_ms.filter(|limit_ms| *limit_ms > 0)
478}
479
480struct SessionEntry {
482 output_generation: Option<u64>,
484 tx: Sender<SessionCommand>,
486 join_handle: Option<thread::JoinHandle<()>>,
488 #[cfg_attr(test, allow(dead_code))]
490 isolate_handle: SharedIsolateHandle,
491 execution_abort: SharedExecutionAbort,
493}
494
495pub struct SessionShutdown {
502 session_id: String,
503 join_handle: Option<thread::JoinHandle<()>>,
504 call_id_router: CallIdRouter,
505}
506
507impl SessionShutdown {
508 pub fn finish(mut self) {
509 if let Some(handle) = self.join_handle.take() {
510 let _ = handle.join();
511 }
512 self.call_id_router
513 .lock()
514 .expect("call_id router lock poisoned")
515 .retain(|_, routed_session_id| routed_session_id != &self.session_id);
516 }
517}
518
519type SlotControl = Arc<(Mutex<usize>, Condvar)>;
521
522pub(crate) type DeferredQueue = Arc<Mutex<VecDeque<SessionMessage>>>;
525
526#[derive(Clone, Copy, Debug, Eq, PartialEq)]
527pub(crate) enum ExecutionAbortReason {
528 Terminated,
530 #[cfg_attr(test, allow(dead_code))]
535 WallClockTimedOut,
536 #[cfg_attr(test, allow(dead_code))]
538 CpuBudgetExceeded,
539}
540
541struct ExecutionAbortState {
542 sender: Option<crossbeam_channel::Sender<()>>,
543 reason: Option<ExecutionAbortReason>,
544}
545
546pub(crate) struct SharedExecutionAbort(Arc<Mutex<Option<ExecutionAbortState>>>);
547
548impl Clone for SharedExecutionAbort {
549 fn clone(&self) -> Self {
550 Self(Arc::clone(&self.0))
551 }
552}
553
554pub(crate) fn new_deferred_queue() -> DeferredQueue {
556 Arc::new(Mutex::new(VecDeque::new()))
557}
558
559pub(crate) fn new_execution_abort() -> SharedExecutionAbort {
560 SharedExecutionAbort(Arc::new(Mutex::new(None)))
561}
562
563pub(crate) struct ActiveExecutionAbort {
564 shared: SharedExecutionAbort,
565}
566
567impl ActiveExecutionAbort {
568 pub(crate) fn arm(shared: &SharedExecutionAbort) -> (Self, crossbeam_channel::Receiver<()>) {
569 let (tx, rx) = crossbeam_channel::bounded::<()>(0);
570 let mut guard = shared.0.lock().unwrap();
571 *guard = Some(ExecutionAbortState {
572 sender: Some(tx),
573 reason: None,
574 });
575 (
576 Self {
577 shared: shared.clone(),
578 },
579 rx,
580 )
581 }
582}
583
584impl Drop for ActiveExecutionAbort {
585 fn drop(&mut self) {
586 *self.shared.0.lock().unwrap() = None;
587 }
588}
589
590pub(crate) fn signal_execution_abort(shared: &SharedExecutionAbort, reason: ExecutionAbortReason) {
591 if let Some(state) = shared.0.lock().unwrap().as_mut() {
592 state.reason.get_or_insert(reason);
593 state.sender.take();
594 }
595}
596
597#[cfg(not(test))]
598fn execution_abort_reason(shared: &SharedExecutionAbort) -> Option<ExecutionAbortReason> {
599 shared
600 .0
601 .lock()
602 .unwrap()
603 .as_ref()
604 .and_then(|state| state.reason)
605}
606
607pub struct SessionManager {
610 sessions: HashMap<String, SessionEntry>,
611 max_concurrency: usize,
612 slot_control: SlotControl,
613 event_tx: RuntimeEventSender,
615 call_id_router: CallIdRouter,
617 shared_call_id: SharedCallIdCounter,
620 snapshot_cache: Arc<SnapshotCache>,
622 warm_pool: Arc<WarmWorkerPool>,
624}
625
626impl SessionManager {
627 pub fn new(
628 max_concurrency: usize,
629 event_tx: RuntimeEventSender,
630 call_id_router: CallIdRouter,
631 snapshot_cache: Arc<SnapshotCache>,
632 ) -> Self {
633 SessionManager {
634 sessions: HashMap::new(),
635 max_concurrency,
636 slot_control: Arc::new((Mutex::new(0), Condvar::new())),
637 event_tx,
638 call_id_router,
639 shared_call_id: Arc::new(AtomicU64::new(1)),
640 snapshot_cache,
641 warm_pool: Arc::new(WarmWorkerPool::default()),
642 }
643 }
644
645 #[allow(dead_code)]
647 pub fn snapshot_cache(&self) -> &Arc<SnapshotCache> {
648 &self.snapshot_cache
649 }
650
651 pub fn pre_warm_workers(
652 &self,
653 bridge_code: String,
654 userland_code: String,
655 heap_limit_mb: Option<u32>,
656 count: usize,
657 ) {
658 self.warm_pool.ensure_count(
659 Arc::clone(&self.snapshot_cache),
660 Arc::clone(&self.slot_control),
661 bridge_code,
662 userland_code,
663 heap_limit_mb,
664 count,
665 );
666 }
667
668 pub fn create_session(
672 &mut self,
673 session_id: String,
674 heap_limit_mb: Option<u32>,
675 cpu_time_limit_ms: Option<u32>,
676 wall_clock_limit_ms: Option<u32>,
677 ) -> Result<(), String> {
678 self.create_session_with_output_generation(
679 session_id,
680 heap_limit_mb,
681 cpu_time_limit_ms,
682 wall_clock_limit_ms,
683 None,
684 None,
685 )
686 }
687
688 pub fn create_session_with_output_generation(
689 &mut self,
690 session_id: String,
691 heap_limit_mb: Option<u32>,
692 cpu_time_limit_ms: Option<u32>,
693 wall_clock_limit_ms: Option<u32>,
694 output_generation: Option<u64>,
695 warm_hint: Option<WarmSessionHint>,
696 ) -> Result<(), String> {
697 if self.sessions.contains_key(&session_id) {
698 return Err(format!("session {} already exists", session_id));
699 }
700
701 let cpu_time_limit_ms = normalize_cpu_time_limit_ms(cpu_time_limit_ms);
702 let wall_clock_limit_ms = normalize_wall_clock_limit_ms(wall_clock_limit_ms);
703 let (tx, rx) = crossbeam_channel::bounded(SESSION_COMMAND_CHANNEL_CAPACITY);
704 let isolate_handle = Arc::new(Mutex::new(None));
705 let execution_abort = new_execution_abort();
706 let assignment = SessionAssignment {
707 heap_limit_mb,
708 cpu_time_limit_ms,
709 wall_clock_limit_ms,
710 rx,
711 slot_control: Arc::clone(&self.slot_control),
712 max_concurrency: self.max_concurrency,
713 event_tx: self.event_tx.clone(),
714 call_id_router: Arc::clone(&self.call_id_router),
715 shared_call_id: Arc::clone(&self.shared_call_id),
716 snapshot_cache: Arc::clone(&self.snapshot_cache),
717 isolate_handle: Arc::clone(&isolate_handle),
718 execution_abort: execution_abort.clone(),
719 session_id: session_id.clone(),
720 output_generation,
721 };
722
723 let join_handle = match self.claim_warm_worker(warm_hint.as_ref(), assignment) {
724 Ok((join_handle, true)) => {
725 if let Some(hint) = warm_hint {
726 self.warm_pool.ensure_count(
727 Arc::clone(&self.snapshot_cache),
728 Arc::clone(&self.slot_control),
729 hint.bridge_code,
730 hint.userland_code,
731 hint.heap_limit_mb,
732 warm_worker_capacity_per_key(),
733 );
734 }
735 join_handle
736 }
737 Ok((join_handle, false)) => join_handle,
738 Err(assignment) => spawn_session_thread(assignment)
739 .map_err(|e| format!("failed to spawn session thread: {}", e))?,
740 };
741
742 self.sessions.insert(
743 session_id,
744 SessionEntry {
745 output_generation,
746 tx,
747 join_handle: Some(join_handle),
748 isolate_handle,
749 execution_abort,
750 },
751 );
752
753 Ok(())
754 }
755
756 #[allow(clippy::result_large_err)]
759 fn claim_warm_worker(
760 &self,
761 warm_hint: Option<&WarmSessionHint>,
762 assignment: SessionAssignment,
763 ) -> Result<(thread::JoinHandle<()>, bool), SessionAssignment> {
764 let Some(hint) = warm_hint else {
765 return Err(assignment);
766 };
767 if warm_worker_capacity_per_key() == 0 {
768 record_warm_worker_miss();
769 return Err(assignment);
770 }
771
772 let key = warm_pool_key(&hint.bridge_code, &hint.userland_code, hint.heap_limit_mb);
773 let Some(worker) = self.warm_pool.claim(&key) else {
774 record_warm_worker_miss();
775 eprintln!(
776 "agentos-v8-runtime: warm worker pool-empty key={} heap={}",
777 warm_key_prefix(&key),
778 key.heap_limit_mb
779 );
780 return Err(assignment);
781 };
782
783 match worker.assignment_tx.send(assignment) {
784 Ok(()) => {
785 record_warm_worker_hit();
786 eprintln!(
787 "agentos-v8-runtime: warm worker claimed key={} heap={}",
788 warm_key_prefix(&key),
789 key.heap_limit_mb
790 );
791 Ok((worker.join_handle, true))
792 }
793 Err(error) => {
794 record_warm_worker_miss();
795 let _ = worker.join_handle.join();
796 Err(error.0)
797 }
798 }
799 }
800
801 pub fn destroy_session_if_output_generation(
802 &mut self,
803 session_id: &str,
804 output_generation: u64,
805 ) -> Result<bool, String> {
806 match self.begin_destroy_session_if_output_generation(session_id, output_generation)? {
807 Some(shutdown) => {
808 shutdown.finish();
809 Ok(true)
810 }
811 None => Ok(false),
812 }
813 }
814
815 pub fn begin_destroy_session_if_output_generation(
816 &mut self,
817 session_id: &str,
818 output_generation: u64,
819 ) -> Result<Option<SessionShutdown>, String> {
820 if self
821 .sessions
822 .get(session_id)
823 .is_none_or(|entry| entry.output_generation != Some(output_generation))
824 {
825 return Ok(None);
826 }
827
828 self.begin_destroy_session(session_id).map(Some)
829 }
830
831 pub fn detach_session_if_output_generation(
832 &mut self,
833 session_id: &str,
834 output_generation: u64,
835 ) -> Result<bool, String> {
836 if self
837 .sessions
838 .get(session_id)
839 .is_none_or(|entry| entry.output_generation != Some(output_generation))
840 {
841 return Ok(false);
842 }
843
844 self.detach_session(session_id)?;
845 Ok(true)
846 }
847
848 fn detach_session(&mut self, session_id: &str) -> Result<(), String> {
849 let entry = self
850 .sessions
851 .get(session_id)
852 .ok_or_else(|| format!("session {} does not exist", session_id))?;
853
854 #[cfg(not(test))]
855 if let Some(handle) = entry
856 .isolate_handle
857 .lock()
858 .ok()
859 .and_then(|guard| guard.as_ref().cloned())
860 {
861 handle.terminate_execution();
862 }
863 signal_execution_abort(&entry.execution_abort, ExecutionAbortReason::Terminated);
864 self.clear_call_routes_for_session(session_id);
865 let mut entry = self.sessions.remove(session_id).unwrap();
866 let _ = entry.tx.try_send(SessionCommand::Shutdown);
867 drop(entry.tx);
868 let _ = entry.join_handle.take();
869 Ok(())
870 }
871
872 pub fn destroy_session(&mut self, session_id: &str) -> Result<(), String> {
876 self.begin_destroy_session(session_id)?.finish();
877 Ok(())
878 }
879
880 pub fn begin_destroy_session(&mut self, session_id: &str) -> Result<SessionShutdown, String> {
885 if !self.sessions.contains_key(session_id) {
886 return Err(format!("session {} does not exist", session_id));
887 }
888
889 self.clear_call_routes_for_session(session_id);
890 let mut entry = self
891 .sessions
892 .remove(session_id)
893 .expect("checked session exists");
894
895 #[cfg(not(test))]
896 if let Some(handle) = entry
897 .isolate_handle
898 .lock()
899 .ok()
900 .and_then(|guard| guard.as_ref().cloned())
901 {
902 handle.terminate_execution();
903 }
904 signal_execution_abort(&entry.execution_abort, ExecutionAbortReason::Terminated);
905 let _ = entry.tx.try_send(SessionCommand::Shutdown);
909 let join_handle = entry.join_handle.take();
910 drop(entry);
911 Ok(SessionShutdown {
912 session_id: session_id.to_owned(),
913 join_handle,
914 call_id_router: Arc::clone(&self.call_id_router),
915 })
916 }
917
918 pub(crate) fn take_session_shutdown_handles(&mut self) -> Vec<thread::JoinHandle<()>> {
919 self.call_id_router
920 .lock()
921 .expect("call_id router lock poisoned")
922 .clear();
923
924 let mut handles: Vec<_> = self
925 .sessions
926 .drain()
927 .filter_map(|(_, mut entry)| {
928 #[cfg(not(test))]
929 if let Some(handle) = entry
930 .isolate_handle
931 .lock()
932 .ok()
933 .and_then(|guard| guard.as_ref().cloned())
934 {
935 handle.terminate_execution();
936 }
937 signal_execution_abort(&entry.execution_abort, ExecutionAbortReason::Terminated);
938 let _ = entry.tx.try_send(SessionCommand::Shutdown);
939 drop(entry.tx);
940 entry.join_handle.take()
941 })
942 .collect();
943 handles.extend(self.warm_pool.shutdown_handles());
944 handles
945 }
946
947 pub(crate) fn clear_call_route(&self, call_id: u64) {
948 self.call_id_router
949 .lock()
950 .expect("call_id router lock poisoned")
951 .remove(&call_id);
952 }
953
954 fn clear_call_routes_for_session(&self, session_id: &str) {
955 self.call_id_router
956 .lock()
957 .expect("call_id router lock poisoned")
958 .retain(|_, routed_session_id| routed_session_id != session_id);
959 }
960
961 pub fn session_command_sender(
966 &self,
967 session_id: &str,
968 msg: &SessionMessage,
969 ) -> Result<Sender<SessionCommand>, String> {
970 let entry = self
971 .sessions
972 .get(session_id)
973 .ok_or_else(|| format!("session {} does not exist", session_id))?;
974
975 #[cfg(not(test))]
976 if matches!(msg, SessionMessage::TerminateExecution) {
977 if let Some(handle) = entry
978 .isolate_handle
979 .lock()
980 .ok()
981 .and_then(|guard| guard.as_ref().cloned())
982 {
983 handle.terminate_execution();
984 }
985 }
986 if matches!(msg, SessionMessage::TerminateExecution) {
987 signal_execution_abort(&entry.execution_abort, ExecutionAbortReason::Terminated);
988 }
989
990 Ok(entry.tx.clone())
991 }
992
993 pub fn session_sender(&self, session_id: &str) -> Result<Sender<SessionCommand>, String> {
996 self.sessions
997 .get(session_id)
998 .map(|entry| entry.tx.clone())
999 .ok_or_else(|| format!("session {} does not exist", session_id))
1000 }
1001
1002 pub fn send_to_session(&self, session_id: &str, msg: SessionMessage) -> Result<(), String> {
1005 let sender = self.session_command_sender(session_id, &msg)?;
1006 sender
1007 .send(SessionCommand::Message(msg))
1008 .map_err(|e| format!("session thread disconnected: {}", e))
1009 }
1010
1011 pub fn destroy_sessions<I>(&mut self, session_ids: I)
1015 where
1016 I: IntoIterator<Item = String>,
1017 {
1018 for shutdown in self.begin_destroy_sessions(session_ids) {
1019 shutdown.finish();
1020 }
1021 }
1022
1023 pub fn begin_destroy_sessions<I>(&mut self, session_ids: I) -> Vec<SessionShutdown>
1026 where
1027 I: IntoIterator<Item = String>,
1028 {
1029 session_ids
1030 .into_iter()
1031 .filter_map(|sid| self.begin_destroy_session(&sid).ok())
1032 .collect()
1033 }
1034
1035 #[allow(dead_code)]
1037 pub fn session_count(&self) -> usize {
1038 self.sessions.len()
1039 }
1040
1041 #[allow(dead_code)]
1043 pub fn all_sessions(&self) -> Vec<String> {
1044 self.sessions.keys().cloned().collect()
1045 }
1046
1047 #[allow(dead_code)]
1049 pub fn active_slot_count(&self) -> usize {
1050 let (lock, _) = &*self.slot_control;
1051 *lock.lock().unwrap()
1052 }
1053
1054 pub fn call_id_router(&self) -> &CallIdRouter {
1056 &self.call_id_router
1057 }
1058}
1059
1060#[cfg(not(test))]
1062fn send_event_with_generation(
1063 event_tx: &RuntimeEventSender,
1064 output_generation: Option<u64>,
1065 event: RuntimeEvent,
1066) {
1067 if let Err(error) = event_tx.send(RuntimeEventEnvelope {
1068 output_generation,
1069 event,
1070 }) {
1071 eprintln!("failed to send runtime event: {error}");
1072 }
1073}
1074
1075fn send_late_message_warning(
1076 event_tx: &RuntimeEventSender,
1077 session_id: &str,
1078 output_generation: Option<u64>,
1079 error_code: &str,
1080 detail: String,
1081) {
1082 let warning = RuntimeEvent::Log {
1083 session_id: session_id.to_string(),
1084 channel: 1,
1085 message: format!("[{error_code}] {detail}"),
1086 };
1087 if let Err(error) = event_tx.send(RuntimeEventEnvelope {
1088 output_generation,
1089 event: warning,
1090 }) {
1091 eprintln!("failed to send late-session warning: {error}");
1092 }
1093}
1094
1095fn handle_late_session_message(
1096 event_tx: &RuntimeEventSender,
1097 session_id: &str,
1098 output_generation: Option<u64>,
1099 message: SessionMessage,
1100) {
1101 match message {
1102 SessionMessage::BridgeResponse(BridgeResponse {
1103 call_id,
1104 status,
1105 payload,
1106 }) => send_late_message_warning(
1107 event_tx,
1108 session_id,
1109 output_generation,
1110 LATE_BRIDGE_RESPONSE_ERROR_CODE,
1111 format!(
1112 "dropping BridgeResponse after execution completed (call_id={call_id}, status={status}, payload_len={})",
1113 payload.len()
1114 ),
1115 ),
1116 SessionMessage::StreamEvent(StreamEvent {
1117 event_type,
1118 payload,
1119 }) => {
1120 if event_type == "timer" || event_type == "net_socket" {
1125 return;
1126 }
1127 send_late_message_warning(
1128 event_tx,
1129 session_id,
1130 output_generation,
1131 LATE_STREAM_EVENT_ERROR_CODE,
1132 format!(
1133 "dropping StreamEvent after execution completed (event_type={event_type}, payload_len={})",
1134 payload.len()
1135 ),
1136 )
1137 }
1138 SessionMessage::TerminateExecution => send_late_message_warning(
1139 event_tx,
1140 session_id,
1141 output_generation,
1142 LATE_TERMINATE_EXECUTION_ERROR_CODE,
1143 String::from("dropping TerminateExecution after execution completed"),
1144 ),
1145 SessionMessage::InjectGlobals { .. } | SessionMessage::Execute { .. } => {}
1146 }
1147}
1148
1149fn defer_session_command_before_slot(
1150 deferred_commands: &mut VecDeque<SessionCommand>,
1151 event_tx: &RuntimeEventSender,
1152 session_id: &str,
1153 output_generation: Option<u64>,
1154 command: SessionCommand,
1155) -> bool {
1156 if deferred_commands.len() < MAX_DEFERRED_SESSION_COMMANDS {
1157 deferred_commands.push_back(command);
1158 return true;
1159 }
1160
1161 send_late_message_warning(
1162 event_tx,
1163 session_id,
1164 output_generation,
1165 DEFERRED_COMMAND_LIMIT_ERROR_CODE,
1166 format!(
1167 "dropping queued session before slot acquisition because deferred command queue exceeded limit of {MAX_DEFERRED_SESSION_COMMANDS}"
1168 ),
1169 );
1170 false
1171}
1172
1173#[cfg(not(test))]
1174fn install_wasm_module_bytes_global<'s>(scope: &mut v8::HandleScope<'s>, bytes: &[u8]) -> bool {
1175 let global = scope.get_current_context().global(scope);
1176 let Some(name) = v8::String::new(scope, "__agentOSWasmModuleBytes") else {
1177 return false;
1178 };
1179 let len = bytes.len();
1180 let backing_store = v8::ArrayBuffer::new_backing_store_from_bytes(bytes.to_vec());
1181 let array_buffer = v8::ArrayBuffer::with_backing_store(scope, &backing_store.make_shared());
1182 let Some(bytes_value) = v8::Uint8Array::new(scope, array_buffer, 0, len) else {
1183 return false;
1184 };
1185 global.set(scope, name.into(), bytes_value.into()).is_some()
1186}
1187
1188fn spawn_session_thread(assignment: SessionAssignment) -> std::io::Result<thread::JoinHandle<()>> {
1192 let name_prefix = if assignment.session_id.len() > 8 {
1193 assignment.session_id[..8].to_string()
1194 } else {
1195 assignment.session_id.clone()
1196 };
1197 thread::Builder::new()
1198 .name(format!("session-{}", name_prefix))
1199 .spawn(move || session_thread(assignment, None))
1200}
1201
1202#[allow(clippy::too_many_arguments)]
1203fn session_thread(
1204 assignment: SessionAssignment,
1205 #[cfg_attr(test, allow(unused_variables))] precreated_isolate: Option<PrecreatedIsolate>,
1206) {
1207 let SessionAssignment {
1208 heap_limit_mb,
1209 cpu_time_limit_ms,
1210 wall_clock_limit_ms,
1211 rx,
1212 slot_control,
1213 max_concurrency,
1214 event_tx,
1215 call_id_router,
1216 shared_call_id,
1217 snapshot_cache,
1218 isolate_handle,
1219 execution_abort,
1220 session_id,
1221 output_generation,
1222 } = assignment;
1223 #[cfg(test)]
1224 let _ = (
1225 heap_limit_mb,
1226 cpu_time_limit_ms,
1227 wall_clock_limit_ms,
1228 call_id_router,
1229 shared_call_id,
1230 snapshot_cache,
1231 isolate_handle,
1232 execution_abort,
1233 );
1234
1235 let mut deferred_commands = VecDeque::new();
1238 let acquired_slot = {
1239 let (lock, cvar) = &*slot_control;
1240 let mut count = lock.lock().unwrap();
1241 loop {
1242 if *count < max_concurrency {
1243 *count += 1;
1244 break true;
1245 }
1246
1247 let (next_count, _) = cvar
1248 .wait_timeout(count, std::time::Duration::from_millis(50))
1249 .unwrap();
1250 count = next_count;
1251
1252 match rx.try_recv() {
1253 Ok(SessionCommand::Shutdown)
1254 | Err(crossbeam_channel::TryRecvError::Disconnected) => {
1255 break false;
1256 }
1257 Ok(command) => {
1258 if !defer_session_command_before_slot(
1259 &mut deferred_commands,
1260 &event_tx,
1261 &session_id,
1262 output_generation,
1263 command,
1264 ) {
1265 break false;
1266 }
1267 }
1268 Err(crossbeam_channel::TryRecvError::Empty) => {}
1269 }
1270 }
1271 };
1272
1273 if !acquired_slot {
1274 return;
1275 }
1276
1277 #[cfg(all(not(test), unix))]
1283 let exec_thread_cpu_clock = crate::timeout::current_thread_cpu_clock();
1284 #[cfg(all(not(test), not(unix)))]
1285 let exec_thread_cpu_clock: Option<crate::timeout::ThreadCpuClock> = None;
1286
1287 #[cfg(not(test))]
1291 let (
1292 mut v8_isolate,
1293 mut _v8_context,
1294 mut from_snapshot,
1295 mut isolate_bridge_code,
1296 mut isolate_userland_code,
1297 ) = match precreated_isolate {
1298 Some(precreated) => (
1299 Some(precreated.isolate),
1300 Some(precreated.context),
1301 true,
1302 Some(precreated.bridge_code),
1303 Some(precreated.userland_code),
1304 ),
1305 None => (None, None, false, None, None),
1306 };
1307
1308 #[cfg(not(test))]
1309 let mut pending = bridge::PendingPromises::new();
1310
1311 #[cfg(not(test))]
1313 let mut last_globals_payload: Option<Vec<u8>> = None;
1314
1315 #[cfg(not(test))]
1317 let mut bridge_cache: Option<execution::BridgeCodeCache> = None;
1318
1319 #[cfg(not(test))]
1321 let mut last_bridge_code: Option<String> = None;
1322 #[cfg(not(test))]
1324 let mut last_userland_code: Option<String> = None;
1325
1326 #[cfg(not(test))]
1332 let mut high_resolution_time_origin = Instant::now();
1333
1334 #[cfg(not(test))]
1335 if let Some(iso) = v8_isolate.as_mut() {
1336 *isolate_handle
1337 .lock()
1338 .expect("session isolate handle lock poisoned") = Some(iso.thread_safe_handle());
1339 }
1340
1341 loop {
1343 let next_command = if let Some(command) = deferred_commands.pop_front() {
1344 Ok(command)
1345 } else {
1346 rx.recv()
1347 };
1348
1349 match next_command {
1350 Ok(SessionCommand::Shutdown) | Err(_) => break,
1351 Ok(SessionCommand::SetModuleReader(reader)) => {
1352 execution::install_session_guest_reader(Some(reader));
1353 }
1354 Ok(SessionCommand::Message(msg)) => match msg {
1355 SessionMessage::InjectGlobals { payload } => {
1356 #[cfg(not(test))]
1357 {
1358 last_globals_payload = Some(payload);
1360 }
1361 #[cfg(test)]
1362 {
1363 let _ = payload;
1364 }
1365 }
1366 SessionMessage::Execute {
1367 mode,
1368 file_path,
1369 bridge_code,
1370 post_restore_script,
1371 userland_code,
1372 high_resolution_time,
1373 user_code,
1374 wasm_module_bytes,
1375 } => {
1376 #[cfg(test)]
1379 let _ = &userland_code;
1380 #[cfg(test)]
1381 let _ = high_resolution_time;
1382 #[cfg(test)]
1383 let _ = &wasm_module_bytes;
1384 #[cfg(not(test))]
1385 {
1386 let session_id = session_id.clone();
1387 let should_update_cached_bridge_code = !bridge_code.is_empty();
1389 let effective_bridge_code = if bridge_code.is_empty() {
1390 last_bridge_code.as_deref().unwrap_or("").to_string()
1391 } else {
1392 bridge_code
1393 };
1394 let should_update_cached_userland_code = !userland_code.is_empty();
1396 let effective_userland_code = if userland_code.is_empty() {
1397 last_userland_code.as_deref().unwrap_or("").to_string()
1398 } else {
1399 userland_code
1400 };
1401
1402 if let Err(message) =
1403 snapshot::validate_bridge_code_size(&effective_bridge_code)
1404 {
1405 let result_frame = RuntimeEvent::ExecutionResult {
1406 session_id,
1407 exit_code: 1,
1408 exports: None,
1409 error: Some(ExecutionErrorBin {
1410 error_type: "Error".into(),
1411 message,
1412 stack: String::new(),
1413 code: snapshot::V8_BRIDGE_CODE_LIMIT_ERROR_CODE.into(),
1414 }),
1415 };
1416 send_event_with_generation(&event_tx, output_generation, result_frame);
1417 continue;
1418 }
1419
1420 if should_update_cached_bridge_code {
1421 last_bridge_code = Some(effective_bridge_code.clone());
1422 }
1423 if should_update_cached_userland_code {
1424 last_userland_code = Some(effective_userland_code.clone());
1425 }
1426
1427 if v8_isolate.is_some()
1428 && (isolate_bridge_code.as_deref()
1429 != Some(effective_bridge_code.as_str())
1430 || isolate_userland_code.as_deref()
1431 != Some(effective_userland_code.as_str()))
1432 {
1433 *isolate_handle
1434 .lock()
1435 .expect("session isolate handle lock poisoned") = None;
1436 reset_pending_promises(&mut pending);
1443 drop(_v8_context.take());
1444 isolate::drop_isolate(v8_isolate.take());
1445 from_snapshot = false;
1446 isolate_bridge_code = None;
1447 isolate_userland_code = None;
1448 }
1449
1450 if v8_isolate.is_none() {
1452 isolate::init_v8_platform();
1453 let phase_start = Instant::now();
1457 let snapshot_blob = match snapshot_cache.get_or_create_with_userland(
1458 &effective_bridge_code,
1459 (!effective_userland_code.is_empty())
1460 .then_some(effective_userland_code.as_str()),
1461 ) {
1462 Ok(blob) => Some(blob),
1463 Err(message) => {
1464 eprintln!(
1469 "agentos-v8-runtime: snapshot creation failed, \
1470 falling back to fresh isolate: {message}"
1471 );
1472 None
1473 }
1474 };
1475 record_v8_session_phase("snapshot_get", phase_start.elapsed());
1476 let mut iso = match snapshot_blob {
1477 Some(blob) => {
1478 from_snapshot = true;
1479 eprintln!(
1480 "agentos-v8-runtime: restored session isolate from_snapshot=true"
1481 );
1482 let phase_start = Instant::now();
1483 let snapshot_blob = (*blob).clone();
1487 record_v8_session_phase("blob_clone", phase_start.elapsed());
1488 let phase_start = Instant::now();
1489 let isolate = snapshot::create_isolate_from_snapshot(
1490 snapshot_blob,
1491 heap_limit_mb,
1492 );
1493 record_v8_session_phase("isolate_new", phase_start.elapsed());
1494 isolate
1495 }
1496 None => {
1497 from_snapshot = false;
1498 let phase_start = Instant::now();
1499 let isolate = isolate::create_isolate(heap_limit_mb);
1500 record_v8_session_phase("isolate_new", phase_start.elapsed());
1501 isolate
1502 }
1503 };
1504 iso.set_host_import_module_dynamically_callback(
1505 execution::dynamic_import_callback,
1506 );
1507 iso.set_host_initialize_import_meta_object_callback(
1508 execution::import_meta_object_callback,
1509 );
1510 high_resolution_time_origin = Instant::now();
1511 *isolate_handle
1512 .lock()
1513 .expect("session isolate handle lock poisoned") =
1514 Some(iso.thread_safe_handle());
1515 let ctx = isolate::create_context(&mut iso);
1516 _v8_context = Some(ctx);
1517 v8_isolate = Some(iso);
1518 isolate_bridge_code = Some(effective_bridge_code.clone());
1519 isolate_userland_code = Some(effective_userland_code.clone());
1520 }
1521
1522 let iso = v8_isolate.as_mut().unwrap();
1523 iso.cancel_terminate_execution();
1524
1525 let exec_context = isolate::create_context(iso);
1530
1531 if high_resolution_time {
1532 let scope = &mut v8::HandleScope::new(iso);
1533 let ctx = v8::Local::new(scope, &exec_context);
1534 let scope = &mut v8::ContextScope::new(scope, ctx);
1535 execution::install_high_resolution_time_global(
1536 scope,
1537 &high_resolution_time_origin as *const Instant,
1538 );
1539 }
1540
1541 if let Some(ref payload) = last_globals_payload {
1543 let scope = &mut v8::HandleScope::new(iso);
1544 let ctx = v8::Local::new(scope, &exec_context);
1545 let scope = &mut v8::ContextScope::new(scope, ctx);
1546 if let Err(error) =
1547 execution::inject_globals_from_payload(scope, payload)
1548 {
1549 let result_frame = RuntimeEvent::ExecutionResult {
1550 session_id,
1551 exit_code: 1,
1552 exports: None,
1553 error: Some(ExecutionErrorBin {
1554 error_type: error.error_type,
1555 message: error.message,
1556 stack: error.stack,
1557 code: error.code.unwrap_or_default(),
1558 }),
1559 };
1560 send_event_with_generation(
1561 &event_tx,
1562 output_generation,
1563 result_frame,
1564 );
1565 continue;
1566 }
1567 }
1568
1569 let (_active_execution_abort, abort_rx) =
1572 ActiveExecutionAbort::arm(&execution_abort);
1573
1574 let deferred_queue = new_deferred_queue();
1576
1577 let channel_rx = ChannelResponseReceiver::with_abort(
1579 rx.clone(),
1580 abort_rx.clone(),
1581 Arc::clone(&deferred_queue),
1582 );
1583 let bridge_ctx = BridgeCallContext::with_receiver(
1584 Box::new(ChannelRuntimeEventSender::new(
1585 event_tx.clone(),
1586 output_generation,
1587 )),
1588 Box::new(channel_rx),
1589 session_id.clone(),
1590 Arc::clone(&call_id_router),
1591 Arc::clone(&shared_call_id),
1592 );
1593
1594 let _sync_store;
1598 let _async_store;
1599 let sync_bridge_fns = sync_bridge_fns();
1600 let async_bridge_fns = async_bridge_fns();
1601 {
1602 let scope = &mut v8::HandleScope::new(iso);
1603 let ctx = v8::Local::new(scope, &exec_context);
1604 let scope = &mut v8::ContextScope::new(scope, ctx);
1605
1606 (_sync_store, _async_store) = bridge::replace_bridge_fns(
1607 scope,
1608 &bridge_ctx as *const BridgeCallContext,
1609 &pending as *const bridge::PendingPromises,
1610 sync_bridge_fns,
1611 async_bridge_fns,
1612 );
1613 }
1614
1615 if !post_restore_script.is_empty() {
1618 let scope = &mut v8::HandleScope::new(iso);
1619 let ctx = v8::Local::new(scope, &exec_context);
1620 let scope = &mut v8::ContextScope::new(scope, ctx);
1621 let (prs_code, prs_err) =
1622 execution::run_init_script(scope, &post_restore_script);
1623 if prs_code != 0 {
1624 let result_frame = RuntimeEvent::ExecutionResult {
1625 session_id,
1626 exit_code: prs_code,
1627 exports: None,
1628 error: prs_err.map(|e| ExecutionErrorBin {
1629 error_type: e.error_type,
1630 message: e.message,
1631 stack: e.stack,
1632 code: e.code.unwrap_or_default(),
1633 }),
1634 };
1635 send_event_with_generation(
1636 &event_tx,
1637 output_generation,
1638 result_frame,
1639 );
1640 continue;
1641 }
1642 }
1643
1644 if let Some(wasm_module_bytes) = wasm_module_bytes.as_ref() {
1645 let scope = &mut v8::HandleScope::new(iso);
1646 let ctx = v8::Local::new(scope, &exec_context);
1647 let scope = &mut v8::ContextScope::new(scope, ctx);
1648 if !install_wasm_module_bytes_global(scope, wasm_module_bytes) {
1649 let result_frame = RuntimeEvent::ExecutionResult {
1650 session_id,
1651 exit_code: 1,
1652 exports: None,
1653 error: Some(ExecutionErrorBin {
1654 error_type: "Error".into(),
1655 message: "failed to install __agentOSWasmModuleBytes"
1656 .into(),
1657 stack: String::new(),
1658 code: String::new(),
1659 }),
1660 };
1661 send_event_with_generation(
1662 &event_tx,
1663 output_generation,
1664 result_frame,
1665 );
1666 continue;
1667 }
1668 }
1669
1670 let mut cpu_budget_guard = match cpu_time_limit_ms {
1682 Some(budget_ms) => {
1683 let cpu_clock = match exec_thread_cpu_clock {
1689 Some(clock) => clock,
1690 None => {
1691 let result_frame = RuntimeEvent::ExecutionResult {
1692 session_id,
1693 exit_code: 1,
1694 exports: None,
1695 error: Some(ExecutionErrorBin {
1696 error_type: "Error".into(),
1697 message: format!(
1698 "{}: per-thread CPU clock unavailable; cannot enforce limits.jsRuntime.cpuTimeLimitMs",
1699 crate::timeout::CPU_BUDGET_GUARD_START_ERROR_CODE
1700 ),
1701 stack: String::new(),
1702 code: crate::timeout::CPU_BUDGET_GUARD_START_ERROR_CODE
1703 .into(),
1704 }),
1705 };
1706 send_event_with_generation(
1707 &event_tx,
1708 output_generation,
1709 result_frame,
1710 );
1711 continue;
1712 }
1713 };
1714 let handle = iso.thread_safe_handle();
1715 match crate::timeout::CpuBudgetGuard::new(
1716 budget_ms,
1717 cpu_clock,
1718 handle,
1719 execution_abort.clone(),
1720 ) {
1721 Ok(guard) => Some(guard),
1722 Err(message) => {
1723 let result_frame = RuntimeEvent::ExecutionResult {
1724 session_id,
1725 exit_code: 1,
1726 exports: None,
1727 error: Some(ExecutionErrorBin {
1728 error_type: "Error".into(),
1729 message,
1730 stack: String::new(),
1731 code:
1732 crate::timeout::CPU_BUDGET_GUARD_START_ERROR_CODE
1733 .into(),
1734 }),
1735 };
1736 send_event_with_generation(
1737 &event_tx,
1738 output_generation,
1739 result_frame,
1740 );
1741 continue;
1742 }
1743 }
1744 }
1745 _ => None,
1746 };
1747
1748 let mut wall_clock_guard = match wall_clock_limit_ms {
1758 Some(limit_ms) => {
1759 let handle = iso.thread_safe_handle();
1760 match crate::timeout::TimeoutGuard::with_execution_abort(
1761 limit_ms,
1762 handle,
1763 execution_abort.clone(),
1764 ) {
1765 Ok(guard) => Some(guard),
1766 Err(message) => {
1767 let result_frame = RuntimeEvent::ExecutionResult {
1768 session_id,
1769 exit_code: 1,
1770 exports: None,
1771 error: Some(ExecutionErrorBin {
1772 error_type: "Error".into(),
1773 message,
1774 stack: String::new(),
1775 code:
1776 crate::timeout::TIMEOUT_GUARD_START_ERROR_CODE
1777 .into(),
1778 }),
1779 };
1780 send_event_with_generation(
1781 &event_tx,
1782 output_generation,
1783 result_frame,
1784 );
1785 continue;
1786 }
1787 }
1788 }
1789 _ => None,
1790 };
1791
1792 let bridge_code_for_exec = if from_snapshot {
1796 ""
1797 } else {
1798 &effective_bridge_code
1799 };
1800 let file_path_opt = if file_path.is_empty() {
1801 None
1802 } else {
1803 Some(file_path.as_str())
1804 };
1805 let phase_start = Instant::now();
1806 let (mut code, mut exports, mut error) = if mode == 0 {
1807 let scope = &mut v8::HandleScope::new(iso);
1808 let ctx = v8::Local::new(scope, &exec_context);
1809 let scope = &mut v8::ContextScope::new(scope, ctx);
1810 let (c, e) = execution::execute_script_with_options(
1811 scope,
1812 Some(&bridge_ctx),
1813 bridge_code_for_exec,
1814 &user_code,
1815 file_path_opt,
1816 &mut bridge_cache,
1817 );
1818 (c, None, e)
1819 } else {
1820 let scope = &mut v8::HandleScope::new(iso);
1821 let ctx = v8::Local::new(scope, &exec_context);
1822 let scope = &mut v8::ContextScope::new(scope, ctx);
1823 execution::execute_module(
1824 scope,
1825 &bridge_ctx,
1826 bridge_code_for_exec,
1827 &user_code,
1828 file_path_opt,
1829 &mut bridge_cache,
1830 )
1831 };
1832
1833 if mode != 0 && error.is_none() {
1837 let scope = &mut v8::HandleScope::new(iso);
1838 let ctx = v8::Local::new(scope, &exec_context);
1839 let scope = &mut v8::ContextScope::new(scope, ctx);
1840 if let Some((next_code, next_exports, next_error)) =
1841 execution::finalize_pending_module_evaluation(scope)
1842 {
1843 code = next_code;
1844 exports = next_exports;
1845 error = next_error;
1846 }
1847 }
1848 record_v8_session_phase("user_code_execute", phase_start.elapsed());
1849
1850 let should_enter_event_loop = !pending.is_empty()
1857 || execution::has_pending_module_evaluation()
1858 || execution::has_pending_script_evaluation()
1859 || !deferred_queue.lock().unwrap().is_empty();
1860 let event_loop_status = if should_enter_event_loop {
1861 let scope = &mut v8::HandleScope::new(iso);
1862 let ctx = v8::Local::new(scope, &exec_context);
1863 let scope = &mut v8::ContextScope::new(scope, ctx);
1864 run_event_loop(
1865 scope,
1866 &rx,
1867 &pending,
1868 Some(&abort_rx),
1869 Some(&deferred_queue),
1870 )
1871 } else {
1872 EventLoopStatus::Completed
1873 };
1874
1875 let mut terminated =
1876 matches!(event_loop_status, EventLoopStatus::Terminated);
1877 if let EventLoopStatus::Failed(next_code, next_error) = event_loop_status {
1878 code = next_code;
1879 error = Some(next_error);
1880 }
1881
1882 if !terminated && mode != 0 && error.is_none() {
1885 let scope = &mut v8::HandleScope::new(iso);
1886 let ctx = v8::Local::new(scope, &exec_context);
1887 let scope = &mut v8::ContextScope::new(scope, ctx);
1888 if let Some((next_code, next_exports, next_error)) =
1889 execution::finalize_pending_module_evaluation(scope)
1890 {
1891 code = next_code;
1892 exports = next_exports;
1893 error = next_error;
1894 }
1895 }
1896
1897 if !terminated && error.is_none() {
1902 {
1904 let scope = &mut v8::HandleScope::new(iso);
1905 let ctx = v8::Local::new(scope, &exec_context);
1906 let scope = &mut v8::ContextScope::new(scope, ctx);
1907 let global = ctx.global(scope);
1908 let key = v8::String::new(scope, "_waitForActiveHandles").unwrap();
1909 if let Some(func) = global.get(scope, key.into()) {
1910 if func.is_function() {
1911 let func =
1912 v8::Local::<v8::Function>::try_from(func).unwrap();
1913 let recv = v8::undefined(scope).into();
1914 if let Some(result) = func.call(scope, recv, &[]) {
1915 if result.is_promise() {
1916 let promise =
1917 v8::Local::<v8::Promise>::try_from(result)
1918 .unwrap();
1919 if promise.state() == v8::PromiseState::Pending {
1920 execution::set_pending_script_evaluation(
1921 scope, promise,
1922 );
1923 }
1924 }
1925 }
1926 }
1927 }
1928 }
1929
1930 if !pending.is_empty()
1932 || execution::has_pending_script_evaluation()
1933 || !deferred_queue.lock().unwrap().is_empty()
1934 {
1935 let scope = &mut v8::HandleScope::new(iso);
1936 let ctx = v8::Local::new(scope, &exec_context);
1937 let scope = &mut v8::ContextScope::new(scope, ctx);
1938 let event_loop_status = run_event_loop(
1939 scope,
1940 &rx,
1941 &pending,
1942 Some(&abort_rx),
1943 Some(&deferred_queue),
1944 );
1945
1946 if matches!(event_loop_status, EventLoopStatus::Terminated) {
1947 terminated = true;
1948 }
1949 if let EventLoopStatus::Failed(next_code, next_error) =
1950 event_loop_status
1951 {
1952 code = next_code;
1953 error = Some(next_error);
1954 }
1955 }
1956 }
1957
1958 if !terminated && mode == 0 && error.is_none() {
1959 let scope = &mut v8::HandleScope::new(iso);
1960 let ctx = v8::Local::new(scope, &exec_context);
1961 let scope = &mut v8::ContextScope::new(scope, ctx);
1962 if let Some((next_code, next_error)) =
1963 execution::finalize_pending_script_evaluation(scope)
1964 {
1965 code = next_code;
1966 error = next_error;
1967 }
1968 }
1969
1970 let abort_reason = execution_abort_reason(&execution_abort);
1976 let wall_clock_timed_out =
1977 wall_clock_guard.as_ref().is_some_and(|g| g.timed_out())
1978 || matches!(
1979 abort_reason,
1980 Some(ExecutionAbortReason::WallClockTimedOut)
1981 );
1982 let cpu_budget_exceeded =
1983 cpu_budget_guard.as_ref().is_some_and(|g| g.exceeded())
1984 || matches!(
1985 abort_reason,
1986 Some(ExecutionAbortReason::CpuBudgetExceeded)
1987 );
1988 let cpu_budget_exceeded = cpu_budget_exceeded
1992 && !matches!(
1993 abort_reason,
1994 Some(ExecutionAbortReason::WallClockTimedOut)
1995 );
1996 let wall_clock_timed_out = wall_clock_timed_out && !cpu_budget_exceeded;
1997
1998 if let Some(ref mut guard) = cpu_budget_guard {
2000 guard.cancel();
2001 }
2002 drop(cpu_budget_guard);
2003 if let Some(ref mut guard) = wall_clock_guard {
2004 guard.cancel();
2005 }
2006 drop(wall_clock_guard);
2007
2008 if matches!(abort_reason, Some(ExecutionAbortReason::Terminated)) {
2009 terminated = true;
2010 code = 1;
2011 exports = None;
2012 error = None;
2013 }
2014 if terminated || cpu_budget_exceeded || wall_clock_timed_out {
2015 iso.cancel_terminate_execution();
2016 }
2017
2018 let result_frame = if cpu_budget_exceeded {
2020 if let Some(budget_ms) = cpu_time_limit_ms {
2021 let capacity = budget_ms as usize;
2022 warn_limit_exhausted(TrackedLimit::V8CpuTimeMs, capacity, capacity);
2023 }
2024 RuntimeEvent::ExecutionResult {
2025 session_id,
2026 exit_code: 1,
2027 exports: None,
2028 error: Some(ExecutionErrorBin {
2029 error_type: "Error".into(),
2030 message: "Script execution exceeded the CPU-time budget \
2031 (limits.jsRuntime.cpuTimeLimitMs)"
2032 .into(),
2033 stack: String::new(),
2034 code: "ERR_SCRIPT_CPU_BUDGET_EXCEEDED".into(),
2035 }),
2036 }
2037 } else if wall_clock_timed_out {
2038 if let Some(limit_ms) = wall_clock_limit_ms {
2039 let capacity = limit_ms as usize;
2040 warn_limit_exhausted(
2041 TrackedLimit::V8WallClockMs,
2042 capacity,
2043 capacity,
2044 );
2045 }
2046 RuntimeEvent::ExecutionResult {
2047 session_id,
2048 exit_code: 1,
2049 exports: None,
2050 error: Some(ExecutionErrorBin {
2051 error_type: "Error".into(),
2052 message: "Script execution exceeded the wall-clock limit \
2053 (limits.jsRuntime.wallClockLimitMs)"
2054 .into(),
2055 stack: String::new(),
2056 code: "ERR_SCRIPT_WALL_CLOCK_EXCEEDED".into(),
2057 }),
2058 }
2059 } else if terminated {
2060 RuntimeEvent::ExecutionResult {
2061 session_id,
2062 exit_code: 1,
2063 exports: None,
2064 error: Some(ExecutionErrorBin {
2065 error_type: "Error".into(),
2066 message: "Execution terminated".into(),
2067 stack: String::new(),
2068 code: String::new(),
2069 }),
2070 }
2071 } else {
2072 RuntimeEvent::ExecutionResult {
2073 session_id,
2074 exit_code: code,
2075 exports,
2076 error: error.map(|e| ExecutionErrorBin {
2077 error_type: e.error_type,
2078 message: e.message,
2079 stack: e.stack,
2080 code: e.code.unwrap_or_default(),
2081 }),
2082 }
2083 };
2084
2085 execution::clear_pending_module_evaluation();
2086 execution::clear_pending_script_evaluation();
2087 execution::clear_module_state();
2088
2089 send_event_with_generation(&event_tx, output_generation, result_frame);
2090 }
2091 #[cfg(test)]
2092 {
2093 let _ = (mode, file_path, bridge_code, post_restore_script, user_code);
2094 }
2095 }
2096 SessionMessage::BridgeResponse(_)
2097 | SessionMessage::StreamEvent(_)
2098 | SessionMessage::TerminateExecution => {
2099 handle_late_session_message(&event_tx, &session_id, output_generation, msg);
2100 }
2101 },
2102 }
2103 }
2104
2105 #[cfg(not(test))]
2107 {
2108 *isolate_handle
2109 .lock()
2110 .expect("session isolate handle lock poisoned") = None;
2111 reset_pending_promises(&mut pending);
2117 drop(_v8_context.take());
2118 isolate::drop_isolate(v8_isolate.take());
2119 }
2120
2121 {
2123 let (lock, cvar) = &*slot_control;
2124 let mut count = lock.lock().unwrap();
2125 *count -= 1;
2126 cvar.notify_one();
2127 }
2128}
2129
2130struct BridgeFnPartitions {
2133 sync: Vec<&'static str>,
2134 async_fns: Vec<&'static str>,
2135}
2136
2137pub(crate) fn sync_bridge_fns() -> &'static [&'static str] {
2138 &bridge_fn_partitions().sync
2139}
2140
2141pub(crate) fn async_bridge_fns() -> &'static [&'static str] {
2142 &bridge_fn_partitions().async_fns
2143}
2144
2145fn bridge_fn_partitions() -> &'static BridgeFnPartitions {
2146 static PARTITIONS: OnceLock<BridgeFnPartitions> = OnceLock::new();
2147 PARTITIONS.get_or_init(|| BridgeFnPartitions {
2148 sync: bridge_fns_for(|convention| {
2149 matches!(
2150 convention,
2151 BridgeCallConvention::Sync | BridgeCallConvention::SyncPromise
2152 )
2153 }),
2154 async_fns: bridge_fns_for(|convention| convention == BridgeCallConvention::Async),
2155 })
2156}
2157
2158fn bridge_fns_for(filter: impl Fn(BridgeCallConvention) -> bool) -> Vec<&'static str> {
2159 bridge_contract()
2160 .groups
2161 .iter()
2162 .filter(|group| filter(group.convention))
2163 .flat_map(|group| group.names.iter().map(String::as_str))
2164 .collect()
2165}
2166
2167#[doc(hidden)]
2180pub fn reset_pending_promises(pending: &mut crate::bridge::PendingPromises) {
2181 drop(std::mem::take(pending));
2184}
2185
2186#[doc(hidden)]
2202pub fn run_event_loop(
2203 scope: &mut v8::HandleScope,
2204 rx: &Receiver<SessionCommand>,
2205 pending: &crate::bridge::PendingPromises,
2206 abort_rx: Option<&crossbeam_channel::Receiver<()>>,
2207 deferred: Option<&DeferredQueue>,
2208) -> EventLoopStatus {
2209 while !pending.is_empty()
2210 || execution::pending_module_evaluation_needs_wait(scope)
2211 || execution::pending_script_evaluation_needs_wait(scope)
2212 || pending_guest_timer_count(scope) > 0
2213 || pending_guest_immediate_count(scope) > 0
2214 || deferred
2215 .map(|dq| !dq.lock().unwrap().is_empty())
2216 .unwrap_or(false)
2217 {
2218 pump_v8_message_loop(scope);
2219
2220 if let Some(dq) = deferred {
2222 let frames: Vec<SessionMessage> = dq.lock().unwrap().drain(..).collect();
2223 for frame in frames {
2224 let status = dispatch_event_loop_frame(scope, frame, pending);
2225 if !matches!(status, EventLoopStatus::Completed) {
2226 return status;
2227 }
2228 }
2229 if pending.is_empty()
2230 && !execution::pending_module_evaluation_needs_wait(scope)
2231 && !execution::pending_script_evaluation_needs_wait(scope)
2232 && pending_guest_timer_count(scope) == 0
2233 && pending_guest_immediate_count(scope) == 0
2234 {
2235 break;
2236 }
2237 }
2238
2239 for _ in 0..100 {
2243 scope.perform_microtask_checkpoint();
2244 pump_v8_message_loop(scope);
2245 if let Some(dq) = deferred {
2247 if !dq.lock().unwrap().is_empty() {
2248 break; }
2250 }
2251 }
2252
2253 if pending_guest_immediate_count(scope) > 0 {
2254 match try_recv_session_command(scope, rx, abort_rx) {
2255 Ok(Some(cmd)) => {
2256 let status = dispatch_session_command(scope, cmd, pending);
2257 if !matches!(status, EventLoopStatus::Completed) {
2258 return status;
2259 }
2260 }
2261 Ok(None) => {
2262 let status = drain_guest_immediates(scope);
2263 if !matches!(status, EventLoopStatus::Completed) {
2264 return status;
2265 }
2266 }
2267 Err(status) => return status,
2268 }
2269 scope.perform_microtask_checkpoint();
2270 pump_v8_message_loop(scope);
2271 }
2272
2273 if pending.is_empty()
2276 && !execution::pending_module_evaluation_needs_wait(scope)
2277 && !execution::pending_script_evaluation_needs_wait(scope)
2278 && pending_guest_timer_count(scope) == 0
2279 && pending_guest_immediate_count(scope) == 0
2280 && deferred
2281 .map(|dq| dq.lock().unwrap().is_empty())
2282 .unwrap_or(true)
2283 {
2284 break;
2285 }
2286
2287 let cmd = loop {
2291 if pending_guest_immediate_count(scope) > 0 {
2292 match try_recv_session_command(scope, rx, abort_rx) {
2293 Ok(Some(cmd)) => break cmd,
2294 Ok(None) => {
2295 let status = drain_guest_immediates(scope);
2296 if !matches!(status, EventLoopStatus::Completed) {
2297 return status;
2298 }
2299 scope.perform_microtask_checkpoint();
2300 pump_v8_message_loop(scope);
2301 continue;
2302 }
2303 Err(status) => return status,
2304 }
2305 }
2306 let recv_result = if let Some(abort) = abort_rx {
2307 crossbeam_channel::select! {
2308 recv(rx) -> result => result.ok(),
2309 recv(abort) -> _ => {
2310 scope.terminate_execution();
2311 return EventLoopStatus::Terminated;
2312 },
2313 default(std::time::Duration::from_millis(1)) => None,
2314 }
2315 } else {
2316 rx.recv_timeout(std::time::Duration::from_millis(1)).ok()
2317 };
2318 if let Some(cmd) = recv_result {
2319 break cmd;
2320 }
2321 scope.perform_microtask_checkpoint();
2323 pump_v8_message_loop(scope);
2324 if let Some(dq) = deferred {
2325 if !dq.lock().unwrap().is_empty() {
2326 let frames: Vec<SessionMessage> = dq.lock().unwrap().drain(..).collect();
2328 for frame in frames {
2329 let status = dispatch_event_loop_frame(scope, frame, pending);
2330 if !matches!(status, EventLoopStatus::Completed) {
2331 return status;
2332 }
2333 }
2334 }
2335 }
2336 if pending.is_empty()
2338 && !execution::pending_module_evaluation_needs_wait(scope)
2339 && !execution::pending_script_evaluation_needs_wait(scope)
2340 && pending_guest_timer_count(scope) == 0
2341 && pending_guest_immediate_count(scope) == 0
2342 && deferred
2343 .map(|dq| dq.lock().unwrap().is_empty())
2344 .unwrap_or(true)
2345 {
2346 return EventLoopStatus::Completed;
2347 }
2348 };
2349
2350 let status = dispatch_session_command(scope, cmd, pending);
2351 if !matches!(status, EventLoopStatus::Completed) {
2352 return status;
2353 }
2354 }
2355 EventLoopStatus::Completed
2356}
2357
2358fn try_recv_session_command(
2359 scope: &mut v8::HandleScope,
2360 rx: &Receiver<SessionCommand>,
2361 abort_rx: Option<&crossbeam_channel::Receiver<()>>,
2362) -> Result<Option<SessionCommand>, EventLoopStatus> {
2363 if let Some(abort) = abort_rx {
2364 crossbeam_channel::select! {
2365 recv(abort) -> _ => {
2366 scope.terminate_execution();
2367 Err(EventLoopStatus::Terminated)
2368 },
2369 recv(rx) -> result => Ok(result.ok()),
2370 default => Ok(None),
2371 }
2372 } else {
2373 match rx.try_recv() {
2374 Ok(cmd) => Ok(Some(cmd)),
2375 Err(crossbeam_channel::TryRecvError::Empty) => Ok(None),
2376 Err(crossbeam_channel::TryRecvError::Disconnected) => Ok(None),
2377 }
2378 }
2379}
2380
2381fn dispatch_session_command(
2382 scope: &mut v8::HandleScope,
2383 cmd: SessionCommand,
2384 pending: &crate::bridge::PendingPromises,
2385) -> EventLoopStatus {
2386 match cmd {
2387 SessionCommand::Message(frame) => dispatch_event_loop_frame(scope, frame, pending),
2388 SessionCommand::SetModuleReader(reader) => {
2389 execution::install_session_guest_reader(Some(reader));
2390 EventLoopStatus::Completed
2391 }
2392 SessionCommand::Shutdown => EventLoopStatus::Terminated,
2393 }
2394}
2395
2396fn pending_guest_timer_count(scope: &mut v8::HandleScope) -> usize {
2397 let tc = &mut v8::TryCatch::new(scope);
2398 let context = tc.get_current_context();
2399 let global = context.global(tc);
2400 let key = match v8::String::new(tc, "_getPendingTimerCount") {
2401 Some(key) => key,
2402 None => return 0,
2403 };
2404 let Some(func_value) = global.get(tc, key.into()) else {
2405 return 0;
2406 };
2407 let Ok(func) = v8::Local::<v8::Function>::try_from(func_value) else {
2408 return 0;
2409 };
2410 let Some(result) = func.call(tc, global.into(), &[]) else {
2411 return 0;
2412 };
2413
2414 result
2415 .integer_value(tc)
2416 .and_then(|count| usize::try_from(count).ok())
2417 .unwrap_or(0)
2418}
2419
2420fn pending_guest_immediate_count(scope: &mut v8::HandleScope) -> usize {
2421 let tc = &mut v8::TryCatch::new(scope);
2422 let context = tc.get_current_context();
2423 let global = context.global(tc);
2424 let key = match v8::String::new(tc, "_getPendingImmediateCount") {
2425 Some(key) => key,
2426 None => return 0,
2427 };
2428 let Some(func_value) = global.get(tc, key.into()) else {
2429 return 0;
2430 };
2431 let Ok(func) = v8::Local::<v8::Function>::try_from(func_value) else {
2432 return 0;
2433 };
2434 let Some(result) = func.call(tc, global.into(), &[]) else {
2435 return 0;
2436 };
2437
2438 result
2439 .integer_value(tc)
2440 .and_then(|count| usize::try_from(count).ok())
2441 .unwrap_or(0)
2442}
2443
2444fn drain_guest_immediates(scope: &mut v8::HandleScope) -> EventLoopStatus {
2445 let tc = &mut v8::TryCatch::new(scope);
2446 let context = tc.get_current_context();
2447 let global = context.global(tc);
2448 let key = match v8::String::new(tc, "_drainImmediates") {
2449 Some(key) => key,
2450 None => return EventLoopStatus::Completed,
2451 };
2452 let Some(func_value) = global.get(tc, key.into()) else {
2453 return EventLoopStatus::Completed;
2454 };
2455 let Ok(func) = v8::Local::<v8::Function>::try_from(func_value) else {
2456 return EventLoopStatus::Completed;
2457 };
2458 let _ = func.call(tc, global.into(), &[]);
2459 tc.perform_microtask_checkpoint();
2460 if let Some(exception) = tc.exception() {
2461 let (code, err) = execution::exception_to_result(tc, exception);
2462 return EventLoopStatus::Failed(code, err);
2463 }
2464 if let Some(err) = execution::take_unhandled_promise_rejection(tc) {
2465 return EventLoopStatus::Failed(1, err);
2466 }
2467 EventLoopStatus::Completed
2468}
2469
2470fn pump_v8_message_loop(scope: &mut v8::HandleScope) {
2471 let platform = v8::V8::get_current_platform();
2472 while v8::Platform::pump_message_loop(&platform, scope, false) {
2473 scope.perform_microtask_checkpoint();
2474 }
2475}
2476
2477#[derive(Debug)]
2480#[doc(hidden)]
2481pub enum EventLoopStatus {
2482 Completed,
2483 Terminated,
2484 Failed(i32, ExecutionError),
2485}
2486
2487fn dispatch_event_loop_frame(
2488 scope: &mut v8::HandleScope,
2489 frame: SessionMessage,
2490 pending: &crate::bridge::PendingPromises,
2491) -> EventLoopStatus {
2492 match frame {
2493 SessionMessage::BridgeResponse(BridgeResponse {
2494 call_id,
2495 status,
2496 payload,
2497 }) => {
2498 let (result, error) = if status == 1 {
2499 (None, Some(String::from_utf8_lossy(&payload).to_string()))
2500 } else if status == 2 || !payload.is_empty() {
2501 (Some(payload), None)
2503 } else {
2504 (None, None)
2505 };
2506 let _ = crate::bridge::resolve_pending_promise(
2507 scope, pending, call_id, status, result, error,
2508 );
2509 EventLoopStatus::Completed
2511 }
2512 SessionMessage::StreamEvent(StreamEvent {
2513 event_type,
2514 payload,
2515 }) => {
2516 let tc = &mut v8::TryCatch::new(scope);
2517 crate::stream::dispatch_stream_event(tc, &event_type, &payload);
2518 tc.perform_microtask_checkpoint();
2519 if let Some(exception) = tc.exception() {
2520 let (code, err) = execution::exception_to_result(tc, exception);
2521 return EventLoopStatus::Failed(code, err);
2522 }
2523 if let Some(err) = execution::take_unhandled_promise_rejection(tc) {
2524 return EventLoopStatus::Failed(1, err);
2525 }
2526 EventLoopStatus::Completed
2527 }
2528 SessionMessage::TerminateExecution => {
2529 scope.terminate_execution();
2530 EventLoopStatus::Terminated
2531 }
2532 _ => {
2533 EventLoopStatus::Completed
2535 }
2536 }
2537}
2538
2539pub(crate) struct ChannelResponseReceiver {
2549 rx: Receiver<SessionCommand>,
2550 abort_rx: Option<crossbeam_channel::Receiver<()>>,
2551 deferred: DeferredQueue,
2552}
2553
2554impl ChannelResponseReceiver {
2555 #[allow(dead_code)]
2556 pub(crate) fn new(rx: Receiver<SessionCommand>, deferred: DeferredQueue) -> Self {
2557 ChannelResponseReceiver {
2558 rx,
2559 abort_rx: None,
2560 deferred,
2561 }
2562 }
2563
2564 pub(crate) fn with_abort(
2565 rx: Receiver<SessionCommand>,
2566 abort_rx: crossbeam_channel::Receiver<()>,
2567 deferred: DeferredQueue,
2568 ) -> Self {
2569 ChannelResponseReceiver {
2570 rx,
2571 abort_rx: Some(abort_rx),
2572 deferred,
2573 }
2574 }
2575}
2576
2577impl crate::host_call::BridgeResponseReceiver for ChannelResponseReceiver {
2578 fn recv_response(&self, expected_call_id: u64) -> Result<BridgeResponse, String> {
2579 loop {
2580 let cmd = if let Some(ref abort) = self.abort_rx {
2582 crossbeam_channel::select! {
2583 recv(self.rx) -> result => match result {
2584 Ok(cmd) => cmd,
2585 Err(_) => return Err("channel closed".into()),
2586 },
2587 recv(abort) -> _ => {
2588 return Err("execution aborted".into());
2589 },
2590 }
2591 } else {
2592 match self.rx.recv() {
2593 Ok(cmd) => cmd,
2594 Err(_) => return Err("channel closed".into()),
2595 }
2596 };
2597
2598 match cmd {
2599 SessionCommand::Message(frame) => {
2600 if let SessionMessage::BridgeResponse(response) = &frame {
2601 let call_id = response.call_id;
2602 if call_id == expected_call_id {
2603 crate::host_call::record_sync_bridge_response_channel_received(call_id);
2604 return Ok(response.clone());
2605 }
2606 push_deferred_sync_message(&self.deferred, frame)?;
2607 continue;
2608 }
2609 push_deferred_sync_message(&self.deferred, frame)?;
2611 }
2612 SessionCommand::SetModuleReader(reader) => {
2613 execution::install_session_guest_reader(Some(reader));
2614 }
2615 SessionCommand::Shutdown => return Err("session shutdown".into()),
2616 }
2617 }
2618 }
2619}
2620
2621fn push_deferred_sync_message(
2622 deferred: &DeferredQueue,
2623 frame: SessionMessage,
2624) -> Result<(), String> {
2625 let mut queue = deferred.lock().unwrap();
2626 if queue.len() >= MAX_DEFERRED_SYNC_MESSAGES {
2627 return Err(format!(
2628 "sync bridge deferred message queue exceeded limit of {MAX_DEFERRED_SYNC_MESSAGES}"
2629 ));
2630 }
2631 queue.push_back(frame);
2632 Ok(())
2633}
2634
2635#[cfg(test)]
2636mod tests {
2637 use super::*;
2638 use std::collections::{HashMap, HashSet};
2639
2640 fn test_manager(max: usize) -> SessionManager {
2642 test_manager_with_events(max).0
2643 }
2644
2645 fn test_manager_with_events(max: usize) -> (SessionManager, Receiver<RuntimeEventEnvelope>) {
2646 let (tx, _rx) = crossbeam_channel::unbounded();
2647 let router: CallIdRouter = Arc::new(Mutex::new(HashMap::new()));
2648 let snap_cache = Arc::new(SnapshotCache::new(4));
2649 let manager = SessionManager::new(max, tx, router, snap_cache);
2650 (manager, _rx)
2651 }
2652
2653 #[test]
2654 fn zero_cpu_time_limit_is_normalized_to_no_timeout() {
2655 assert_eq!(normalize_cpu_time_limit_ms(None), None);
2656 assert_eq!(normalize_cpu_time_limit_ms(Some(0)), None);
2657 assert_eq!(normalize_cpu_time_limit_ms(Some(1)), Some(1));
2658 }
2659
2660 fn expect_late_message_warning(
2661 rx: &Receiver<RuntimeEventEnvelope>,
2662 session_id: &str,
2663 error_code: &str,
2664 detail_fragment: &str,
2665 ) {
2666 let event = rx
2667 .recv_timeout(std::time::Duration::from_millis(200))
2668 .expect("late-message warning");
2669 match event.event {
2670 RuntimeEvent::Log {
2671 session_id: observed_session_id,
2672 channel,
2673 message,
2674 } => {
2675 assert_eq!(observed_session_id, session_id);
2676 assert_eq!(channel, 1, "late warnings should use stderr channel");
2677 assert!(
2678 message.contains(error_code),
2679 "warning should contain error code {error_code}, got {message}"
2680 );
2681 assert!(
2682 message.contains(detail_fragment),
2683 "warning should mention {detail_fragment}, got {message}"
2684 );
2685 }
2686 other => panic!("expected late-message warning log, got {other:?}"),
2687 }
2688 }
2689
2690 #[test]
2691 fn bridge_contract_function_partitions_cover_contract() {
2692 let contract = bridge_contract();
2693
2694 let expected_sync = contract
2695 .groups
2696 .iter()
2697 .filter(|group| {
2698 matches!(
2699 group.convention,
2700 BridgeCallConvention::Sync | BridgeCallConvention::SyncPromise
2701 )
2702 })
2703 .flat_map(|group| group.names.iter().map(String::as_str))
2704 .collect::<HashSet<_>>();
2705 let expected_async = contract
2706 .groups
2707 .iter()
2708 .filter(|group| group.convention == BridgeCallConvention::Async)
2709 .flat_map(|group| group.names.iter().map(String::as_str))
2710 .collect::<HashSet<_>>();
2711
2712 let sync_names = sync_bridge_fns();
2713 let async_names = async_bridge_fns();
2714 let registered_sync = sync_names.iter().copied().collect::<HashSet<_>>();
2715 let registered_async = async_names.iter().copied().collect::<HashSet<_>>();
2716
2717 assert_eq!(
2718 registered_sync, expected_sync,
2719 "sync bridge function partition drifted from crates/bridge/bridge-contract.json"
2720 );
2721 assert_eq!(
2722 registered_async, expected_async,
2723 "async bridge function partition drifted from crates/bridge/bridge-contract.json"
2724 );
2725 assert!(
2726 registered_sync.is_disjoint(®istered_async),
2727 "sync and async bridge function partitions must not overlap"
2728 );
2729 }
2730
2731 #[test]
2732 fn session_management() {
2733 {
2738 let mut mgr = test_manager(4);
2739
2740 mgr.create_session("session-aaa".into(), None, None, None)
2741 .expect("create session A");
2742 assert_eq!(mgr.session_count(), 1);
2743
2744 std::thread::sleep(std::time::Duration::from_millis(200));
2746
2747 mgr.destroy_session("session-aaa")
2749 .expect("destroy session A");
2750 assert_eq!(mgr.session_count(), 0);
2751 }
2752
2753 {
2755 let mut mgr = test_manager(4);
2756
2757 mgr.create_session("session-bbb".into(), None, None, None)
2758 .expect("create session B");
2759 mgr.create_session("session-ccc".into(), Some(16), None, None)
2760 .expect("create session C");
2761 assert_eq!(mgr.session_count(), 2);
2762
2763 std::thread::sleep(std::time::Duration::from_millis(200));
2764
2765 let err = mgr.create_session("session-bbb".into(), None, None, None);
2767 assert!(err.is_err());
2768 assert!(err.unwrap_err().contains("already exists"));
2769
2770 let err = mgr.send_to_session("missing", SessionMessage::TerminateExecution);
2772 assert!(err.is_err());
2773 assert!(err.unwrap_err().contains("does not exist"));
2774
2775 let err = mgr.destroy_session("no-such-session");
2777 assert!(err.is_err());
2778 assert!(err.unwrap_err().contains("does not exist"));
2779
2780 mgr.destroy_sessions(["session-bbb".into(), "session-ccc".into()]);
2781 assert_eq!(mgr.session_count(), 0);
2782 }
2783
2784 {
2786 let mut mgr = test_manager(2);
2787
2788 mgr.create_session("s1".into(), None, None, None)
2789 .expect("create s1");
2790 mgr.create_session("s2".into(), None, None, None)
2791 .expect("create s2");
2792 mgr.create_session("s3".into(), None, None, None)
2793 .expect("create s3");
2794
2795 std::thread::sleep(std::time::Duration::from_millis(300));
2797
2798 assert_eq!(mgr.active_slot_count(), 2);
2800 assert_eq!(mgr.session_count(), 3);
2801
2802 mgr.destroy_session("s1").expect("destroy s1");
2804 std::thread::sleep(std::time::Duration::from_millis(300));
2805 assert_eq!(mgr.active_slot_count(), 2);
2806 assert_eq!(mgr.session_count(), 2);
2807
2808 mgr.destroy_sessions(["s2".into(), "s3".into()]);
2810 std::thread::sleep(std::time::Duration::from_millis(100));
2811 assert_eq!(mgr.session_count(), 0);
2812 assert_eq!(mgr.active_slot_count(), 0);
2813 }
2814 }
2815
2816 #[test]
2817 fn detach_session_clears_call_id_routes_for_session() {
2818 let mut mgr = test_manager(1);
2819 mgr.create_session_with_output_generation(
2820 "session-route".into(),
2821 None,
2822 None,
2823 None,
2824 Some(7),
2825 None,
2826 )
2827 .expect("create session");
2828 mgr.call_id_router()
2829 .lock()
2830 .expect("call_id router")
2831 .insert(42, "session-route".into());
2832
2833 assert!(
2834 mgr.detach_session_if_output_generation("session-route", 7)
2835 .expect("detach session"),
2836 "matching output generation should detach session"
2837 );
2838 assert!(
2839 mgr.call_id_router()
2840 .lock()
2841 .expect("call_id router")
2842 .get(&42)
2843 .is_none(),
2844 "detach should clear stale bridge call routes for the session"
2845 );
2846 }
2847
2848 #[test]
2849 fn begin_destroy_session_removes_entry_before_finish() {
2850 let mut mgr = test_manager(1);
2851 mgr.create_session("two-phase".into(), None, None, None)
2852 .expect("create session");
2853
2854 let first_shutdown = mgr
2855 .begin_destroy_session("two-phase")
2856 .expect("begin destroy session");
2857 assert_eq!(
2858 mgr.session_count(),
2859 0,
2860 "entry should be removed before the shutdown is finished"
2861 );
2862
2863 mgr.create_session("two-phase".into(), None, None, None)
2866 .expect("re-create session while first shutdown is unfinished");
2867
2868 let second_shutdown = mgr
2869 .begin_destroy_session("two-phase")
2870 .expect("begin destroy re-created session");
2871 first_shutdown.finish();
2872 second_shutdown.finish();
2873 assert_eq!(mgr.session_count(), 0);
2874 }
2875
2876 #[test]
2877 fn session_shutdown_finish_clears_late_call_routes() {
2878 let mut mgr = test_manager(1);
2879 mgr.create_session("late-route".into(), None, None, None)
2880 .expect("create session");
2881
2882 let shutdown = mgr
2883 .begin_destroy_session("late-route")
2884 .expect("begin destroy session");
2885 mgr.call_id_router()
2888 .lock()
2889 .expect("call_id router")
2890 .insert(42, "late-route".into());
2891
2892 shutdown.finish();
2893 assert!(
2894 mgr.call_id_router()
2895 .lock()
2896 .expect("call_id router")
2897 .get(&42)
2898 .is_none(),
2899 "finish should clear call routes registered during shutdown"
2900 );
2901 }
2902
2903 #[test]
2904 fn channel_response_receiver_filters_bridge_response() {
2905 use crate::host_call::BridgeResponseReceiver;
2906
2907 let (tx, rx) = crossbeam_channel::bounded(10);
2909 let deferred = new_deferred_queue();
2910 let receiver = ChannelResponseReceiver::new(rx, Arc::clone(&deferred));
2911
2912 tx.send(SessionCommand::Message(SessionMessage::StreamEvent(
2914 StreamEvent {
2915 event_type: "child_stdout".into(),
2916 payload: vec![0x01, 0x02],
2917 },
2918 )))
2919 .unwrap();
2920 tx.send(SessionCommand::Message(SessionMessage::TerminateExecution))
2921 .unwrap();
2922 tx.send(SessionCommand::Message(SessionMessage::BridgeResponse(
2923 BridgeResponse {
2924 call_id: 1,
2925 status: 0,
2926 payload: vec![0xAB],
2927 },
2928 )))
2929 .unwrap();
2930
2931 let frame = receiver.recv_response(1).unwrap();
2933 assert!(
2934 frame.call_id == 1,
2935 "expected BridgeResponse with call_id=1, got {:?}",
2936 frame
2937 );
2938
2939 let dq = deferred.lock().unwrap();
2941 assert_eq!(dq.len(), 2, "expected 2 deferred messages");
2942 assert!(
2943 matches!(&dq[0], SessionMessage::StreamEvent(StreamEvent { event_type, .. }) if event_type == "child_stdout"),
2944 "first deferred should be StreamEvent"
2945 );
2946 assert!(
2947 matches!(&dq[1], SessionMessage::TerminateExecution),
2948 "second deferred should be TerminateExecution"
2949 );
2950 }
2951
2952 #[test]
2953 fn channel_response_receiver_rejects_deferred_queue_overflow() {
2954 use crate::host_call::BridgeResponseReceiver;
2955
2956 let (tx, rx) = crossbeam_channel::bounded(MAX_DEFERRED_SYNC_MESSAGES + 1);
2957 let deferred = new_deferred_queue();
2958 let receiver = ChannelResponseReceiver::new(rx, Arc::clone(&deferred));
2959
2960 for index in 0..=MAX_DEFERRED_SYNC_MESSAGES {
2961 tx.send(SessionCommand::Message(SessionMessage::StreamEvent(
2962 StreamEvent {
2963 event_type: format!("child_stdout_{index}"),
2964 payload: Vec::new(),
2965 },
2966 )))
2967 .unwrap();
2968 }
2969
2970 let error = receiver
2971 .recv_response(1)
2972 .expect_err("deferred queue overflow should reject sync bridge wait");
2973 assert!(error.contains("deferred message queue exceeded limit"));
2974 assert_eq!(deferred.lock().unwrap().len(), MAX_DEFERRED_SYNC_MESSAGES);
2975 }
2976
2977 #[test]
2978 fn pre_slot_deferred_command_overflow_is_bounded_and_logged() {
2979 let (event_tx, event_rx) = crossbeam_channel::unbounded();
2980 let mut deferred_commands = VecDeque::new();
2981
2982 for _ in 0..MAX_DEFERRED_SESSION_COMMANDS {
2983 assert!(defer_session_command_before_slot(
2984 &mut deferred_commands,
2985 &event_tx,
2986 "queued-session",
2987 Some(3),
2988 SessionCommand::Message(SessionMessage::TerminateExecution),
2989 ));
2990 }
2991
2992 assert!(!defer_session_command_before_slot(
2993 &mut deferred_commands,
2994 &event_tx,
2995 "queued-session",
2996 Some(3),
2997 SessionCommand::Message(SessionMessage::TerminateExecution),
2998 ));
2999 assert_eq!(deferred_commands.len(), MAX_DEFERRED_SESSION_COMMANDS);
3000
3001 let warning = event_rx.recv().expect("overflow warning");
3002 assert_eq!(warning.output_generation, Some(3));
3003 match warning.event {
3004 RuntimeEvent::Log {
3005 session_id,
3006 channel,
3007 message,
3008 } => {
3009 assert_eq!(session_id, "queued-session");
3010 assert_eq!(channel, 1);
3011 assert!(message.contains(DEFERRED_COMMAND_LIMIT_ERROR_CODE));
3012 }
3013 other => panic!("expected overflow warning log, got {other:?}"),
3014 }
3015 }
3016
3017 #[test]
3018 fn late_terminate_execution_is_logged_instead_of_silently_dropped() {
3019 let (mut mgr, rx) = test_manager_with_events(1);
3020 mgr.create_session("late-terminate".into(), None, None, None)
3021 .expect("create session");
3022
3023 mgr.send_to_session("late-terminate", SessionMessage::TerminateExecution)
3024 .expect("send late terminate");
3025
3026 expect_late_message_warning(
3027 &rx,
3028 "late-terminate",
3029 LATE_TERMINATE_EXECUTION_ERROR_CODE,
3030 "TerminateExecution",
3031 );
3032
3033 mgr.destroy_session("late-terminate")
3034 .expect("destroy session");
3035 }
3036
3037 #[test]
3038 fn channel_response_receiver_abort_unblocks_waiting_sync_call() {
3039 use crate::host_call::BridgeResponseReceiver;
3040
3041 let (_tx, rx) = crossbeam_channel::bounded(1);
3042 let deferred = new_deferred_queue();
3043 let execution_abort = new_execution_abort();
3044 let (_active_abort, abort_rx) = ActiveExecutionAbort::arm(&execution_abort);
3045 let receiver = ChannelResponseReceiver::with_abort(rx, abort_rx, deferred);
3046
3047 let (result_tx, result_rx) = std::sync::mpsc::channel();
3048 let join_handle = std::thread::spawn(move || {
3049 let _ = result_tx.send(receiver.recv_response(1));
3050 });
3051
3052 std::thread::sleep(std::time::Duration::from_millis(25));
3053 signal_execution_abort(&execution_abort, ExecutionAbortReason::Terminated);
3054
3055 let result = result_rx
3056 .recv_timeout(std::time::Duration::from_secs(1))
3057 .expect("abort should unblock the waiting receiver");
3058 assert_eq!(
3059 result.expect_err("abort should not yield a bridge response"),
3060 "execution aborted"
3061 );
3062
3063 join_handle
3064 .join()
3065 .expect("receiver thread should exit cleanly");
3066 }
3067
3068 #[test]
3069 fn late_bridge_response_is_logged_instead_of_silently_dropped() {
3070 let (mut mgr, rx) = test_manager_with_events(1);
3071 mgr.create_session("late-bridge".into(), None, None, None)
3072 .expect("create session");
3073
3074 mgr.send_to_session(
3075 "late-bridge",
3076 SessionMessage::BridgeResponse(BridgeResponse {
3077 call_id: 41,
3078 status: 0,
3079 payload: vec![0xAA, 0xBB],
3080 }),
3081 )
3082 .expect("send late bridge response");
3083
3084 expect_late_message_warning(
3085 &rx,
3086 "late-bridge",
3087 LATE_BRIDGE_RESPONSE_ERROR_CODE,
3088 "BridgeResponse",
3089 );
3090
3091 mgr.destroy_session("late-bridge").expect("destroy session");
3092 }
3093
3094 #[test]
3107 fn reset_pending_promises_drops_resolver_globals_before_isolate_teardown() {
3108 use crate::bridge::{register_async_bridge_fns, PendingPromises};
3109 use crate::host_call::BridgeCallContext;
3110 use crate::isolate;
3111 use std::process::Command;
3112
3113 const SUBPROCESS_ENV: &str = "AGENTOS_V8_RESET_PENDING_PROMISES_SUBPROCESS";
3118 if std::env::var_os(SUBPROCESS_ENV).is_none() {
3119 let output = Command::new(std::env::current_exe().expect("current test binary"))
3120 .arg("session::tests::reset_pending_promises_drops_resolver_globals_before_isolate_teardown")
3121 .arg("--exact")
3122 .arg("--nocapture")
3123 .env(SUBPROCESS_ENV, "1")
3124 .output()
3125 .expect("spawn reset-pending-promises subprocess");
3126 assert!(
3127 output.status.success(),
3128 "reset-pending-promises subprocess failed with status {:?}\nstdout:\n{}\nstderr:\n{}",
3129 output.status.code(),
3130 String::from_utf8_lossy(&output.stdout),
3131 String::from_utf8_lossy(&output.stderr)
3132 );
3133 return;
3134 }
3135
3136 isolate::init_v8_platform();
3137
3138 let mut v8_isolate = isolate::create_isolate(None);
3139 let context = isolate::create_context(&mut v8_isolate);
3140 let scope = &mut v8::HandleScope::new(&mut v8_isolate);
3141 let context = v8::Local::new(scope, &context);
3142 let scope = &mut v8::ContextScope::new(scope, context);
3143
3144 let bridge_ctx = BridgeCallContext::new(
3145 Box::new(std::io::sink()),
3146 Box::new(std::io::empty()),
3147 String::from("reset-pending-test"),
3148 );
3149 let mut pending = PendingPromises::new();
3150
3151 const REGISTERED: usize = 8;
3156 let _async_fns = register_async_bridge_fns(
3157 scope,
3158 &bridge_ctx as *const BridgeCallContext,
3159 &pending as *const PendingPromises,
3160 &["_asyncFn"],
3161 );
3162 let source = format!("for (let i = 0; i < {REGISTERED}; i++) {{ _asyncFn(i); }}");
3163 {
3164 let tc = &mut v8::TryCatch::new(scope);
3165 let code = v8::String::new(tc, &source).unwrap();
3166 let script = v8::Script::compile(tc, code, None).unwrap();
3167 assert!(
3168 script.run(tc).is_some(),
3169 "async bridge calls should register resolvers, not throw"
3170 );
3171 assert!(!tc.has_caught(), "async bridge calls should not throw");
3172 }
3173 assert_eq!(
3174 pending.len(),
3175 REGISTERED,
3176 "each _asyncFn call must register a pending resolver Global"
3177 );
3178
3179 reset_pending_promises(&mut pending);
3183
3184 assert_eq!(
3185 pending.len(),
3186 0,
3187 "reset_pending_promises must drop all pending resolver Globals before isolate teardown"
3188 );
3189
3190 }
3193}