Documentation v0.19.3 all versions

Activities

Activities are the fundamental building blocks of work within an Obelisk workflow. They represent individual, idempotent operations that perform a specific task, typically a network activity. Think of them as the "verbs" of your workflow – the actions that get things done. Activities are designed to be resilient, with built-in mechanisms for handling errors and ensuring reliable execution.

Defining Features of Activities

  • Idempotency: This is the most crucial aspect of an activity. An activity must be idempotent, meaning it can be executed multiple times with the same input and produce the same result without unintended side effects. This is essential for retries and ensuring the integrity of your workflow, even in the face of failures. The responsibility for fulfilling this contract lies with the activity's implementation.

  • WASM Sandboxing: Activities are executed within a WebAssembly (WASM) sandbox. This provides a secure, isolated environment, preventing activities from having unrestricted access to the system.

  • Resilience: Obelisk automatically handles retries for activities that fail due to errors, timeouts, or panics(traps). This retry mechanism, combined with idempotency, makes activities highly reliable. The input parameters and execution results of activities are persistently stored, contributing to this resilience.

Creating an Activity

  1. Definition: You define a workflow as code. Each function must have its signature specified using WIT (WebAssembly Interface Type) IDL. Any language that is supported by wit-bindgen including Rust and Go can be used.

  2. Compilation to WASM: Your activity code is compiled into a WebAssembly Component.

  3. Deployment: Create a new [[wasm_activity]] table with the compiled WASM file location in the Obelisk configuration file.

  4. Execution: The activity can be triggered directly using CLI, WebUI or gRPC, but most commonly it is executed as a step in a workflow. Obelisk persists the execution submission and execution result. If the activity times out, panics(traps) or returns an error, it will be retried after a timeout.

Example - calculating Fibonacci

In this contrived example we will create an activity that does not communicate with an external system. Its purpose is to show the interactions between activities and workflows. First we need to define an interface with a single function using WIT IDL. We will then export the interface.

package template-fibo:activity;

interface fibo-activity-ifc {
    fibo: func(n: u8) -> u64;
}
world any {
    export fibo-activity-ifc;
}

The corresponding bindings are generated automatically using wit-bindgen if we place the WIT file in the wit folder. Then we implement the Guest trait in Rust.

impl Guest for Component {
    fn fibo(n: u8) -> u64 {
        if n == 0 {
            0
        } else if n == 1 {
            1
        } else {
            Self::fibo(n - 1) + Self::fibo(n - 2)
        }
    }
}

In order to simplify creating the Cargo project, you can use cargo-generate:

cargo generate obeli-sk/obelisk-templates fibo/activity --name myactivity
cd myactivity
cargo build --release
obelisk server run

Follow the template's README to execute the activity.

HTTP Client

Activities can make HTTP requests using the WASI 0.2 HTTP client. This allows them to interact with external services and APIs. Importantly, this HTTP client is controlled, with built-in tracing to enhance security and predictability. Disk I/O is not permitted.

Example - WAKI based HTTP client

Instead of using the generated bindings directly we can use the waki HTTP client.

impl Guest for Component {
    fn get(url: String) -> Result<String, String> {
        let resp = waki::Client::new()
            .get(&url)
            .connect_timeout(Duration::from_secs(1))
            .send()
            .map_err(|err| format!("{err:?}"))?;
        let body = resp.body().map_err(|err| format!("{err:?}"))?;
        Ok(String::from_utf8_lossy(&body).into_owned())
    }
}

#[cfg(test)]
mod tests {

    #[test]
    fn integration_test() {
        let url = std::env::var("TEST_URL").expect("TEST_URL must be set");
        let body = Component::get(url).unwrap();
        println!("body: {body}");
    }
}

This project can be generated using the http-simple template

cargo generate obeli-sk/obelisk-templates http-simple/activity

If you need to use GraphQL, check out the cynic crate and the following template:

cargo generate obeli-sk/obelisk-templates graphql-github/activity

Note: If you need an async HTTP client, use wstd crate instead of waki.

Logging, debugging, testing

To understand the internal state of an activity, you can use the following:

  • printf style debugging: enable forward_stdout or forward_stderr in the [[activity]] section of the configuratin. Output will appear in the server log.
  • obelisk:log API - see Runtime Support

Unit testing is possible with Cargo's test harness.

Run the testing WASM artifact as usual:

cargo test

Make sure to add the following in your .cargo/config.toml, so that the test binary is run with wasmtime or other WASI 0.2 compatible runtime:

[build]
target = "wasm32-wasip2"

[target.wasm32-wasip2]
runner = "wasmtime run -Scli -Shttp  --env TEST_URL"

The stargazers demo repository contains unit and integration tests for activities.

On this page