Crate io_context [] [src]

This crate defines a Context, which carries deadlines, cancelations signals and request scoped values across API boundaries.

Incoming server requests should create a request specific Context from the background context. While outgoing requests to servers should accept a Context in there methods to allow for cancelation and deadlines. A chain of funtions calls for handling a request should propagate the Context, optionally adding their own deadlines or cancelation signals. As demostrated in the example below.

fn main() {
    // First create our background context. To this context we could add
    // signal handling, e.g. when the user pressed ctrl-c.
    let mut ctx = Context::background();
    // This signal should be canceled once ctrl-c is pressed.
    let cancel_signal = ctx.add_cancel_signal();
    let ctx = ctx.freeze();
    loop {
        // Create a context for our request. This will copy any deadlines
        // and cancelation signals from the background context into the
        // request specific one.
        //
        // However adding a deadline or cancelation signal to the client
        // will not after the background context.
        let request_ctx = Context::create_child(&ctx);

        // Read a request.
        let request = read_request();
        // Pass the request context along with the request to the request
        // handler.
        let response = handle_request(request_ctx, request).unwrap();
        println!("got response: {:?}", response);
    }
}

fn handle_request(ctx: Context, request: Request) -> io::Result<Response> {
    // Check if the context's deadline was exceeded, or if the context was
    // canceled.
    if let Some(reason) = ctx.done() {
        // For convience `DoneReason`, returned by `Context.done`, can be
        // converted into an `io::Error`.
        return Err(reason.into());
    }

    // Make request to an external server, passing our request context.
    make_http_request(ctx, "https://api.example.com".to_owned())
}

// An outgoing request should accept a `Context` as first parameter.
fn make_http_request(ctx: Context, url: String) -> io::Result<Response> {
    // Again checking the context is done.
    if let Some(reason) = ctx.done() {
        return Err(reason.into());
    }
    // Here any deadlines should be added to the HTTP request, using
    // `context.deadline` method to retrieve it.

    // But we'll fake the response here, for the sake of simplicity.
    Ok(Response)
}

Contexts should not stored in structs. Instead functions and methods that need a context should accept it as first parameter. For more conventions see the Context convention documentation. A possible exception for this is a struct that implements a Future, which represents the state of that future.

Usage

In the main function of the program a background context should be created, which is used in creating all child (request specific) contexts.

Request deadlines can be added using the add_deadline and add_timeout methods defined on Context. While cancelation signals can be added using the add_cancel_signal method. Request scoped values, e.g. a request id, can be added using the add_value method. For examples see below and the documentation of different linked methods.

fn main() {
    // Create a background context.
    let mut ctx = Context::background();

    // Add cancelation to the context. We can use this to respond to a
    // kill signal from the user, e.g. when pressing ctrl-c.
    let cancel_signal = ctx.add_cancel_signal();

    // ... Some time later when we received a kill signal.
    cancel_signal.cancel(); // Now the context and it's children will be canceled.
}

In libraries

Libraries should accept contexts in their API, the convention is to accept it as first parameter in all methods and functions that need it. Do not store a context in a struct. For more conventions see the Context convention documentation.

use std::io;
use std::time::Duration;

struct Connection { /* Some fields. */ }

impl Connection {
    // This executes some long runnning operation.
    fn execute_operation(&self, ctx: Context, input: &[u8]) -> io::Result<()> {
        // Do step 1 of the work.
        work_step_1(input);

        // Check if the context is done, e.g. if the deadline has exceeded.
        if let Some(reason) = ctx.done() {
            return Err(reason.into());
        }

        // Do some more work.
        work_step_2();

        // Check the context again.
        if let Some(reason) = ctx.done() {
            rollback();
            return Err(reason.into());
        }

        // More work, etc.
        work_step_3();

        // Of course we can have even more preciese control if we pass the
        // context to our work functions as well, that is omitted in this
        // example.

        Ok(())
    }
}

fn main() {
    let mut ctx = Context::background();
    ctx.add_timeout(Duration::from_secs(5));
    let ctx = ctx.freeze();

    // We create a new child context just for the specific operation. Any
    // deadlines or cancelation signals added to the parent will be added
    // to the child context as well. However any deadlines or cancelation
    // signals added to the child will not affect the parent.
    let mut child_ctx = Context::create_child(&ctx);
    child_ctx.add_timeout(Duration::from_secs(1));

    // Now we execute the operation, while controlling it's deadline. We
    // could also add a cancelation signal so we could stop it from another
    // thread (or future), see `Context.add_cancel_signal`.
    let connection = Connection{};
    connection.execute_operation(child_ctx, b"some input").unwrap();
}

When should a Context be used?

A Context is mainly useful if an operation is long running. As a rule of thumb; if an operation is less then 10 milliseconds it is not worth it to add a Context to the operation.

Examples of when adding a Context is worth it:

  • Any operation which involve any kind of I/O (network, disk or otherwise).
  • Any operation which support cancelation or timeouts already.
  • Long looping operations which process input in batches.

The common theme in the example operations above is the fact that they all could be or are long running. If you, both as a developer and user of your application, would like more control over these kind of operations the use of a Context is a good fit.

Structs

CancelSignal

A cancelation signal, see Context.add_cancel_signal. See the crate documentation for an example.

Context

A context that carries a deadline, cancelation signals and request scoped values across API boundaries and between processes.

Enums

DoneReason

The reason why a context was stopped, see Context.done. This "error" can be turned into an io::Error by using the Into trait (From<DoneReason> is implemented for io::Error).