Databrush Logo Databrush RENOIR

Tokio Feature

By default Renoir uses standard OS threads for both workers and network I/O. Enabling the tokio feature switches the runtime to use Tokio throughout, which unlocks async execution and a set of async operators.

Enabling the feature

[dependencies]
renoir = { version = "*", features = ["tokio"] }

What changes with tokio enabled

Runtime behavior

Area Without tokio With tokio
Worker threads OS threads tokio::task::spawn_blocking
Network stack Synchronous I/O Tokio async I/O (tokio::net)
execute_blocking() Joins OS threads directly Creates a Tokio multi-thread runtime internally, then blocks
execute().await not available available — runs inside an existing async context

Operators unlocked

Operator Without tokio With tokio
map_async not available available
map_async_memo not available available
map_async_memo_by not available available
Async stream source not available available

Running the context

In a blocking main

execute_blocking() works exactly the same as without the feature. When tokio is enabled it internally creates a multi-threaded Tokio runtime, so you do not need to create one yourself.

use renoir::prelude::*;

fn main() {
    let ctx = StreamContext::new_local();
    // ... streams and operators
    ctx.execute_blocking();
}

In an async main

With the tokio feature you can drive the context from inside an existing Tokio runtime using execute().await. This is useful when Renoir is part of a larger async application.

use renoir::prelude::*;

#[tokio::main]
async fn main() {
    let ctx = StreamContext::new_local();
    // ... streams and operators
    ctx.execute().await;
}

Async operators

map_async

Applies an async closure to each element of the stream. Up to 4 futures run concurrently by default (controlled by BatchMode).

use renoir::prelude::*;

#[tokio::main]
async fn main() {
    let ctx = StreamContext::new_local();
    let result = ctx
        .stream_iter(0..10u64)
        .map_async(|n| async move {
            // simulate an async operation, e.g. an HTTP call or DB query
            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
            n * 2
        })
        .collect_vec();
    ctx.execute().await;
    if let Some(v) = result.get() {
        println!("{v:?}");
    }
}

map_async_memo

Like map_async, but caches results by element value. Repeated elements skip the async call and return the cached output. Useful when the async operation is expensive and inputs repeat frequently.

let result = ctx
    .stream_iter(words)
    .map_async_memo(|word| async move { fetch_embedding(word).await }, 1024)
    .collect_vec();

The second argument is the cache capacity (number of entries to keep in memory).

map_async_memo_by

Like map_async_memo but lets you supply a separate key function, so the cache key can differ from the element itself.

let result = ctx
    .stream_iter(events)
    .map_async_memo_by(
        |event| async move { fetch_user_profile(event.user_id).await },
        |event| event.user_id,
        512,
    )
    .collect_vec();