lunatic_process/wasm.rs
1use std::sync::Arc;
2
3use anyhow::Result;
4use log::trace;
5use tokio::task::JoinHandle;
6use wasmtime::{ResourceLimiter, Val};
7
8use crate::env::Environment;
9use crate::runtimes::wasmtime::{WasmtimeCompiledModule, WasmtimeRuntime};
10use crate::state::ProcessState;
11use crate::{Process, Signal, WasmProcess};
12
13/// Spawns a new wasm process from a compiled module.
14///
15/// A `Process` is created from a `module`, entry `function`, array of arguments and config. The
16/// configuration will define some characteristics of the process, such as maximum memory, fuel
17/// and host function properties (filesystem access, networking, ..).
18///
19/// After it's spawned the process will keep running in the background. A process can be killed
20/// with `Signal::Kill` signal. If you would like to block until the process is finished you can
21/// `.await` on the returned `JoinHandle<()>`.
22pub async fn spawn_wasm<S>(
23 env: Arc<dyn Environment>,
24 runtime: WasmtimeRuntime,
25 module: &WasmtimeCompiledModule<S>,
26 state: S,
27 function: &str,
28 params: Vec<Val>,
29 link: Option<(Option<i64>, Arc<dyn Process>)>,
30) -> Result<(JoinHandle<Result<S>>, Arc<dyn Process>)>
31where
32 S: ProcessState + Send + Sync + ResourceLimiter + 'static,
33{
34 let id = state.id();
35 trace!("Spawning process: {}", id);
36 let signal_mailbox = state.signal_mailbox().clone();
37 let message_mailbox = state.message_mailbox().clone();
38
39 let instance = runtime.instantiate(module, state).await?;
40 let function = function.to_string();
41 let fut = async move { instance.call(&function, params).await };
42 let child_process = crate::new(fut, id, env.clone(), signal_mailbox.1, message_mailbox);
43 let child_process_handle = Arc::new(WasmProcess::new(id, signal_mailbox.0.clone()));
44
45 env.add_process(id, child_process_handle.clone());
46
47 // **Child link guarantees**:
48 // The link signal is going to be put inside of the child's mailbox and is going to be
49 // processed before any child code can run. This means that any failure inside the child
50 // Wasm code will be correctly reported to the parent.
51 //
52 // We assume here that the code inside of `process::new()` will not fail during signal
53 // handling.
54 //
55 // **Parent link guarantees**:
56 // A `tokio::task::yield_now()` call is executed to allow the parent to link the child
57 // before continuing any further execution. This should force the parent to process all
58 // signals right away.
59 //
60 // The parent could have received a `kill` signal in its mailbox before this function was
61 // called and this signal is going to be processed before the link is established (FIFO).
62 // Only after the yield function we can guarantee that the child is going to be notified
63 // if the parent fails. This is ok, as the actual spawning of the child happens after the
64 // call, so the child wouldn't even exist if the parent failed before.
65 //
66 // TODO: The guarantees provided here don't hold anymore in a distributed environment and
67 // will require some rethinking. This function will be executed on a completely
68 // different computer and needs to be synced in a more robust way with the parent
69 // running somewhere else.
70 if let Some((tag, process)) = link {
71 // Send signal to itself to perform the linking
72 process.send(Signal::Link(None, child_process_handle.clone()));
73 // Suspend itself to process all new signals
74 tokio::task::yield_now().await;
75 // Send signal to child to link it
76 signal_mailbox
77 .0
78 .send(Signal::Link(tag, process))
79 .expect("receiver must exist at this point");
80 }
81
82 // Spawn a background process
83 trace!("Process size: {}", std::mem::size_of_val(&child_process));
84 let join = tokio::task::spawn(child_process);
85 Ok((join, child_process_handle))
86}