Rocket/site/guide/state.md
2018-08-07 23:55:25 -07:00

10 KiB

State

Many web applications have a need to maintain state. This can be as simple as maintaining a counter for the number of visits or as complex as needing to access job queues and multiple databases. Rocket provides the tools to enable these kinds of interactions in a safe and simple manner.

Managed State

The enabling feature for maintaining state is managed state. Managed state, as the name implies, is state that Rocket manages for your application. The state is managed on a per-type basis: Rocket will manage at most one value of a given type.

The process for using managed state is simple:

  1. Call manage on the Rocket instance corresponding to your application with the initial value of the state.
  2. Add a State<T> type to any request handler, where T is the type of the value passed into manage.

Adding State

To instruct Rocket to manage state for your application, call the manage method on an instance of Rocket. For example, to ask Rocket to manage a HitCount structure with an internal AtomicUsize with an initial value of 0, we can write the following:

struct HitCount {
    count: AtomicUsize
}

rocket::ignite().manage(HitCount { count: AtomicUsize::new(0) });

The manage method can be called any number of times as long as each call refers to a value of a different type. For instance, to have Rocket manage both a HitCount value and a Config value, we can write:

rocket::ignite()
    .manage(HitCount { count: AtomicUsize::new(0) })
    .manage(Config::from(user_input));

Retrieving State

State that is being managed by Rocket can be retrieved via the State type: a request guard for managed state. To use the request guard, add a State<T> type to any request handler, where T is the type of the managed state. For example, we can retrieve and respond with the current HitCount in a count route as follows:

#[get("/count")]
fn count(hit_count: State<HitCount>) -> String {
    let current_count = hit_count.count.load(Ordering::Relaxed);
    format!("Number of visits: {}", current_count)
}

You can retrieve more than one State type in a single route as well:

#[get("/state")]
fn state(hit_count: State<HitCount>, config: State<Config>) -> T { ... }

If you request a State<T> for a T that is not managed, Rocket won't call the offending route. Instead, Rocket will log an error message and return a 500 error to the client.

You can find a complete example using the HitCount structure in the state example on GitHub and learn more about the manage method and State type in the API docs.

Within Guards

It can also be useful to retrieve managed state from a FromRequest implementation. To do so, simply invoke State<T> as a guard using the Request::guard() method.

fn from_request(req: &'a Request<'r>) -> request::Outcome<T, ()> {
    let hit_count_state = req.guard::<State<HitCount>>()?;
    let current_count = hit_count_state.count.load(Ordering::Relaxed);
    ...
}

Request-Local State

While managed state is global and available application-wide, request-local state is local to a given request, carried along with the request, and dropped once the request is completed. Request-local state can be used whenever a Request is available, such as in a fairing, a request guard, or a responder.

Request-local state is cached: if data of a given type has already been stored, it will be reused. This is especially useful for request guards that might be invoked multiple times during routing and processing of a single request, such as those that deal with authentication.

As an example, consider the following request guard implementation for RequestId that uses request-local state to generate and expose a unique integer ID per request:

/// A global atomic counter for generating IDs.
static request_id_counter: AtomicUsize = AtomicUsize::new(0);

/// A type that represents a request's ID.
struct RequestId(pub usize);

/// Returns the current request's ID, assigning one only as necessary.
impl<'a, 'r> FromRequest<'a, 'r> for RequestId {
    fn from_request(request: &'a Request<'r>) -> request::Outcome {
        // The closure passed to `local_cache` will be executed at most once per
        // request: the first time the `RequestId` guard is used. If it is
        // requested again, `local_cache` will return the same value.
        Outcome::Success(request.local_cache(|| {
            RequestId(request_id_counter.fetch_add(1, Ordering::Relaxed))
        }))
    }
}

Note that, without request-local state, it would not be possible to:

1. Associate a piece of data, here an ID, directly with a request.
2. Ensure that a value is generated at most once per request.

For more examples, see the FromRequest documentation, which uses request-local state to cache expensive authentication and authorization computations, and the Fairing documentation, which uses request-local state to implement request timing.

