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}