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