9.8 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:
- Call
manage
on theRocket
instance corresponding to your application with the initial value of the state. - Add a
State<T>
type to any request handler, whereT
is the type of the value passed intomanage
.
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 { ... }
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);
...
}
Unmanaged State
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.
While this behavior is 100% safe, it isn't fun to return 500 errors to
clients, especially when the issue can be easily avoided. Because of this,
Rocket tries to prevent an application with unmanaged state from ever running
via the unmanaged_state
lint. The lint reads through your code at compile-time
and emits a warning when a State<T>
request guard is being used in a mounted
route for a type T
that isn't being managed.
As an example, consider the following short application using our HitCount
type from previous examples:
#[get("/count")]
fn count(hit_count: State<HitCount>) -> String {
let current_count = hit_count.count.load(Ordering::Relaxed);
format!("Number of visits: {}", current_count)
}
fn main() {
rocket::ignite()
.manage(Config::from(user_input))
.launch()
}
The application is buggy: a value for HitCount
isn't being managed
, but a
State<HitCount>
type is being requested in the count
route. When we compile
this application, Rocket emits the following warning:
warning: HitCount is not currently being managed by Rocket
--> src/main.rs:2:17
|
2 | fn count(hit_count: State<HitCount>) -> String {
| ^^^^^^^^
|
= note: this State request guard will always fail
help: maybe add a call to 'manage' here?
--> src/main.rs:8:5
|
8 | rocket::ignite()
| ^^^^^^^^^^^^^^^^
The unmanaged_state
lint isn't perfect. In particular, it cannot track calls
to manage
across function boundaries. Because of this, you may find yourself
with incorrect warnings. You can disable the lint on a per-route basis by adding
#[allow(unmanaged_state)]
to a route handler. If you wish to disable the lint
globally, add #![allow(unmanaged_state)]
to your crate attributes.
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.
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.3.14"
rocket_codegen = "0.3.14"
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))
}