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