Databases

While Rocket doesn't have built-in support for databases yet, you can combine a few external libraries to get native-feeling access to databases in a Rocket application. Let's take a look at how we might integrate Rocket with two common database libraries: diesel, a type-safe ORM and query builder, and r2d2, a library for connection pooling.

Our approach will be to have Rocket manage a pool of database connections using managed state and then implement a request guard that retrieves one connection. This will allow us to get access to the database in a handler by simply adding a DbConn argument:

#[get("/users")]
fn handler(conn: DbConn) { ... }

Dependencies

To get started, we need to depend on the diesel and r2d2 crates. For detailed information on how to use Diesel, please see the Diesel getting started guide. For this example, we use the following dependencies:

[dependencies]
rocket = "0.4.0-dev"
rocket_codegen = "0.4.0-dev"
diesel = { version = "<= 1.2", features = ["sqlite", "r2d2"] }

Your diesel dependency information may differ. The crates are imported as well:

extern crate rocket;
#[macro_use] extern crate diesel;

Managed Pool

The first step is to initialize a pool of database connections. The init_pool function below uses r2d2 to create a new pool of database connections. Diesel advocates for using a DATABASE_URL environment variable to set the database URL, and we use the same convention here. Excepting the long-winded types, the code is fairly straightforward: the DATABASE_URL environment variable is stored in the DATABASE_URL static, and an r2d2::Pool is created using the default configuration parameters and a Diesel SqliteConnection ConnectionManager.

use diesel::sqlite::SqliteConnection;
use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};

// An alias to the type for a pool of Diesel SQLite connections.
type SqlitePool = Pool<ConnectionManager<SqliteConnection>>;

// The URL to the database, set via the `DATABASE_URL` environment variable.
static DATABASE_URL: &'static str = env!("DATABASE_URL");

/// Initializes a database pool.
fn init_pool() -> SqlitePool {
    let manager = ConnectionManager::<SqliteConnection>::new(DATABASE_URL);
    Pool::new(manager).expect("db pool")
}

We then use managed state to have Rocket manage the pool for us:

fn main() {
    rocket::ignite()
        .manage(init_pool())
        .launch();
}

Connection Guard

The second and final step is to implement a request guard that retrieves a single connection from the managed connection pool. We create a new type, DbConn, that wraps an r2d2 pooled connection. We then implement FromRequest for DbConn so that we can use it as a request guard. Finally, we implement Deref with a target of SqliteConnection so that we can transparently use an &*DbConn as an &SqliteConnection.

use std::ops::Deref;
use rocket::http::Status;
use rocket::request::{self, FromRequest};
use rocket::{Request, State, Outcome};
use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};

// Connection request guard type: a wrapper around an r2d2 pooled connection.
pub struct DbConn(pub PooledConnection<ConnectionManager<SqliteConnection>>);

/// Attempts to retrieve a single connection from the managed database pool. If
/// no pool is currently managed, fails with an `InternalServerError` status. If
/// no connections are available, fails with a `ServiceUnavailable` status.
impl<'a, 'r> FromRequest<'a, 'r> for DbConn {
    type Error = ();

    fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
        let pool = request.guard::<State<SqlitePool>>()?;
        match pool.get() {
            Ok(conn) => Outcome::Success(DbConn(conn)),
            Err(_) => Outcome::Failure((Status::ServiceUnavailable, ()))
        }
    }
}

// For the convenience of using an &DbConn as an &SqliteConnection.
impl Deref for DbConn {
    type Target = SqliteConnection;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

Usage

With these two pieces in place, we can use DbConn as a request guard in any handler or other request guard implementation, giving our application access to a database. As a simple example, we might write a route that returns a JSON array of some Task structures that are fetched from a database:

#[get("/tasks")]
fn get_tasks(conn: DbConn) -> QueryResult<Json<Vec<Task>>> {
    all_tasks.order(tasks::id.desc())
        .load::<Task>(&*conn)
        .map(|tasks| Json(tasks))
}