1use std::sync::atomic::{Ordering, fence};
18use std::sync::{Arc, Condvar, Mutex};
19use std::thread::{self, JoinHandle};
20use std::time::{Duration, Instant};
21
22use javm_cap::cap::Cap;
23use nub_arch_x86_abi::{
24 CapHash as AbiCapHash, FN_ID_NUB_INVOKE_WORKER, FN_ID_NUB_PUT_CAP, InvocationResult,
25 InvokePacket, PARALLEL_INVOKE_STATUS_DONE, PARALLEL_INVOKE_STATUS_EMPTY,
26 PARALLEL_INVOKE_STATUS_EVICT_JIT_READY, PARALLEL_INVOKE_STATUS_READY,
27 PARALLEL_INVOKE_STATUS_RUNNING, PARALLEL_INVOKE_STATUS_STARTING, PARALLEL_INVOKE_STATUS_STOP,
28};
29use nub_host_common::rpc::{ArchivedResponse, Request};
30use rkyv::util::AlignedVec;
31use std::collections::HashSet;
32use tracing::{Span, instrument};
33
34use super::host_funcs::FunctionRegistry;
35use crate::HyperlightError;
36use crate::Result;
37use crate::hypervisor::InterruptHandle;
38use crate::hypervisor::hyperlight_vm::HyperlightVm;
39use crate::hypervisor::virtual_machine::VcpuLane;
40use crate::mem::mgr::SandboxMemoryManager;
41use crate::mem::shared_mem::HostSharedMemory;
42use crate::metrics::{
43 METRIC_GUEST_ERROR, METRIC_GUEST_ERROR_LABEL_CODE, maybe_time_and_emit_guest_call,
44};
45
46pub struct MultiUseSandbox {
55 id: u64,
57 pub(crate) host_funcs: Arc<Mutex<FunctionRegistry>>,
58 pub(crate) mem_mgr: Arc<Mutex<SandboxMemoryManager<HostSharedMemory>>>,
59 vm: Arc<HyperlightVm>,
60 control_lock: Mutex<()>,
61 invoke_workers: Mutex<Option<Arc<ParallelInvokeWorkers>>>,
62 published_blobs: Mutex<HashSet<AbiCapHash>>,
84}
85
86impl MultiUseSandbox {
87 #[instrument(skip_all, parent = Span::current(), level = "Trace")]
93 pub(super) fn from_uninit(
94 host_funcs: Arc<Mutex<FunctionRegistry>>,
95 mgr: SandboxMemoryManager<HostSharedMemory>,
96 vm: HyperlightVm,
97 ) -> MultiUseSandbox {
98 Self {
99 id: super::snapshot::SANDBOX_CONFIGURATION_COUNTER.fetch_add(1, Ordering::Relaxed),
100 host_funcs,
101 mem_mgr: Arc::new(Mutex::new(mgr)),
102 vm: Arc::new(vm),
103 control_lock: Mutex::new(()),
104 invoke_workers: Mutex::new(None),
105 published_blobs: Mutex::new(HashSet::new()),
106 }
107 }
108
109 pub fn id(&self) -> u64 {
111 self.id
112 }
113
114 pub fn vcpu_count(&self) -> Result<usize> {
116 let mem_mgr = self
117 .mem_mgr
118 .lock()
119 .map_err(|_| crate::new_error!("sandbox memory manager mutex poisoned"))?;
120 Ok(mem_mgr.layout.get_vcpu_count())
121 }
122
123 #[instrument(err(Debug), skip(self, payload), parent = Span::current())]
135 pub fn call_raw(&self, fn_id: u32, payload: &[u8]) -> Result<Vec<u8>> {
136 maybe_time_and_emit_guest_call("call_raw", || {
137 let mut workers = self
138 .invoke_workers
139 .lock()
140 .map_err(|_| crate::new_error!("parallel invoke worker mutex poisoned"))?;
141 self.stop_invoke_workers_locked(&mut workers)?;
142 let _control = self
143 .control_lock
144 .lock()
145 .map_err(|_| crate::new_error!("sandbox control mutex poisoned"))?;
146 self.call_guest_function_by_id_on_locked(VcpuLane::PRIMARY, fn_id, payload)
147 })
148 }
149
150 #[instrument(err(Debug), skip(self, payload), parent = Span::current())]
155 pub fn call_raw_on_vcpu(
156 &self,
157 vcpu_index: usize,
158 fn_id: u32,
159 payload: &[u8],
160 ) -> Result<Vec<u8>> {
161 let lane = VcpuLane::new(vcpu_index);
162 maybe_time_and_emit_guest_call("call_raw_on_vcpu", || {
163 let mut workers = self
164 .invoke_workers
165 .lock()
166 .map_err(|_| crate::new_error!("parallel invoke worker mutex poisoned"))?;
167 self.stop_invoke_workers_locked(&mut workers)?;
168 let _control = self
169 .control_lock
170 .lock()
171 .map_err(|_| crate::new_error!("sandbox control mutex poisoned"))?;
172 self.call_guest_function_by_id_on_locked(lane, fn_id, payload)
173 })
174 }
175
176 fn call_guest_function_by_id_on_locked(
177 &self,
178 lane: VcpuLane,
179 fn_id: u32,
180 payload: &[u8],
181 ) -> Result<Vec<u8>> {
182 self.vm.clear_cancel();
186
187 let res = (|| {
188 let req = Request {
189 fn_id,
190 payload: payload.to_vec(),
191 };
192 let req_bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&req)
193 .map_err(|e| crate::new_error!("rkyv-serialize Request: {e}"))?;
194
195 let mut mem_mgr = self
196 .mem_mgr
197 .lock()
198 .map_err(|_| crate::new_error!("sandbox memory manager mutex poisoned"))?;
199
200 mem_mgr.write_guest_function_call_raw(req_bytes.as_slice())?;
201
202 let dispatch_res = if lane == VcpuLane::PRIMARY {
203 self.vm
204 .dispatch_call_from_host(&mut mem_mgr, &self.host_funcs)
205 } else {
206 self.vm
207 .dispatch_call_from_host_on(lane, &mut mem_mgr, &self.host_funcs)
208 };
209
210 if let Err(e) = dispatch_res {
211 let (error, _should_poison) = e.promote();
212 return Err(error);
213 }
214
215 let raw_resp = mem_mgr.read_guest_function_call_result_raw()?;
216
217 let mut aligned = AlignedVec::<16>::with_capacity(raw_resp.len());
218 aligned.extend_from_slice(&raw_resp);
219
220 let resp = rkyv::access::<ArchivedResponse, rkyv::rancor::Error>(aligned.as_slice())
221 .map_err(|e| crate::new_error!("rkyv-access Response: {e}"))?;
222
223 let status = resp.status.to_native();
224 if status != 0 {
225 let msg = resp
226 .error_msg
227 .as_ref()
228 .map(|s| s.as_str().to_string())
229 .unwrap_or_else(|| format!("guest fn_id={fn_id} returned status {status}"));
230 metrics::counter!(
231 METRIC_GUEST_ERROR,
232 METRIC_GUEST_ERROR_LABEL_CODE => status.to_string()
233 )
234 .increment(1);
235 return Err(HyperlightError::GuestError(
236 hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode::GuestError,
237 msg,
238 ));
239 }
240
241 Ok(resp.payload.as_slice().to_vec())
242 })();
243
244 let mut mem_mgr = self
246 .mem_mgr
247 .lock()
248 .map_err(|_| crate::new_error!("sandbox memory manager mutex poisoned"))?;
249 mem_mgr.abort_buffer.clear();
250
251 if res.is_err() {
252 mem_mgr.clear_io_buffers();
253 }
254
255 res
256 }
257
258 pub fn invoke_cached_parallel(
266 &self,
267 job_id: u64,
268 packet: &InvokePacket,
269 ) -> Result<InvocationResult> {
270 let (workers, lane) = loop {
271 let workers = self.ensure_invoke_workers()?;
272 if let Some(lane) = workers.try_acquire_lane()? {
273 break (workers, lane);
274 }
275 if let Some(lane_idx) = workers.reserve_unstarted_lane()? {
276 match self.start_invoke_worker_lane(lane_idx) {
277 Ok(handle) => workers.install_started_lane(lane_idx, handle)?,
278 Err(e) => {
279 workers.release_start_reservation(lane_idx)?;
280 return Err(e);
281 }
282 }
283 continue;
284 }
285 if let Some(lane) = workers.acquire_lane()? {
286 break (workers, lane);
287 }
288 };
289 let lane_idx = lane.index();
290
291 let slot = {
292 let mem_mgr = self
293 .mem_mgr
294 .lock()
295 .map_err(|_| crate::new_error!("sandbox memory manager mutex poisoned"))?;
296 mem_mgr.parallel_invoke_slot_host_ptr(lane_idx)?
297 };
298
299 unsafe {
303 let status = (*slot).status.load(Ordering::Acquire);
304 if status != PARALLEL_INVOKE_STATUS_EMPTY {
305 return Err(crate::new_error!(
306 "parallel invoke lane {} was not empty before submit (status={})",
307 lane_idx,
308 status
309 ));
310 }
311 (*slot).job_id.store(job_id, Ordering::Relaxed);
312 core::ptr::addr_of_mut!((*slot).packet).write_volatile(*packet);
313 fence(Ordering::Release);
314 (*slot)
315 .status
316 .store(PARALLEL_INVOKE_STATUS_READY, Ordering::Release);
317 }
318
319 loop {
320 let done = unsafe {
321 match (*slot).status.load(Ordering::Acquire) {
322 PARALLEL_INVOKE_STATUS_DONE => {
323 fence(Ordering::Acquire);
324 let result = core::ptr::addr_of!((*slot).result).read_volatile();
325 (*slot)
326 .status
327 .store(PARALLEL_INVOKE_STATUS_EMPTY, Ordering::Release);
328 Some(result)
329 }
330 PARALLEL_INVOKE_STATUS_READY | PARALLEL_INVOKE_STATUS_RUNNING => None,
331 other => {
332 return Err(crate::new_error!(
333 "parallel invoke lane {} entered unexpected status {}",
334 lane_idx,
335 other
336 ));
337 }
338 }
339 };
340 if let Some(result) = done {
341 return Ok(result);
342 }
343 if let Some(worker_result) = workers.take_finished_result(lane_idx) {
344 let detail = match worker_result {
345 Ok(()) => "clean worker exit".to_string(),
346 Err(e) => e,
347 };
348 return Err(crate::new_error!(
349 "parallel invoke worker lane {} exited while job {} was pending: {}",
350 lane_idx,
351 job_id,
352 detail
353 ));
354 }
355 thread::yield_now();
356 }
357 }
358
359 pub fn evict_jit_all_parallel(&self) -> Result<()> {
367 maybe_time_and_emit_guest_call("evict_jit_all_parallel", || {
368 let workers = self.ensure_invoke_workers()?;
369 let lanes = workers.acquire_all_lanes()?;
370 let Some(control_lane) = lanes.first().map(LaneLease::index) else {
371 return Ok(());
372 };
373
374 let slot = {
375 let mem_mgr = self
376 .mem_mgr
377 .lock()
378 .map_err(|_| crate::new_error!("sandbox memory manager mutex poisoned"))?;
379 mem_mgr.parallel_invoke_slot_host_ptr(control_lane)?
380 };
381
382 unsafe {
386 let status = (*slot).status.load(Ordering::Acquire);
387 if status != PARALLEL_INVOKE_STATUS_EMPTY {
388 return Err(crate::new_error!(
389 "parallel control lane {} was not empty before evict (status={})",
390 control_lane,
391 status
392 ));
393 }
394 (*slot).job_id.store(0, Ordering::Relaxed);
395 fence(Ordering::Release);
396 (*slot)
397 .status
398 .store(PARALLEL_INVOKE_STATUS_EVICT_JIT_READY, Ordering::Release);
399 }
400
401 loop {
402 let done = unsafe {
403 match (*slot).status.load(Ordering::Acquire) {
404 PARALLEL_INVOKE_STATUS_DONE => {
405 fence(Ordering::Acquire);
406 (*slot)
407 .status
408 .store(PARALLEL_INVOKE_STATUS_EMPTY, Ordering::Release);
409 true
410 }
411 PARALLEL_INVOKE_STATUS_EVICT_JIT_READY | PARALLEL_INVOKE_STATUS_RUNNING => {
412 false
413 }
414 other => {
415 return Err(crate::new_error!(
416 "parallel control lane {} entered unexpected status {}",
417 control_lane,
418 other
419 ));
420 }
421 }
422 };
423 if done {
424 return Ok(());
425 }
426 if let Some(worker_result) = workers.take_finished_result(control_lane) {
427 let detail = match worker_result {
428 Ok(()) => "clean worker exit".to_string(),
429 Err(e) => e,
430 };
431 return Err(crate::new_error!(
432 "parallel invoke worker lane {} exited during evict_jit_all: {}",
433 control_lane,
434 detail
435 ));
436 }
437 thread::yield_now();
438 }
439 })
440 }
441
442 fn ensure_invoke_workers(&self) -> Result<Arc<ParallelInvokeWorkers>> {
443 let mut guard = self
444 .invoke_workers
445 .lock()
446 .map_err(|_| crate::new_error!("parallel invoke worker mutex poisoned"))?;
447 if let Some(workers) = guard.as_ref().cloned() {
448 return Ok(workers);
449 }
450
451 let vcpu_count = self.vcpu_count()?;
452 let first_handle = self.start_invoke_worker_lane(0)?;
453 let workers = Arc::new(ParallelInvokeWorkers::new(vcpu_count, 0, first_handle));
454 *guard = Some(workers.clone());
455 Ok(workers)
456 }
457
458 fn stop_invoke_workers_locked(
459 &self,
460 guard: &mut Option<Arc<ParallelInvokeWorkers>>,
461 ) -> Result<()> {
462 let Some(workers) = guard.as_ref().cloned() else {
463 return Ok(());
464 };
465
466 let started_lanes = workers.mark_stopping_and_wait_idle()?;
467 for lane in started_lanes {
468 {
469 let mem_mgr = self
470 .mem_mgr
471 .lock()
472 .map_err(|_| crate::new_error!("sandbox memory manager mutex poisoned"))?;
473 mem_mgr.write_parallel_invoke_status(lane, PARALLEL_INVOKE_STATUS_STOP)?;
474 }
475 workers
476 .join_lane(lane, Duration::from_secs(5))?
477 .map_err(|e| crate::new_error!("parallel invoke worker lane {lane}: {e}"))?;
478 }
479
480 *guard = None;
481 Ok(())
482 }
483
484 fn start_invoke_worker_lane(&self, lane: usize) -> Result<InvokeWorkerHandle> {
485 let _control = self
486 .control_lock
487 .lock()
488 .map_err(|_| crate::new_error!("sandbox control mutex poisoned"))?;
489
490 {
491 let mut mem_mgr = self
492 .mem_mgr
493 .lock()
494 .map_err(|_| crate::new_error!("sandbox memory manager mutex poisoned"))?;
495 mem_mgr.write_parallel_invoke_status(lane, PARALLEL_INVOKE_STATUS_STARTING)?;
496 let req = Request {
497 fn_id: FN_ID_NUB_INVOKE_WORKER,
498 payload: (lane as u32).to_le_bytes().to_vec(),
499 };
500 let req_bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&req)
501 .map_err(|e| crate::new_error!("rkyv-serialize worker Request: {e}"))?;
502 mem_mgr.write_guest_function_call_raw(req_bytes.as_slice())?;
503 }
504
505 let vm = self.vm.clone();
506 let mem_mgr = self.mem_mgr.clone();
507 let host_funcs = self.host_funcs.clone();
508 let handle = thread::Builder::new()
509 .name(format!("nub-vcpu-worker-{lane}"))
510 .spawn(move || {
511 let lane = VcpuLane::new(lane);
512 vm.dispatch_call_from_host_on_shared(lane, &mem_mgr, &host_funcs)
513 .map_err(|e| format!("dispatch worker lane {}: {e}", lane.index()))?;
514 let mut mem_mgr = mem_mgr
515 .lock()
516 .map_err(|_| "sandbox memory manager mutex poisoned".to_string())?;
517 let _ = mem_mgr
518 .read_guest_function_call_result_raw()
519 .map_err(|e| format!("read worker shutdown response: {e}"))?;
520 Ok(())
521 })
522 .map_err(|e| crate::new_error!("spawn invoke worker lane {lane}: {e}"))?;
523
524 let deadline = Instant::now() + Duration::from_secs(5);
525 loop {
526 let status = {
527 let mem_mgr = self
528 .mem_mgr
529 .lock()
530 .map_err(|_| crate::new_error!("sandbox memory manager mutex poisoned"))?;
531 mem_mgr.read_parallel_invoke_status(lane)?
532 };
533 if status == PARALLEL_INVOKE_STATUS_EMPTY {
534 return Ok(handle);
535 }
536 if handle.is_finished() {
537 let worker_result = handle
538 .join()
539 .map_err(|_| crate::new_error!("invoke worker lane {lane} panicked"))?;
540 return Err(crate::new_error!(
541 "invoke worker lane {} exited during startup: {}",
542 lane,
543 worker_result
544 .err()
545 .unwrap_or_else(|| "clean exit before startup handshake".to_string())
546 ));
547 }
548 if Instant::now() >= deadline {
549 return Err(crate::new_error!(
550 "invoke worker lane {} timed out during startup (status={})",
551 lane,
552 status
553 ));
554 }
555 thread::yield_now();
556 }
557 }
558
559 pub fn put_cap(&self, cap: &Cap) -> Result<AbiCapHash> {
576 let cap_bytes = rkyv::to_bytes::<rkyv::rancor::Error>(cap)
577 .map_err(|e| crate::new_error!("put_cap: rkyv encode (or Ref present): {e}"))?;
578 let resp = self.call_raw(FN_ID_NUB_PUT_CAP, cap_bytes.as_slice())?;
579 if resp.len() != 32 {
580 return Err(crate::new_error!(
581 "put_cap: expected 32-byte hash response, got {}",
582 resp.len()
583 ));
584 }
585 let mut hash: AbiCapHash = [0u8; 32];
586 hash.copy_from_slice(&resp);
587 if hash == [0xFFu8; 32] {
591 return Err(crate::new_error!(
592 "put_cap: guest reported decode/conversion failure (sentinel response)"
593 ));
594 }
595 Ok(hash)
596 }
597
598 pub fn put_cap_with_hash(&self, hash: AbiCapHash, cap: &Cap) -> Result<()> {
618 {
619 let published_blobs = self
620 .published_blobs
621 .lock()
622 .map_err(|_| crate::new_error!("published blob set mutex poisoned"))?;
623 if published_blobs.contains(&hash) {
624 return Ok(());
625 }
626 }
627
628 let got = self.put_cap(cap)?;
629 debug_assert_eq!(
630 got, hash,
631 "put_cap_with_hash: guest-computed hash differs from claimed hash"
632 );
633
634 self.published_blobs
635 .lock()
636 .map_err(|_| crate::new_error!("published blob set mutex poisoned"))?
637 .insert(hash);
638 Ok(())
639 }
640
641 pub fn interrupt_handle(&self) -> Arc<dyn InterruptHandle> {
643 self.vm.interrupt_handle()
644 }
645}
646
647type InvokeWorkerResult = std::result::Result<(), String>;
648type InvokeWorkerHandle = JoinHandle<InvokeWorkerResult>;
649
650struct ParallelInvokeWorkers {
651 lane_count: usize,
652 state: Mutex<ParallelInvokeWorkerState>,
653 ready: Condvar,
654 handles: Mutex<Vec<Option<InvokeWorkerHandle>>>,
655}
656
657struct ParallelInvokeWorkerState {
658 available: Vec<usize>,
659 started: Vec<bool>,
660 stopping: bool,
661}
662
663struct LaneLease {
664 lane: usize,
665 workers: Arc<ParallelInvokeWorkers>,
666}
667
668impl ParallelInvokeWorkers {
669 fn new(lane_count: usize, first_lane: usize, first_handle: InvokeWorkerHandle) -> Self {
670 let mut handles = Vec::with_capacity(lane_count);
671 handles.resize_with(lane_count, || None);
672 handles[first_lane] = Some(first_handle);
673 let mut started = vec![false; lane_count];
674 started[first_lane] = true;
675 Self {
676 lane_count,
677 state: Mutex::new(ParallelInvokeWorkerState {
678 available: vec![first_lane],
679 started,
680 stopping: false,
681 }),
682 ready: Condvar::new(),
683 handles: Mutex::new(handles),
684 }
685 }
686
687 fn try_acquire_lane(self: &Arc<Self>) -> Result<Option<LaneLease>> {
688 let mut state = self
689 .state
690 .lock()
691 .map_err(|_| crate::new_error!("parallel worker mutex poisoned"))?;
692 if state.stopping {
693 return Ok(None);
694 }
695 Ok(state.available.pop().map(|lane| LaneLease {
696 lane,
697 workers: self.clone(),
698 }))
699 }
700
701 fn acquire_lane(self: &Arc<Self>) -> Result<Option<LaneLease>> {
702 let mut state = self
703 .state
704 .lock()
705 .map_err(|_| crate::new_error!("parallel worker mutex poisoned"))?;
706 loop {
707 if state.stopping {
708 return Ok(None);
709 }
710 if let Some(lane) = state.available.pop() {
711 return Ok(Some(LaneLease {
712 lane,
713 workers: self.clone(),
714 }));
715 }
716 state = self
717 .ready
718 .wait(state)
719 .map_err(|_| crate::new_error!("parallel worker mutex poisoned"))?;
720 }
721 }
722
723 fn reserve_unstarted_lane(&self) -> Result<Option<usize>> {
724 let mut state = self
725 .state
726 .lock()
727 .map_err(|_| crate::new_error!("parallel worker mutex poisoned"))?;
728 if state.stopping {
729 return Ok(None);
730 }
731 for lane in 0..self.lane_count {
732 if !state.started[lane] {
733 state.started[lane] = true;
734 return Ok(Some(lane));
735 }
736 }
737 Ok(None)
738 }
739
740 fn install_started_lane(&self, lane: usize, handle: InvokeWorkerHandle) -> Result<()> {
741 {
742 let mut handles = self
743 .handles
744 .lock()
745 .map_err(|_| crate::new_error!("parallel worker mutex poisoned"))?;
746 if lane >= handles.len() || handles[lane].is_some() {
747 return Err(crate::new_error!(
748 "parallel invoke worker lane {} already has a handle",
749 lane
750 ));
751 }
752 handles[lane] = Some(handle);
753 }
754 self.release_lane(lane);
755 Ok(())
756 }
757
758 fn release_start_reservation(&self, lane: usize) -> Result<()> {
759 let mut state = self
760 .state
761 .lock()
762 .map_err(|_| crate::new_error!("parallel worker mutex poisoned"))?;
763 if lane < state.started.len() {
764 state.started[lane] = false;
765 }
766 self.ready.notify_all();
767 Ok(())
768 }
769
770 fn acquire_all_lanes(self: &Arc<Self>) -> Result<Vec<LaneLease>> {
771 let mut state = self
772 .state
773 .lock()
774 .map_err(|_| crate::new_error!("parallel worker mutex poisoned"))?;
775 loop {
776 if state.stopping {
777 return Err(crate::new_error!("parallel invoke workers are stopping"));
778 }
779 let started_count = state.started.iter().filter(|&&started| started).count();
780 if state.available.len() == started_count {
781 return Ok(state
782 .available
783 .drain(..)
784 .map(|lane| LaneLease {
785 lane,
786 workers: self.clone(),
787 })
788 .collect());
789 }
790 state = self
791 .ready
792 .wait(state)
793 .map_err(|_| crate::new_error!("parallel worker mutex poisoned"))?;
794 }
795 }
796
797 fn release_lane(&self, lane: usize) {
798 let mut state = self.state.lock().expect("parallel worker mutex poisoned");
799 state.available.push(lane);
800 self.ready.notify_all();
801 }
802
803 fn mark_stopping_and_wait_idle(&self) -> Result<Vec<usize>> {
804 let mut state = self
805 .state
806 .lock()
807 .map_err(|_| crate::new_error!("parallel worker mutex poisoned"))?;
808 state.stopping = true;
809 self.ready.notify_all();
810 let started_count = state.started.iter().filter(|&&started| started).count();
811 while state.available.len() != started_count {
812 state = self
813 .ready
814 .wait(state)
815 .map_err(|_| crate::new_error!("parallel worker mutex poisoned"))?;
816 }
817 Ok(state
818 .started
819 .iter()
820 .enumerate()
821 .filter_map(|(lane, &started)| started.then_some(lane))
822 .collect())
823 }
824
825 fn take_finished_result(&self, lane: usize) -> Option<InvokeWorkerResult> {
826 let mut handles = self.handles.lock().expect("parallel worker mutex poisoned");
827 let handle = handles.get_mut(lane)?.take_if(|h| h.is_finished())?;
828 Some(match handle.join() {
829 Ok(result) => result,
830 Err(_) => Err("worker thread panicked".to_string()),
831 })
832 }
833
834 fn join_lane(&self, lane: usize, timeout: Duration) -> Result<InvokeWorkerResult> {
835 let handle = self
836 .handles
837 .lock()
838 .map_err(|_| crate::new_error!("parallel worker mutex poisoned"))?
839 .get_mut(lane)
840 .and_then(Option::take);
841 let Some(handle) = handle else {
842 return Ok(Ok(()));
843 };
844 join_invoke_worker_handle(lane, handle, timeout)
845 }
846}
847
848fn join_invoke_worker_handle(
849 lane: usize,
850 handle: InvokeWorkerHandle,
851 timeout: Duration,
852) -> Result<InvokeWorkerResult> {
853 let deadline = Instant::now() + timeout;
854 while !handle.is_finished() {
855 if Instant::now() >= deadline {
856 return Err(crate::new_error!(
857 "parallel invoke worker lane {} timed out during stop",
858 lane
859 ));
860 }
861 thread::yield_now();
862 }
863
864 Ok(match handle.join() {
865 Ok(result) => result,
866 Err(_) => Err("worker thread panicked".to_string()),
867 })
868}
869
870impl LaneLease {
871 fn index(&self) -> usize {
872 self.lane
873 }
874}
875
876impl Drop for LaneLease {
877 fn drop(&mut self) {
878 self.workers.release_lane(self.lane);
879 }
880}
881
882impl std::fmt::Debug for MultiUseSandbox {
883 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
884 f.debug_struct("MultiUseSandbox").finish()
885 }
886}