#context #async-context #deadlines #call-stack #down #values #demand

nightly no-std context-rs

Pass values down the async call stack, with no_std and no_alloc support

2 unstable releases

0.2.2 Jan 23, 2023
0.2.1 Jan 23, 2023
0.2.0 Jan 22, 2023
0.1.0 Jan 21, 2023

#1643 in Rust patterns

MIT license

25KB
395 lines

context-rs

Go has a history of suggesting you provide a ctx context.Context parameter to all functions in an async context, such as web servers. This is useful for passing deadlines and such down into the callstack, to allow leaf-functions to schedule shutdowns.

Rust already passes a context value automatically for you in all async functions, this is already named Context but it's heavily under-featured - only providing a 'wake-up' handle.

Making use of the nightly Provider API, we can modify this context to provide values on demand down the callstack. This avoids using thread_locals, which requires std, or passing through a TypeMap with every function call, which is unergonomic and requires alloc.

Examples

A demonstration of an async deadline, using get_value and provide_ref

use context_rs::{get_value, ProviderFutExt};
use std::time::{Instant, Duration};

// New type makes it easier to have unique keys in the context
#[derive(Clone)]
struct Deadline(Instant);

#[derive(Debug, PartialEq)]
struct Expired;

impl Deadline {
    // check if the deadline stored in the context has expired
    // returns OK if no deadline is stored.
    async fn expired() -> Result<(), Expired> {
        get_value().await.map(|Deadline(deadline)| {
            // if there is a deadline set, check if it has expired
            if deadline < Instant::now() {
                Err(Expired)
            } else {
                Ok(())
            }
        }).unwrap_or(Ok(())) // or ignore it if no deadline is set
    }
}

// some top level work - agnostic to the context
async fn some_work() -> Result<(), Expired> {
    loop {
        some_nested_function().await?
    }
}

// some deeply nested work, cares about the deadline context
async fn some_nested_function() -> Result<(), Expired> {
    // will acquire the deadline from the context itself
    Deadline::expired().await?;

    // do some logic in here

    Ok(())
}

#[tokio::main]
async fn main() {
    // timeout in 2 seconds
    let deadline = Instant::now() + Duration::from_secs(2);

    let res = some_work().provide_ref(&Deadline(deadline)).await;

    assert_eq!(res, Err(Expired));
}

If you only need to access the value temporarily, and the value you want is expensive to clone, you can use with_ref instead of get_value. This will accept a closure with the ref provided for a short lived lifetime.

Dependencies

~0–5.5MB
~20K SLoC