1use std::sync::atomic::{AtomicU64, Ordering};
4use std::sync::{Arc, Mutex};
5
6use aion_core::{ActivityError, Payload};
7use beamr::atom::AtomTable;
8use beamr::module::ModuleRegistry;
9use beamr::native::{BifRegistryImpl, NativeRegistrationError};
10use beamr::process::ExitReason;
11use beamr::scheduler::{Scheduler, SchedulerConfig};
12use beamr::term::Term;
13
14use crate::error::EngineError;
15
16use super::config::{RuntimeConfig, SignalDeliveryConfig};
17use super::nif::{Mfa, NifRegistration};
18use super::payload::payload_to_term;
19
20pub type Pid = u64;
22
23type RetainedHeap = Box<[u64]>;
24type RetainedHeaps = Vec<RetainedHeap>;
25type RetainedSpawnHeaps = Arc<dashmap::DashMap<Pid, Mutex<RetainedHeaps>>>;
26
27#[derive(Debug, Default, Eq, PartialEq)]
33pub struct RuntimeInput {
34 terms: Vec<Term>,
35 heaps: RetainedHeaps,
36}
37
38impl RuntimeInput {
39 pub fn from_payload(payload: &Payload) -> Result<Self, EngineError> {
51 let (term, heaps) = payload_to_term(payload)?.into_parts();
52 Ok(Self {
53 terms: vec![term],
54 heaps,
55 })
56 }
57
58 #[must_use]
60 pub fn arity(&self) -> u8 {
61 u8::try_from(self.terms.len()).unwrap_or(u8::MAX)
62 }
63
64 fn into_spawn_parts(self) -> (Vec<Term>, RetainedHeaps) {
65 (self.terms, self.heaps)
66 }
67}
68
69pub struct RuntimeHandle {
71 pub(super) scheduler: Scheduler,
72 pub(super) atom_table: Arc<AtomTable>,
73 pub(super) module_registry: Arc<ModuleRegistry>,
74 pub(super) native_registry: Arc<BifRegistryImpl>,
75 nif_state: Arc<super::nif_state::EngineNifState>,
76 activity_results: Arc<dashmap::DashMap<(Pid, Pid), Payload>>,
77 activity_errors: Arc<dashmap::DashMap<(Pid, Pid), ActivityError>>,
78 activity_delivery_attempts: Arc<dashmap::DashMap<(Pid, Pid), u32>>,
85 in_vm_children: Arc<dashmap::DashMap<Pid, std::collections::HashSet<Pid>>>,
97 registered_nif_modules: Arc<dashmap::DashSet<String>>,
98 spawn_heaps: RetainedSpawnHeaps,
99 signal_delivery: SignalDeliveryConfig,
100 outbox_enabled: bool,
103 pub(super) wake_confirmer: super::wake_confirm::WakeConfirmer,
106 spawned_pid_watermark: AtomicU64,
114}
115
116impl RuntimeHandle {
117 pub fn new(config: RuntimeConfig) -> Result<Self, EngineError> {
123 let atom_table = Arc::new(AtomTable::with_common_atoms());
124 let module_registry = Arc::new(ModuleRegistry::new());
125 let nif_state = Arc::new(super::nif_state::EngineNifState::default());
128 let scheduler_config = SchedulerConfig {
129 thread_count: config.thread_count,
130 nif_private_data: Some(Arc::clone(&nif_state) as _),
131 ..Default::default()
132 };
133 let native_registry = Arc::new(BifRegistryImpl::new());
134 register_all_bifs(&native_registry, &atom_table)?;
135 let scheduler = Scheduler::with_code_server(
136 scheduler_config,
137 Arc::clone(&module_registry),
138 Arc::clone(&atom_table),
139 Arc::clone(&native_registry),
140 )
141 .map_err(runtime_error_from_display)?;
142
143 Ok(Self {
144 scheduler,
145 atom_table,
146 module_registry,
147 native_registry,
148 nif_state,
149 activity_results: Arc::new(dashmap::DashMap::new()),
150 activity_errors: Arc::new(dashmap::DashMap::new()),
151 activity_delivery_attempts: Arc::new(dashmap::DashMap::new()),
152 in_vm_children: Arc::new(dashmap::DashMap::new()),
153 registered_nif_modules: Arc::new(dashmap::DashSet::new()),
154 spawn_heaps: Arc::new(dashmap::DashMap::new()),
155 signal_delivery: config.signal_delivery,
156 outbox_enabled: config.outbox_enabled,
157 wake_confirmer: super::wake_confirm::WakeConfirmer::new(config.signal_delivery)?,
158 spawned_pid_watermark: AtomicU64::new(0),
159 })
160 }
161
162 pub(crate) fn nif_state(&self) -> &Arc<super::nif_state::EngineNifState> {
164 &self.nif_state
165 }
166
167 pub(crate) fn signal_delivery(&self) -> SignalDeliveryConfig {
169 self.signal_delivery
170 }
171
172 pub(crate) fn outbox_enabled(&self) -> bool {
177 self.outbox_enabled
178 }
179
180 pub fn install_nifs(&self, registration: NifRegistration) -> Result<(), EngineError> {
191 for entry in registration.into_entries() {
192 let mfa = entry.mfa;
193 let module = self.atom_table.intern(&mfa.module);
194 let function = self.atom_table.intern(&mfa.function);
195 let capability = beamr::native::Capability::ExternalIo;
196 let result = if entry.is_dirty {
197 self.native_registry.register_dirty(
198 module,
199 function,
200 mfa.arity,
201 entry.function,
202 beamr::scheduler::dirty::DirtySchedulerKind::Cpu,
203 capability,
204 )
205 } else {
206 self.native_registry.register(
207 module,
208 function,
209 mfa.arity,
210 entry.function,
211 capability,
212 )
213 };
214 result.map_err(|error| nif_registration_error(&mfa, error))?;
215 self.registered_nif_modules.insert(mfa.module);
216 }
217
218 Ok(())
219 }
220
221 #[must_use]
224 pub fn registered_nif_modules(&self) -> Vec<String> {
225 let mut module_names: Vec<_> = self
226 .registered_nif_modules
227 .iter()
228 .map(|module_name| module_name.key().clone())
229 .collect();
230 module_names.sort();
231 module_names
232 }
233
234 pub fn spawn_workflow(
241 &self,
242 deployed_module: &str,
243 function: &str,
244 input: RuntimeInput,
245 ) -> Result<Pid, EngineError> {
246 self.spawn_process(deployed_module, function, input)
247 }
248
249 pub fn spawn_workflow_trapping(
256 &self,
257 deployed_module: &str,
258 function: &str,
259 input: RuntimeInput,
260 ) -> Result<Pid, EngineError> {
261 self.release_dead_spawn_heaps();
262 let module = self.atom_table.intern(deployed_module);
263 let function = self.atom_table.intern(function);
264 let (terms, heaps) = input.into_spawn_parts();
265 let pid = self
266 .scheduler
267 .spawn_trap_exit(module, function, terms)
268 .map_err(runtime_error_from_display)?;
269 self.record_spawned_pid(pid);
270 self.retain_spawn_heaps(pid, heaps);
271 Ok(pid)
272 }
273
274 pub fn spawn_activity(
282 &self,
283 parent_pid: Pid,
284 deployed_module: &str,
285 function: &str,
286 input: RuntimeInput,
287 ) -> Result<Pid, EngineError> {
288 self.release_dead_spawn_heaps();
289 self.ensure_live_pid(parent_pid)?;
290 self.wait_for_process_ready(parent_pid)?;
291 let arity = input.arity();
292 let module = self.atom_table.intern(deployed_module);
293 let function_atom = self.atom_table.intern(function);
294 let (terms, heaps) = input.into_spawn_parts();
295 let pid = if self.is_dirty_with_arity(deployed_module, function, arity) {
296 self.scheduler
297 .spawn_link_dirty(parent_pid, module, function_atom, terms)
298 .map_err(runtime_error_from_display)?
299 } else {
300 self.scheduler
301 .spawn_link(parent_pid, module, function_atom, terms)
302 .map_err(runtime_error_from_display)?
303 };
304 self.record_spawned_pid(pid);
305 self.retain_spawn_heaps(pid, heaps);
306 Ok(pid)
307 }
308
309 pub fn spawn_activity_closure(
329 &self,
330 parent_pid: Pid,
331 closure_term: Term,
332 ) -> Result<Pid, EngineError> {
333 self.release_dead_spawn_heaps();
334 self.ensure_live_pid(parent_pid)?;
335 let pid = self
336 .scheduler
337 .spawn_link_closure(parent_pid, closure_term)
338 .map_err(runtime_error_from_display)?;
339 self.record_spawned_pid(pid);
340 self.in_vm_children
341 .entry(parent_pid)
342 .or_default()
343 .insert(pid);
344 if !self.is_live(parent_pid) {
355 self.kill_in_vm_children(parent_pid);
356 return Err(EngineError::Runtime {
357 reason: format!(
358 "in-vm activity child spawn: parent workflow process {parent_pid} exited during spawn"
359 ),
360 });
361 }
362 Ok(pid)
363 }
364
365 pub(crate) fn deregister_in_vm_child(&self, parent_pid: Pid, child_pid: Pid) {
368 if let Some(mut children) = self.in_vm_children.get_mut(&parent_pid) {
369 children.remove(&child_pid);
370 }
371 self.in_vm_children
372 .remove_if(&parent_pid, |_, children| children.is_empty());
373 }
374
375 pub(crate) fn kill_in_vm_children(&self, workflow_pid: Pid) {
384 let Some((_, children)) = self.in_vm_children.remove(&workflow_pid) else {
385 return;
386 };
387 for child_pid in children {
388 if self.is_live(child_pid) {
389 tracing::debug!(
390 workflow_pid,
391 child_pid,
392 "killing orphaned in-vm activity child on workflow exit"
393 );
394 self.scheduler
395 .terminate_process(child_pid, ExitReason::Kill);
396 }
397 self.release_spawn_heaps(child_pid);
398 }
399 }
400
401 #[must_use]
403 pub fn is_dirty(&self, module: &str, function: &str) -> bool {
404 self.is_dirty_with_arity(module, function, 1)
405 }
406
407 #[must_use]
409 pub fn is_dirty_with_arity(&self, module: &str, function: &str, arity: u8) -> bool {
410 let module = self.atom_table.intern(module);
411 let function = self.atom_table.intern(function);
412 self.native_registry
413 .lookup(module, function, arity)
414 .is_some_and(|entry| entry.dirty_kind.is_some())
415 }
416
417 pub fn cancel_pid(&self, pid: Pid) -> Result<(), EngineError> {
423 self.ensure_live_pid(pid)?;
424 self.scheduler.terminate_process(pid, ExitReason::Kill);
425 self.release_spawn_heaps(pid);
426 Ok(())
427 }
428
429 pub fn set_trap_exit(&self, pid: Pid, value: bool) -> Result<bool, EngineError> {
435 self.scheduler
436 .set_trap_exit(pid, value)
437 .map_err(runtime_error_from_display)
438 }
439
440 #[must_use]
442 pub fn is_live(&self, pid: Pid) -> bool {
443 self.scheduler.process_table().get(pid).is_some()
444 }
445
446 pub fn trap_exit(&self, pid: Pid) -> Result<bool, EngineError> {
452 self.scheduler
453 .trap_exit(pid)
454 .ok_or_else(|| runtime_error(format!("process {pid} is not live")))
455 }
456
457 pub fn is_linked(&self, left: Pid, right: Pid) -> Result<bool, EngineError> {
463 self.ensure_live_pid(left)?;
464 self.ensure_live_pid(right)?;
465 Ok(self.scheduler.is_linked(left, right))
466 }
467
468 pub fn shutdown(&self) -> Result<(), EngineError> {
474 self.wake_confirmer.shutdown();
478 let workflow_pids: Vec<Pid> = self
484 .in_vm_children
485 .iter()
486 .map(|entry| *entry.key())
487 .collect();
488 for workflow_pid in workflow_pids {
489 self.kill_in_vm_children(workflow_pid);
490 }
491 self.scheduler.shutdown();
492 self.spawn_heaps.clear();
493 Ok(())
494 }
495
496 fn spawn_process(
497 &self,
498 deployed_module: &str,
499 function: &str,
500 input: RuntimeInput,
501 ) -> Result<Pid, EngineError> {
502 self.release_dead_spawn_heaps();
503 let module = self.atom_table.intern(deployed_module);
504 let function = self.atom_table.intern(function);
505 let (terms, heaps) = input.into_spawn_parts();
506 let pid = self
507 .scheduler
508 .spawn(module, function, terms)
509 .map_err(runtime_error_from_display)?;
510 self.record_spawned_pid(pid);
511 self.retain_spawn_heaps(pid, heaps);
512 Ok(pid)
513 }
514
515 fn record_spawned_pid(&self, pid: Pid) {
516 self.spawned_pid_watermark.fetch_max(pid, Ordering::AcqRel);
517 }
518
519 fn retain_spawn_heaps(&self, pid: Pid, heaps: RetainedHeaps) {
520 if heaps.is_empty() {
521 return;
522 }
523 self.spawn_heaps.insert(pid, Mutex::new(heaps));
524 }
525
526 pub(super) fn release_spawn_heaps(&self, pid: Pid) {
527 self.spawn_heaps.remove(&pid);
528 }
529
530 fn release_dead_spawn_heaps(&self) {
531 let dead_pids: Vec<Pid> = self
532 .spawn_heaps
533 .iter()
534 .filter_map(|entry| {
535 let pid = *entry.key();
536 self.scheduler
537 .process_table()
538 .get(pid)
539 .is_none()
540 .then_some(pid)
541 })
542 .collect();
543 for pid in dead_pids {
544 self.release_spawn_heaps(pid);
545 }
546 }
547
548 pub(super) fn ensure_live_pid(&self, pid: Pid) -> Result<(), EngineError> {
549 if self.scheduler.process_table().get(pid).is_some() {
550 Ok(())
551 } else {
552 Err(runtime_error(format!("process {pid} is not live")))
553 }
554 }
555
556 pub(super) fn ensure_monitorable_pid(&self, pid: Pid) -> Result<(), EngineError> {
566 if self.scheduler.process_table().get(pid).is_some() {
567 return Ok(());
568 }
569 if pid > 0 && pid <= self.spawned_pid_watermark.load(Ordering::Acquire) {
570 return Ok(());
571 }
572 Err(runtime_error(format!(
573 "process {pid} was never spawned by this runtime"
574 )))
575 }
576
577 #[cfg(test)]
582 pub fn register_waiting_test_module(&self, deployed_name: &str, function: &str) {
583 use std::collections::HashMap;
584
585 use beamr::loader::Instruction;
586 use beamr::loader::decode::compact::Operand;
587 use beamr::module::Module;
588
589 let module = self.atom_table.intern(deployed_name);
590 let function = self.atom_table.intern(function);
591 let label = 10;
592 self.module_registry.insert(Module {
593 name: module,
594 generation: 0,
595 origin: beamr::module::ModuleOrigin::Preloaded,
596 exports: HashMap::from([((function, 1), label)]),
597 label_index: HashMap::from([(label, 0)]),
598 code: vec![
599 Instruction::Label { label },
600 Instruction::Wait {
601 fail: Operand::Label(label),
602 },
603 ],
604 function_table: Vec::new(),
605 line_table: Vec::new(),
606 literals: Vec::new(),
607 constant_pool: beamr::constant_pool::ConstantPool::new(),
608 resolved_imports: Vec::new(),
609 lambdas: Vec::new(),
610 string_table: Vec::new(),
611 line_info: Vec::new(),
612 });
613 }
614
615 #[cfg(test)]
621 pub fn spawn_test_process(&self) -> Result<Pid, EngineError> {
622 let pid = self.scheduler.spawn_test_process(false);
623 self.record_spawned_pid(pid);
624 Ok(pid)
625 }
626
627 #[cfg(test)]
633 pub fn spawn_test_process_with_trap_exit(&self, trap_exit: bool) -> Result<Pid, EngineError> {
634 let pid = self.scheduler.spawn_test_process(trap_exit);
635 self.record_spawned_pid(pid);
636 Ok(pid)
637 }
638
639 #[cfg(test)]
646 pub fn spawn_linked_test_process(&self, parent_pid: Pid) -> Result<Pid, EngineError> {
647 self.ensure_live_pid(parent_pid)?;
648 let pid = self
649 .scheduler
650 .spawn_linked_test_process(parent_pid)
651 .map_err(runtime_error_from_display)?;
652 self.record_spawned_pid(pid);
653 Ok(pid)
654 }
655
656 #[cfg(test)]
662 pub fn has_trapped_exit_message(
663 &self,
664 target_pid: Pid,
665 source_pid: Pid,
666 ) -> Result<bool, EngineError> {
667 self.ensure_live_pid(target_pid)?;
668 Ok(self
669 .scheduler
670 .has_trapped_exit_message(target_pid, source_pid)
671 .unwrap_or(false))
672 }
673
674 #[cfg(test)]
683 pub fn wait_for_trapped_exit(
684 &self,
685 target_pid: Pid,
686 source_pid: Pid,
687 ) -> Result<(), EngineError> {
688 let deadline = std::time::Instant::now() + std::time::Duration::from_millis(50);
689 while std::time::Instant::now() < deadline {
690 if self
691 .scheduler
692 .has_trapped_exit_message(target_pid, source_pid)
693 .unwrap_or(false)
694 {
695 return Ok(());
696 }
697 std::thread::sleep(std::time::Duration::from_millis(1));
698 }
699 Err(runtime_error(format!(
700 "trapped exit from {source_pid} to {target_pid} did not arrive"
701 )))
702 }
703
704 #[cfg(test)]
710 pub fn terminate_test_process_with_error(&self, pid: Pid) -> Result<(), EngineError> {
711 self.ensure_live_pid(pid)?;
712 self.scheduler.terminate_process(pid, ExitReason::Error);
713 Ok(())
714 }
715
716 #[cfg(test)]
717 pub(crate) fn lookup_native_for_test(
718 &self,
719 module: &str,
720 function: &str,
721 arity: u8,
722 ) -> Option<beamr::native::NativeEntry> {
723 let module = self.atom_table.intern(module);
724 let function = self.atom_table.intern(function);
725 self.native_registry.lookup(module, function, arity)
726 }
727
728 #[cfg(test)]
729 pub(crate) fn run_until_exit_for_test(&self, pid: Pid) -> (ExitReason, Term) {
730 let (reason, owned_result) = self.scheduler.run_until_exit(pid);
731 self.release_spawn_heaps(pid);
732 (reason, owned_result.root())
733 }
734
735 #[cfg(test)]
736 pub(crate) fn retained_spawn_heap_count_for_test(&self) -> usize {
737 self.release_dead_spawn_heaps();
738 self.spawn_heaps.len()
739 }
740}
741
742fn runtime_error(reason: String) -> EngineError {
743 EngineError::Runtime { reason }
744}
745
746fn runtime_error_from_display(reason: impl std::fmt::Display) -> EngineError {
747 runtime_error(reason.to_string())
748}
749
750fn nif_registration_error(mfa: &Mfa, error: NativeRegistrationError) -> EngineError {
751 match error {
752 NativeRegistrationError::DuplicateMfa { .. } => EngineError::NifRegistration {
753 reason: format!("native function already registered for {}", mfa.display()),
754 },
755 }
756}
757
758fn register_all_bifs(
759 registry: &BifRegistryImpl,
760 atom_table: &AtomTable,
761) -> Result<(), EngineError> {
762 use beamr::native::{
763 bifs::register_gate1_bifs, gate3_bifs::register_gate3_bifs,
764 gleam_ffi::register_gleam_ffi_bifs, otp_stubs::init_otp_atoms,
765 otp_stubs::register_otp_stubs, process_bifs::register_gate2_bifs,
766 selector_ffi::register_selector_bifs, stdlib_stubs::register_stdlib_stubs,
767 };
768 register_gate1_bifs(registry, atom_table).map_err(runtime_error_from_display)?;
769 register_gate2_bifs(registry, atom_table).map_err(runtime_error_from_display)?;
770 register_gate3_bifs(registry, atom_table).map_err(runtime_error_from_display)?;
771 register_stdlib_stubs(registry, atom_table).map_err(runtime_error_from_display)?;
772 register_selector_bifs(registry, atom_table).map_err(runtime_error_from_display)?;
773 register_gleam_ffi_bifs(registry, atom_table).map_err(runtime_error_from_display)?;
774 init_otp_atoms(atom_table);
775 register_otp_stubs(registry, atom_table).map_err(runtime_error_from_display)?;
776 Ok(())
777}
778
779mod delivery;
780
781pub(crate) use delivery::InVmChildOutcome;
782
783#[cfg(test)]
784#[path = "handle/test_support.rs"]
785mod test_support;
786
787#[cfg(test)]
788mod tests {
789 use aion_core::Payload;
790 use std::time::Duration;
791
792 use beamr::loader::Instruction;
793 use beamr::loader::decode::compact::Operand;
794 use beamr::module::{Module, ResolvedImport, ResolvedImportTarget};
795 use beamr::native::ProcessContext;
796 use beamr::term::Term;
797 use beamr::term::binary_ref::BinaryRef;
798
799 use super::{RuntimeHandle, RuntimeInput};
800 use crate::error::EngineError;
801 use crate::runtime::{Mfa, NifEntry, NifRegistration, RuntimeConfig, SignalDeliveryConfig};
802
803 fn forty_two(args: &[Term], _context: &mut ProcessContext) -> Result<Term, Term> {
804 if args.len() > 255 {
805 return Err(Term::small_int(0));
806 }
807 Ok(Term::small_int(42))
808 }
809
810 fn thirteen(args: &[Term], _context: &mut ProcessContext) -> Result<Term, Term> {
811 if args.len() > 255 {
812 return Err(Term::small_int(0));
813 }
814 Ok(Term::small_int(13))
815 }
816
817 fn binary_length(args: &[Term], _context: &mut ProcessContext) -> Result<Term, Term> {
818 match args {
819 [term] => BinaryRef::new(*term)
820 .and_then(|binary| i64::try_from(binary.as_bytes().len()).ok())
821 .map(Term::small_int)
822 .ok_or_else(|| Term::small_int(0)),
823 _ => Err(Term::small_int(0)),
824 }
825 }
826
827 fn native_call_module_for_test(
828 module: beamr::atom::Atom,
829 function: beamr::atom::Atom,
830 target_module: beamr::atom::Atom,
831 target_function: beamr::atom::Atom,
832 native_entry: Option<beamr::native::NativeEntry>,
833 ) -> Module {
834 native_call_module_with_arity_for_test(
835 module,
836 function,
837 target_module,
838 target_function,
839 0,
840 native_entry,
841 )
842 }
843
844 fn native_call_module_with_arity_for_test(
845 module: beamr::atom::Atom,
846 function: beamr::atom::Atom,
847 target_module: beamr::atom::Atom,
848 target_function: beamr::atom::Atom,
849 arity: u8,
850 native_entry: Option<beamr::native::NativeEntry>,
851 ) -> Module {
852 let label = 1;
853 let code = vec![
854 Instruction::Label { label },
855 Instruction::CallExt {
856 arity: Operand::Unsigned(arity.into()),
857 import: Operand::Unsigned(0),
858 },
859 Instruction::Return,
860 ];
861 let mut module_data = Module {
862 name: module,
863 generation: 0,
864 origin: beamr::module::ModuleOrigin::Preloaded,
865 exports: std::collections::HashMap::from([((function, arity), label)]),
866 label_index: std::collections::HashMap::from([(label, 0)]),
867 code,
868 function_table: Vec::new(),
869 line_table: Vec::new(),
870 literals: Vec::new(),
871 constant_pool: beamr::constant_pool::ConstantPool::new(),
872 resolved_imports: Vec::new(),
873 lambdas: Vec::new(),
874 string_table: Vec::new(),
875 line_info: Vec::new(),
876 };
877 if let Some(native_entry) = native_entry {
878 module_data.resolved_imports.push(ResolvedImport {
879 module: target_module,
880 function: target_function,
881 arity,
882 target: ResolvedImportTarget::Native(native_entry),
883 });
884 }
885 module_data
886 }
887
888 fn assert_send_sync<T: Send + Sync>() {}
889
890 fn fixture_workflow_beam() -> &'static [u8] {
891 include_bytes!("../../tests/fixtures/aion_fixture_workflow.beam")
892 }
893
894 #[test]
895 fn runtime_handle_is_send_sync() {
896 assert_send_sync::<RuntimeHandle>();
897 }
898
899 #[test]
900 fn registers_spawns_and_shuts_down() -> Result<(), Box<dyn std::error::Error>> {
901 let runtime = RuntimeHandle::new(RuntimeConfig::new(None))?;
902 runtime.register_module("aion_fixture_workflow", fixture_workflow_beam())?;
903
904 let pid =
905 runtime.spawn_workflow("aion_fixture_workflow", "wait", RuntimeInput::default())?;
906 assert!(runtime.cancel_pid(pid).is_ok());
907 runtime.shutdown()?;
908 Ok(())
909 }
910
911 #[test]
912 fn signal_delivery_to_dead_process_returns_typed_error()
913 -> Result<(), Box<dyn std::error::Error>> {
914 let signal_delivery =
915 SignalDeliveryConfig::new(Duration::ZERO, 1, Duration::ZERO, Duration::ZERO);
916 let runtime =
917 RuntimeHandle::new(RuntimeConfig::new(Some(1)).with_signal_delivery(signal_delivery))?;
918 let pid = runtime.spawn_test_process()?;
919 runtime.terminate_test_process_with_error(pid)?;
920
921 let error = runtime
922 .deliver_signal_received(pid)
923 .err()
924 .ok_or("dead process delivery unexpectedly succeeded")?;
925
926 assert!(matches!(error, EngineError::Runtime { .. }));
927 runtime.shutdown()?;
928 Ok(())
929 }
930
931 #[test]
932 fn duplicate_nif_mfa_returns_typed_error() -> Result<(), Box<dyn std::error::Error>> {
933 let runtime = RuntimeHandle::new(RuntimeConfig::new(None))?;
934 let mfa = Mfa::new("host", "answer", 0);
935 let mut registration = NifRegistration::new();
936 registration.add_host_nifs([
937 NifEntry::new(mfa.clone(), forty_two),
938 NifEntry::dirty(mfa, thirteen),
939 ]);
940
941 let error = runtime.install_nifs(registration).err();
942
943 assert!(matches!(
944 error,
945 Some(EngineError::NifRegistration { reason })
946 if reason.contains("host:answer/0")
947 ));
948 assert_eq!(runtime.registered_nif_modules(), vec!["host"]);
949 runtime.shutdown()?;
950 Ok(())
951 }
952
953 #[test]
954 fn payload_binary_remains_valid_through_spawn_and_is_released()
955 -> Result<(), Box<dyn std::error::Error>> {
956 let runtime = RuntimeHandle::new(RuntimeConfig::new(None))?;
957 let mfa = Mfa::new("host", "binary_length", 1);
958 let mut registration = NifRegistration::new();
959 registration.add_host_nifs([NifEntry::new(mfa, binary_length)]);
960 runtime.install_nifs(registration)?;
961
962 let native_entry = runtime.lookup_native_for_test("host", "binary_length", 1);
963 let module = native_call_module_with_arity_for_test(
964 runtime.atom_table.intern("payload_echo"),
965 runtime.atom_table.intern("run"),
966 runtime.atom_table.intern("host"),
967 runtime.atom_table.intern("binary_length"),
968 1,
969 native_entry,
970 );
971 runtime.module_registry.insert(module);
972 let payload = Payload::new(
973 aion_core::ContentType::Json,
974 br#"{"hello":"world"}"#.to_vec(),
975 );
976
977 let pid =
978 runtime.spawn_workflow("payload_echo", "run", RuntimeInput::from_payload(&payload)?)?;
979 assert_eq!(runtime.retained_spawn_heap_count_for_test(), 1);
980 let (reason, result) = runtime.run_until_exit_for_test(pid);
981
982 assert_eq!(reason, beamr::process::ExitReason::Normal);
983 assert_eq!(
984 result.as_small_int(),
985 Some(i64::try_from(payload.bytes().len()).unwrap_or(0))
986 );
987 assert_eq!(runtime.retained_spawn_heap_count_for_test(), 0);
988 runtime.shutdown()?;
989 Ok(())
990 }
991
992 #[test]
993 fn workflow_outcome_releases_payload_heaps() -> Result<(), Box<dyn std::error::Error>> {
994 let runtime = RuntimeHandle::new(RuntimeConfig::new(None))?;
995 let mfa = Mfa::new("host", "binary_length", 1);
996 let mut registration = NifRegistration::new();
997 registration.add_host_nifs([NifEntry::new(mfa, binary_length)]);
998 runtime.install_nifs(registration)?;
999
1000 let native_entry = runtime.lookup_native_for_test("host", "binary_length", 1);
1001 let module = native_call_module_with_arity_for_test(
1002 runtime.atom_table.intern("payload_workflow_outcome"),
1003 runtime.atom_table.intern("run"),
1004 runtime.atom_table.intern("host"),
1005 runtime.atom_table.intern("binary_length"),
1006 1,
1007 native_entry,
1008 );
1009 runtime.module_registry.insert(module);
1010 let payload = Payload::new(
1011 aion_core::ContentType::Json,
1012 br#"{"workflow":"outcome"}"#.to_vec(),
1013 );
1014
1015 let pid = runtime.spawn_workflow(
1016 "payload_workflow_outcome",
1017 "run",
1018 RuntimeInput::from_payload(&payload)?,
1019 )?;
1020 assert_eq!(runtime.retained_spawn_heap_count_for_test(), 1);
1021 let outcome = runtime.workflow_outcome(pid)?;
1022
1023 assert_eq!(
1024 outcome?,
1025 Payload::from_json(&serde_json::json!(payload.bytes().len()))?
1026 );
1027 assert_eq!(runtime.retained_spawn_heap_count_for_test(), 0);
1028 runtime.shutdown()?;
1029 Ok(())
1030 }
1031
1032 #[test]
1033 fn repeated_completed_payload_spawns_do_not_accumulate_retained_heaps()
1034 -> Result<(), Box<dyn std::error::Error>> {
1035 let runtime = RuntimeHandle::new(RuntimeConfig::new(None))?;
1036 let mfa = Mfa::new("host", "binary_length", 1);
1037 let mut registration = NifRegistration::new();
1038 registration.add_host_nifs([NifEntry::new(mfa, binary_length)]);
1039 runtime.install_nifs(registration)?;
1040
1041 let native_entry = runtime.lookup_native_for_test("host", "binary_length", 1);
1042 let module = native_call_module_with_arity_for_test(
1043 runtime.atom_table.intern("payload_echo_many"),
1044 runtime.atom_table.intern("run"),
1045 runtime.atom_table.intern("host"),
1046 runtime.atom_table.intern("binary_length"),
1047 1,
1048 native_entry,
1049 );
1050 runtime.module_registry.insert(module);
1051 let payload = Payload::new(
1052 aion_core::ContentType::Json,
1053 br#"{"iteration":true}"#.to_vec(),
1054 );
1055
1056 for _ in 0..1_000 {
1057 let pid = runtime.spawn_workflow(
1058 "payload_echo_many",
1059 "run",
1060 RuntimeInput::from_payload(&payload)?,
1061 )?;
1062 let (reason, result) = runtime.run_until_exit_for_test(pid);
1063 assert_eq!(reason, beamr::process::ExitReason::Normal);
1064 assert_eq!(
1065 result.as_small_int(),
1066 Some(i64::try_from(payload.bytes().len()).unwrap_or(0))
1067 );
1068 assert_eq!(runtime.retained_spawn_heap_count_for_test(), 0);
1069 }
1070
1071 runtime.shutdown()?;
1072 Ok(())
1073 }
1074
1075 #[test]
1076 fn distinct_nifs_are_registered_and_callable() -> Result<(), Box<dyn std::error::Error>> {
1077 let runtime = RuntimeHandle::new(RuntimeConfig::new(None))?;
1078 let mut registration = NifRegistration::new();
1079 registration.add_engine_nifs().add_host_nifs([
1080 NifEntry::new(Mfa::new("host", "answer", 0), forty_two),
1081 NifEntry::dirty(Mfa::new("host", "thirteen", 0), thirteen),
1082 ]);
1083
1084 runtime.install_nifs(registration)?;
1085
1086 assert_eq!(
1087 runtime.registered_nif_modules(),
1088 vec!["aion_flow_ffi", "host"]
1089 );
1090 let answer = runtime.lookup_native_for_test("host", "answer", 0);
1091 assert!(answer.is_some());
1092 assert!(
1093 runtime
1094 .lookup_native_for_test("host", "thirteen", 0)
1095 .is_some_and(|entry| entry.dirty_kind.is_some())
1096 );
1097
1098 let host_nif_call = native_call_module_for_test(
1099 runtime.atom_table.intern("host_nif_call"),
1100 runtime.atom_table.intern("answer"),
1101 runtime.atom_table.intern("host"),
1102 runtime.atom_table.intern("answer"),
1103 answer,
1104 );
1105 runtime.module_registry.insert(host_nif_call);
1106 let pid = runtime.spawn_workflow("host_nif_call", "answer", RuntimeInput::default())?;
1107 let (reason, result) = runtime.run_until_exit_for_test(pid);
1108
1109 assert_eq!(reason, beamr::process::ExitReason::Normal);
1110 assert_eq!(result, Term::small_int(42));
1111 runtime.shutdown()?;
1112 Ok(())
1113 }
1114}