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 registered_nif_modules: Arc<dashmap::DashSet<String>>,
79 spawn_heaps: RetainedSpawnHeaps,
80 signal_delivery: SignalDeliveryConfig,
81 outbox_enabled: bool,
84 pub(super) wake_confirmer: super::wake_confirm::WakeConfirmer,
87 spawned_pid_watermark: AtomicU64,
95}
96
97impl RuntimeHandle {
98 pub fn new(config: RuntimeConfig) -> Result<Self, EngineError> {
104 let atom_table = Arc::new(AtomTable::with_common_atoms());
105 let module_registry = Arc::new(ModuleRegistry::new());
106 let nif_state = Arc::new(super::nif_state::EngineNifState::default());
109 let scheduler_config = SchedulerConfig {
110 thread_count: config.thread_count,
111 nif_private_data: Some(Arc::clone(&nif_state) as _),
112 ..Default::default()
113 };
114 let native_registry = Arc::new(BifRegistryImpl::new());
115 register_all_bifs(&native_registry, &atom_table)?;
116 let scheduler = Scheduler::with_code_server(
117 scheduler_config,
118 Arc::clone(&module_registry),
119 Arc::clone(&atom_table),
120 Arc::clone(&native_registry),
121 )
122 .map_err(runtime_error_from_display)?;
123
124 Ok(Self {
125 scheduler,
126 atom_table,
127 module_registry,
128 native_registry,
129 nif_state,
130 activity_results: Arc::new(dashmap::DashMap::new()),
131 activity_errors: Arc::new(dashmap::DashMap::new()),
132 registered_nif_modules: Arc::new(dashmap::DashSet::new()),
133 spawn_heaps: Arc::new(dashmap::DashMap::new()),
134 signal_delivery: config.signal_delivery,
135 outbox_enabled: config.outbox_enabled,
136 wake_confirmer: super::wake_confirm::WakeConfirmer::new(config.signal_delivery)?,
137 spawned_pid_watermark: AtomicU64::new(0),
138 })
139 }
140
141 pub(crate) fn nif_state(&self) -> &Arc<super::nif_state::EngineNifState> {
143 &self.nif_state
144 }
145
146 pub(crate) fn signal_delivery(&self) -> SignalDeliveryConfig {
148 self.signal_delivery
149 }
150
151 pub(crate) fn outbox_enabled(&self) -> bool {
156 self.outbox_enabled
157 }
158
159 pub fn install_nifs(&self, registration: NifRegistration) -> Result<(), EngineError> {
170 for entry in registration.into_entries() {
171 let mfa = entry.mfa;
172 let module = self.atom_table.intern(&mfa.module);
173 let function = self.atom_table.intern(&mfa.function);
174 let capability = beamr::native::Capability::ExternalIo;
175 let result = if entry.is_dirty {
176 self.native_registry.register_dirty(
177 module,
178 function,
179 mfa.arity,
180 entry.function,
181 beamr::scheduler::dirty::DirtySchedulerKind::Cpu,
182 capability,
183 )
184 } else {
185 self.native_registry.register(
186 module,
187 function,
188 mfa.arity,
189 entry.function,
190 capability,
191 )
192 };
193 result.map_err(|error| nif_registration_error(&mfa, error))?;
194 self.registered_nif_modules.insert(mfa.module);
195 }
196
197 Ok(())
198 }
199
200 #[must_use]
203 pub fn registered_nif_modules(&self) -> Vec<String> {
204 let mut module_names: Vec<_> = self
205 .registered_nif_modules
206 .iter()
207 .map(|module_name| module_name.key().clone())
208 .collect();
209 module_names.sort();
210 module_names
211 }
212
213 pub fn spawn_workflow(
220 &self,
221 deployed_module: &str,
222 function: &str,
223 input: RuntimeInput,
224 ) -> Result<Pid, EngineError> {
225 self.spawn_process(deployed_module, function, input)
226 }
227
228 pub fn spawn_workflow_trapping(
235 &self,
236 deployed_module: &str,
237 function: &str,
238 input: RuntimeInput,
239 ) -> Result<Pid, EngineError> {
240 self.release_dead_spawn_heaps();
241 let module = self.atom_table.intern(deployed_module);
242 let function = self.atom_table.intern(function);
243 let (terms, heaps) = input.into_spawn_parts();
244 let pid = self
245 .scheduler
246 .spawn_trap_exit(module, function, terms)
247 .map_err(runtime_error_from_display)?;
248 self.record_spawned_pid(pid);
249 self.retain_spawn_heaps(pid, heaps);
250 Ok(pid)
251 }
252
253 pub fn spawn_activity(
261 &self,
262 parent_pid: Pid,
263 deployed_module: &str,
264 function: &str,
265 input: RuntimeInput,
266 ) -> Result<Pid, EngineError> {
267 self.release_dead_spawn_heaps();
268 self.ensure_live_pid(parent_pid)?;
269 self.wait_for_process_ready(parent_pid)?;
270 let arity = input.arity();
271 let module = self.atom_table.intern(deployed_module);
272 let function_atom = self.atom_table.intern(function);
273 let (terms, heaps) = input.into_spawn_parts();
274 let pid = if self.is_dirty_with_arity(deployed_module, function, arity) {
275 self.scheduler
276 .spawn_link_dirty(parent_pid, module, function_atom, terms)
277 .map_err(runtime_error_from_display)?
278 } else {
279 self.scheduler
280 .spawn_link(parent_pid, module, function_atom, terms)
281 .map_err(runtime_error_from_display)?
282 };
283 self.record_spawned_pid(pid);
284 self.retain_spawn_heaps(pid, heaps);
285 Ok(pid)
286 }
287
288 #[must_use]
290 pub fn is_dirty(&self, module: &str, function: &str) -> bool {
291 self.is_dirty_with_arity(module, function, 1)
292 }
293
294 #[must_use]
296 pub fn is_dirty_with_arity(&self, module: &str, function: &str, arity: u8) -> bool {
297 let module = self.atom_table.intern(module);
298 let function = self.atom_table.intern(function);
299 self.native_registry
300 .lookup(module, function, arity)
301 .is_some_and(|entry| entry.dirty_kind.is_some())
302 }
303
304 pub fn cancel_pid(&self, pid: Pid) -> Result<(), EngineError> {
310 self.ensure_live_pid(pid)?;
311 self.scheduler.terminate_process(pid, ExitReason::Kill);
312 self.release_spawn_heaps(pid);
313 Ok(())
314 }
315
316 pub fn set_trap_exit(&self, pid: Pid, value: bool) -> Result<bool, EngineError> {
322 self.scheduler
323 .set_trap_exit(pid, value)
324 .map_err(runtime_error_from_display)
325 }
326
327 #[must_use]
329 pub fn is_live(&self, pid: Pid) -> bool {
330 self.scheduler.process_table().get(pid).is_some()
331 }
332
333 pub fn trap_exit(&self, pid: Pid) -> Result<bool, EngineError> {
339 self.scheduler
340 .trap_exit(pid)
341 .ok_or_else(|| runtime_error(format!("process {pid} is not live")))
342 }
343
344 pub fn is_linked(&self, left: Pid, right: Pid) -> Result<bool, EngineError> {
350 self.ensure_live_pid(left)?;
351 self.ensure_live_pid(right)?;
352 Ok(self.scheduler.is_linked(left, right))
353 }
354
355 pub fn shutdown(&self) -> Result<(), EngineError> {
361 self.wake_confirmer.shutdown();
365 self.scheduler.shutdown();
366 self.spawn_heaps.clear();
367 Ok(())
368 }
369
370 fn spawn_process(
371 &self,
372 deployed_module: &str,
373 function: &str,
374 input: RuntimeInput,
375 ) -> Result<Pid, EngineError> {
376 self.release_dead_spawn_heaps();
377 let module = self.atom_table.intern(deployed_module);
378 let function = self.atom_table.intern(function);
379 let (terms, heaps) = input.into_spawn_parts();
380 let pid = self
381 .scheduler
382 .spawn(module, function, terms)
383 .map_err(runtime_error_from_display)?;
384 self.record_spawned_pid(pid);
385 self.retain_spawn_heaps(pid, heaps);
386 Ok(pid)
387 }
388
389 fn record_spawned_pid(&self, pid: Pid) {
390 self.spawned_pid_watermark.fetch_max(pid, Ordering::AcqRel);
391 }
392
393 fn retain_spawn_heaps(&self, pid: Pid, heaps: RetainedHeaps) {
394 if heaps.is_empty() {
395 return;
396 }
397 self.spawn_heaps.insert(pid, Mutex::new(heaps));
398 }
399
400 pub(super) fn release_spawn_heaps(&self, pid: Pid) {
401 self.spawn_heaps.remove(&pid);
402 }
403
404 fn release_dead_spawn_heaps(&self) {
405 let dead_pids: Vec<Pid> = self
406 .spawn_heaps
407 .iter()
408 .filter_map(|entry| {
409 let pid = *entry.key();
410 self.scheduler
411 .process_table()
412 .get(pid)
413 .is_none()
414 .then_some(pid)
415 })
416 .collect();
417 for pid in dead_pids {
418 self.release_spawn_heaps(pid);
419 }
420 }
421
422 pub(super) fn ensure_live_pid(&self, pid: Pid) -> Result<(), EngineError> {
423 if self.scheduler.process_table().get(pid).is_some() {
424 Ok(())
425 } else {
426 Err(runtime_error(format!("process {pid} is not live")))
427 }
428 }
429
430 pub(super) fn ensure_monitorable_pid(&self, pid: Pid) -> Result<(), EngineError> {
440 if self.scheduler.process_table().get(pid).is_some() {
441 return Ok(());
442 }
443 if pid > 0 && pid <= self.spawned_pid_watermark.load(Ordering::Acquire) {
444 return Ok(());
445 }
446 Err(runtime_error(format!(
447 "process {pid} was never spawned by this runtime"
448 )))
449 }
450
451 #[cfg(test)]
456 pub fn register_waiting_test_module(&self, deployed_name: &str, function: &str) {
457 use std::collections::HashMap;
458
459 use beamr::loader::Instruction;
460 use beamr::loader::decode::compact::Operand;
461 use beamr::module::Module;
462
463 let module = self.atom_table.intern(deployed_name);
464 let function = self.atom_table.intern(function);
465 let label = 10;
466 self.module_registry.insert(Module {
467 name: module,
468 generation: 0,
469 origin: beamr::module::ModuleOrigin::Preloaded,
470 exports: HashMap::from([((function, 1), label)]),
471 label_index: HashMap::from([(label, 0)]),
472 code: vec![
473 Instruction::Label { label },
474 Instruction::Wait {
475 fail: Operand::Label(label),
476 },
477 ],
478 function_table: Vec::new(),
479 line_table: Vec::new(),
480 literals: Vec::new(),
481 constant_pool: beamr::constant_pool::ConstantPool::new(),
482 resolved_imports: Vec::new(),
483 lambdas: Vec::new(),
484 string_table: Vec::new(),
485 line_info: Vec::new(),
486 });
487 }
488
489 #[cfg(test)]
495 pub fn spawn_test_process(&self) -> Result<Pid, EngineError> {
496 let pid = self.scheduler.spawn_test_process(false);
497 self.record_spawned_pid(pid);
498 Ok(pid)
499 }
500
501 #[cfg(test)]
507 pub fn spawn_test_process_with_trap_exit(&self, trap_exit: bool) -> Result<Pid, EngineError> {
508 let pid = self.scheduler.spawn_test_process(trap_exit);
509 self.record_spawned_pid(pid);
510 Ok(pid)
511 }
512
513 #[cfg(test)]
520 pub fn spawn_linked_test_process(&self, parent_pid: Pid) -> Result<Pid, EngineError> {
521 self.ensure_live_pid(parent_pid)?;
522 let pid = self
523 .scheduler
524 .spawn_linked_test_process(parent_pid)
525 .map_err(runtime_error_from_display)?;
526 self.record_spawned_pid(pid);
527 Ok(pid)
528 }
529
530 #[cfg(test)]
536 pub fn has_trapped_exit_message(
537 &self,
538 target_pid: Pid,
539 source_pid: Pid,
540 ) -> Result<bool, EngineError> {
541 self.ensure_live_pid(target_pid)?;
542 Ok(self
543 .scheduler
544 .has_trapped_exit_message(target_pid, source_pid)
545 .unwrap_or(false))
546 }
547
548 #[cfg(test)]
557 pub fn wait_for_trapped_exit(
558 &self,
559 target_pid: Pid,
560 source_pid: Pid,
561 ) -> Result<(), EngineError> {
562 let deadline = std::time::Instant::now() + std::time::Duration::from_millis(50);
563 while std::time::Instant::now() < deadline {
564 if self
565 .scheduler
566 .has_trapped_exit_message(target_pid, source_pid)
567 .unwrap_or(false)
568 {
569 return Ok(());
570 }
571 std::thread::sleep(std::time::Duration::from_millis(1));
572 }
573 Err(runtime_error(format!(
574 "trapped exit from {source_pid} to {target_pid} did not arrive"
575 )))
576 }
577
578 #[cfg(test)]
584 pub fn terminate_test_process_with_error(&self, pid: Pid) -> Result<(), EngineError> {
585 self.ensure_live_pid(pid)?;
586 self.scheduler.terminate_process(pid, ExitReason::Error);
587 Ok(())
588 }
589
590 #[cfg(test)]
591 pub(crate) fn lookup_native_for_test(
592 &self,
593 module: &str,
594 function: &str,
595 arity: u8,
596 ) -> Option<beamr::native::NativeEntry> {
597 let module = self.atom_table.intern(module);
598 let function = self.atom_table.intern(function);
599 self.native_registry.lookup(module, function, arity)
600 }
601
602 #[cfg(test)]
603 pub(crate) fn run_until_exit_for_test(&self, pid: Pid) -> (ExitReason, Term) {
604 let (reason, owned_result) = self.scheduler.run_until_exit(pid);
605 self.release_spawn_heaps(pid);
606 (reason, owned_result.root())
607 }
608
609 #[cfg(test)]
610 pub(crate) fn retained_spawn_heap_count_for_test(&self) -> usize {
611 self.release_dead_spawn_heaps();
612 self.spawn_heaps.len()
613 }
614}
615
616fn runtime_error(reason: String) -> EngineError {
617 EngineError::Runtime { reason }
618}
619
620fn runtime_error_from_display(reason: impl std::fmt::Display) -> EngineError {
621 runtime_error(reason.to_string())
622}
623
624fn nif_registration_error(mfa: &Mfa, error: NativeRegistrationError) -> EngineError {
625 match error {
626 NativeRegistrationError::DuplicateMfa { .. } => EngineError::NifRegistration {
627 reason: format!("native function already registered for {}", mfa.display()),
628 },
629 }
630}
631
632fn register_all_bifs(
633 registry: &BifRegistryImpl,
634 atom_table: &AtomTable,
635) -> Result<(), EngineError> {
636 use beamr::native::{
637 bifs::register_gate1_bifs, gate3_bifs::register_gate3_bifs,
638 gleam_ffi::register_gleam_ffi_bifs, otp_stubs::init_otp_atoms,
639 otp_stubs::register_otp_stubs, process_bifs::register_gate2_bifs,
640 selector_ffi::register_selector_bifs, stdlib_stubs::register_stdlib_stubs,
641 };
642 register_gate1_bifs(registry, atom_table).map_err(runtime_error_from_display)?;
643 register_gate2_bifs(registry, atom_table).map_err(runtime_error_from_display)?;
644 register_gate3_bifs(registry, atom_table).map_err(runtime_error_from_display)?;
645 register_stdlib_stubs(registry, atom_table).map_err(runtime_error_from_display)?;
646 register_selector_bifs(registry, atom_table).map_err(runtime_error_from_display)?;
647 register_gleam_ffi_bifs(registry, atom_table).map_err(runtime_error_from_display)?;
648 init_otp_atoms(atom_table);
649 register_otp_stubs(registry, atom_table).map_err(runtime_error_from_display)?;
650 Ok(())
651}
652
653mod delivery;
654
655#[cfg(test)]
656#[path = "handle/test_support.rs"]
657mod test_support;
658
659#[cfg(test)]
660mod tests {
661 use aion_core::Payload;
662 use std::time::Duration;
663
664 use beamr::loader::Instruction;
665 use beamr::loader::decode::compact::Operand;
666 use beamr::module::{Module, ResolvedImport, ResolvedImportTarget};
667 use beamr::native::ProcessContext;
668 use beamr::term::Term;
669 use beamr::term::binary_ref::BinaryRef;
670
671 use super::{RuntimeHandle, RuntimeInput};
672 use crate::error::EngineError;
673 use crate::runtime::{Mfa, NifEntry, NifRegistration, RuntimeConfig, SignalDeliveryConfig};
674
675 fn forty_two(args: &[Term], _context: &mut ProcessContext) -> Result<Term, Term> {
676 if args.len() > 255 {
677 return Err(Term::small_int(0));
678 }
679 Ok(Term::small_int(42))
680 }
681
682 fn thirteen(args: &[Term], _context: &mut ProcessContext) -> Result<Term, Term> {
683 if args.len() > 255 {
684 return Err(Term::small_int(0));
685 }
686 Ok(Term::small_int(13))
687 }
688
689 fn binary_length(args: &[Term], _context: &mut ProcessContext) -> Result<Term, Term> {
690 match args {
691 [term] => BinaryRef::new(*term)
692 .and_then(|binary| i64::try_from(binary.as_bytes().len()).ok())
693 .map(Term::small_int)
694 .ok_or_else(|| Term::small_int(0)),
695 _ => Err(Term::small_int(0)),
696 }
697 }
698
699 fn native_call_module_for_test(
700 module: beamr::atom::Atom,
701 function: beamr::atom::Atom,
702 target_module: beamr::atom::Atom,
703 target_function: beamr::atom::Atom,
704 native_entry: Option<beamr::native::NativeEntry>,
705 ) -> Module {
706 native_call_module_with_arity_for_test(
707 module,
708 function,
709 target_module,
710 target_function,
711 0,
712 native_entry,
713 )
714 }
715
716 fn native_call_module_with_arity_for_test(
717 module: beamr::atom::Atom,
718 function: beamr::atom::Atom,
719 target_module: beamr::atom::Atom,
720 target_function: beamr::atom::Atom,
721 arity: u8,
722 native_entry: Option<beamr::native::NativeEntry>,
723 ) -> Module {
724 let label = 1;
725 let code = vec![
726 Instruction::Label { label },
727 Instruction::CallExt {
728 arity: Operand::Unsigned(arity.into()),
729 import: Operand::Unsigned(0),
730 },
731 Instruction::Return,
732 ];
733 let mut module_data = Module {
734 name: module,
735 generation: 0,
736 origin: beamr::module::ModuleOrigin::Preloaded,
737 exports: std::collections::HashMap::from([((function, arity), label)]),
738 label_index: std::collections::HashMap::from([(label, 0)]),
739 code,
740 function_table: Vec::new(),
741 line_table: Vec::new(),
742 literals: Vec::new(),
743 constant_pool: beamr::constant_pool::ConstantPool::new(),
744 resolved_imports: Vec::new(),
745 lambdas: Vec::new(),
746 string_table: Vec::new(),
747 line_info: Vec::new(),
748 };
749 if let Some(native_entry) = native_entry {
750 module_data.resolved_imports.push(ResolvedImport {
751 module: target_module,
752 function: target_function,
753 arity,
754 target: ResolvedImportTarget::Native(native_entry),
755 });
756 }
757 module_data
758 }
759
760 fn assert_send_sync<T: Send + Sync>() {}
761
762 fn fixture_workflow_beam() -> &'static [u8] {
763 include_bytes!("../../tests/fixtures/aion_fixture_workflow.beam")
764 }
765
766 #[test]
767 fn runtime_handle_is_send_sync() {
768 assert_send_sync::<RuntimeHandle>();
769 }
770
771 #[test]
772 fn registers_spawns_and_shuts_down() -> Result<(), Box<dyn std::error::Error>> {
773 let runtime = RuntimeHandle::new(RuntimeConfig::new(None))?;
774 runtime.register_module("aion_fixture_workflow", fixture_workflow_beam())?;
775
776 let pid =
777 runtime.spawn_workflow("aion_fixture_workflow", "wait", RuntimeInput::default())?;
778 assert!(runtime.cancel_pid(pid).is_ok());
779 runtime.shutdown()?;
780 Ok(())
781 }
782
783 #[test]
784 fn signal_delivery_to_dead_process_returns_typed_error()
785 -> Result<(), Box<dyn std::error::Error>> {
786 let signal_delivery =
787 SignalDeliveryConfig::new(Duration::ZERO, 1, Duration::ZERO, Duration::ZERO);
788 let runtime =
789 RuntimeHandle::new(RuntimeConfig::new(Some(1)).with_signal_delivery(signal_delivery))?;
790 let pid = runtime.spawn_test_process()?;
791 runtime.terminate_test_process_with_error(pid)?;
792
793 let error = runtime
794 .deliver_signal_received(pid)
795 .err()
796 .ok_or("dead process delivery unexpectedly succeeded")?;
797
798 assert!(matches!(error, EngineError::Runtime { .. }));
799 runtime.shutdown()?;
800 Ok(())
801 }
802
803 #[test]
804 fn duplicate_nif_mfa_returns_typed_error() -> Result<(), Box<dyn std::error::Error>> {
805 let runtime = RuntimeHandle::new(RuntimeConfig::new(None))?;
806 let mfa = Mfa::new("host", "answer", 0);
807 let mut registration = NifRegistration::new();
808 registration.add_host_nifs([
809 NifEntry::new(mfa.clone(), forty_two),
810 NifEntry::dirty(mfa, thirteen),
811 ]);
812
813 let error = runtime.install_nifs(registration).err();
814
815 assert!(matches!(
816 error,
817 Some(EngineError::NifRegistration { reason })
818 if reason.contains("host:answer/0")
819 ));
820 assert_eq!(runtime.registered_nif_modules(), vec!["host"]);
821 runtime.shutdown()?;
822 Ok(())
823 }
824
825 #[test]
826 fn payload_binary_remains_valid_through_spawn_and_is_released()
827 -> Result<(), Box<dyn std::error::Error>> {
828 let runtime = RuntimeHandle::new(RuntimeConfig::new(None))?;
829 let mfa = Mfa::new("host", "binary_length", 1);
830 let mut registration = NifRegistration::new();
831 registration.add_host_nifs([NifEntry::new(mfa, binary_length)]);
832 runtime.install_nifs(registration)?;
833
834 let native_entry = runtime.lookup_native_for_test("host", "binary_length", 1);
835 let module = native_call_module_with_arity_for_test(
836 runtime.atom_table.intern("payload_echo"),
837 runtime.atom_table.intern("run"),
838 runtime.atom_table.intern("host"),
839 runtime.atom_table.intern("binary_length"),
840 1,
841 native_entry,
842 );
843 runtime.module_registry.insert(module);
844 let payload = Payload::new(
845 aion_core::ContentType::Json,
846 br#"{"hello":"world"}"#.to_vec(),
847 );
848
849 let pid =
850 runtime.spawn_workflow("payload_echo", "run", RuntimeInput::from_payload(&payload)?)?;
851 assert_eq!(runtime.retained_spawn_heap_count_for_test(), 1);
852 let (reason, result) = runtime.run_until_exit_for_test(pid);
853
854 assert_eq!(reason, beamr::process::ExitReason::Normal);
855 assert_eq!(
856 result.as_small_int(),
857 Some(i64::try_from(payload.bytes().len()).unwrap_or(0))
858 );
859 assert_eq!(runtime.retained_spawn_heap_count_for_test(), 0);
860 runtime.shutdown()?;
861 Ok(())
862 }
863
864 #[test]
865 fn workflow_outcome_releases_payload_heaps() -> Result<(), Box<dyn std::error::Error>> {
866 let runtime = RuntimeHandle::new(RuntimeConfig::new(None))?;
867 let mfa = Mfa::new("host", "binary_length", 1);
868 let mut registration = NifRegistration::new();
869 registration.add_host_nifs([NifEntry::new(mfa, binary_length)]);
870 runtime.install_nifs(registration)?;
871
872 let native_entry = runtime.lookup_native_for_test("host", "binary_length", 1);
873 let module = native_call_module_with_arity_for_test(
874 runtime.atom_table.intern("payload_workflow_outcome"),
875 runtime.atom_table.intern("run"),
876 runtime.atom_table.intern("host"),
877 runtime.atom_table.intern("binary_length"),
878 1,
879 native_entry,
880 );
881 runtime.module_registry.insert(module);
882 let payload = Payload::new(
883 aion_core::ContentType::Json,
884 br#"{"workflow":"outcome"}"#.to_vec(),
885 );
886
887 let pid = runtime.spawn_workflow(
888 "payload_workflow_outcome",
889 "run",
890 RuntimeInput::from_payload(&payload)?,
891 )?;
892 assert_eq!(runtime.retained_spawn_heap_count_for_test(), 1);
893 let outcome = runtime.workflow_outcome(pid)?;
894
895 assert_eq!(
896 outcome?,
897 Payload::from_json(&serde_json::json!(payload.bytes().len()))?
898 );
899 assert_eq!(runtime.retained_spawn_heap_count_for_test(), 0);
900 runtime.shutdown()?;
901 Ok(())
902 }
903
904 #[test]
905 fn repeated_completed_payload_spawns_do_not_accumulate_retained_heaps()
906 -> Result<(), Box<dyn std::error::Error>> {
907 let runtime = RuntimeHandle::new(RuntimeConfig::new(None))?;
908 let mfa = Mfa::new("host", "binary_length", 1);
909 let mut registration = NifRegistration::new();
910 registration.add_host_nifs([NifEntry::new(mfa, binary_length)]);
911 runtime.install_nifs(registration)?;
912
913 let native_entry = runtime.lookup_native_for_test("host", "binary_length", 1);
914 let module = native_call_module_with_arity_for_test(
915 runtime.atom_table.intern("payload_echo_many"),
916 runtime.atom_table.intern("run"),
917 runtime.atom_table.intern("host"),
918 runtime.atom_table.intern("binary_length"),
919 1,
920 native_entry,
921 );
922 runtime.module_registry.insert(module);
923 let payload = Payload::new(
924 aion_core::ContentType::Json,
925 br#"{"iteration":true}"#.to_vec(),
926 );
927
928 for _ in 0..1_000 {
929 let pid = runtime.spawn_workflow(
930 "payload_echo_many",
931 "run",
932 RuntimeInput::from_payload(&payload)?,
933 )?;
934 let (reason, result) = runtime.run_until_exit_for_test(pid);
935 assert_eq!(reason, beamr::process::ExitReason::Normal);
936 assert_eq!(
937 result.as_small_int(),
938 Some(i64::try_from(payload.bytes().len()).unwrap_or(0))
939 );
940 assert_eq!(runtime.retained_spawn_heap_count_for_test(), 0);
941 }
942
943 runtime.shutdown()?;
944 Ok(())
945 }
946
947 #[test]
948 fn distinct_nifs_are_registered_and_callable() -> Result<(), Box<dyn std::error::Error>> {
949 let runtime = RuntimeHandle::new(RuntimeConfig::new(None))?;
950 let mut registration = NifRegistration::new();
951 registration.add_engine_nifs().add_host_nifs([
952 NifEntry::new(Mfa::new("host", "answer", 0), forty_two),
953 NifEntry::dirty(Mfa::new("host", "thirteen", 0), thirteen),
954 ]);
955
956 runtime.install_nifs(registration)?;
957
958 assert_eq!(
959 runtime.registered_nif_modules(),
960 vec!["aion_flow_ffi", "host"]
961 );
962 let answer = runtime.lookup_native_for_test("host", "answer", 0);
963 assert!(answer.is_some());
964 assert!(
965 runtime
966 .lookup_native_for_test("host", "thirteen", 0)
967 .is_some_and(|entry| entry.dirty_kind.is_some())
968 );
969
970 let host_nif_call = native_call_module_for_test(
971 runtime.atom_table.intern("host_nif_call"),
972 runtime.atom_table.intern("answer"),
973 runtime.atom_table.intern("host"),
974 runtime.atom_table.intern("answer"),
975 answer,
976 );
977 runtime.module_registry.insert(host_nif_call);
978 let pid = runtime.spawn_workflow("host_nif_call", "answer", RuntimeInput::default())?;
979 let (reason, result) = runtime.run_until_exit_for_test(pid);
980
981 assert_eq!(reason, beamr::process::ExitReason::Normal);
982 assert_eq!(result, Term::small_int(42));
983 runtime.shutdown()?;
984 Ok(())
985 }
986}