Rust Interior Mutability with Async_std and Tide

Published

Normally if you make a struct in Rust, all of the fields in that struct have the same mutability as the struct itself - that is, if the struct is immutable, every field will be immutable as well.

struct MyStruct {
  my_field: i32
};

let a = MyStruct { my_field: 5 };
a.my_field = 50;  // Error! my_field is immutable because MyStruct is immutable.

But what if you really need to be able to safely modify a field? This is where the concept of "interior mutability" is used. For single threaded apps, you can use RefCell (documented here). RefCell gives you a wrapper that lets you safely encapsulate an object so that the container is immutable, but the data inside is mutable.

For web apps, such as the apps I've been building in Tide, you'll need to use an Arc (atomic reference counted container) and either a Mutex or a RwLock. The Mutex/RwLock allow the object to be safely shared between threads while ensuring that only one thread can read or write to the object at any given time.

In the Tide web framework, you have the concept of a state. This state must be immutable, cloneable, Send and Sync.

use tide::prelude::*;
use tera::Tera;
use tide_tera::prelude::*;

#[derive(Clone)]
struct State {
    tera: Tera,
}

#[async_std::main]
async fn main() -> tide::Result<()> {
    let bind_host = std::env::var("BIND_HOST")
        .unwrap_or("127.0.0.1:8080".to_string());

    let state = State {
        tera: Tera::new("templates/**/*").unwrap(),
    };

    // Create main tide app with our state object
    let mut app = tide::with_state(state);

    app.at("/").get(index).post(save);

    app.listen(bind_host).await?;

    Ok(())
}

async fn index(req: Request<State>) -> tide::Result {
    let state = req.state();
    let tera = &state.tera;

    tera.render_response("index.html", &context! {
        "content" => "some cool content",
    })
}

Here our state is simple - it's just a Tera context that lets us render HTML from templates. Tera doesn't need to be mutable so all is well. But, the moment you need something mutable, Rust will complain that the State struct is no longer Send and Sync, making it unusable for a multithreaded app.

For my purposes, I needed to be able to cache responses from an endpoint that was expensive to call. This means I needed to be able to write to some object that was accessible to every route. Boxing a mutable reference won't work - what you need to do is wrap the object in a Arc and then a Mutex.

Async_std provides these locking containers, but so does Tokio. I mostly just use Async_std because that's the default for Tide. The docs for using a Mutex in async_std are here:

use async_lock::Mutex;
use std::sync::Arc;

let mutex = Arc::new(Mutex::new(10));
let guard = mutex.lock_arc().await;
assert_eq!(*guard, 10);

Adapting the example to the web app, we can make State hold an Arc to a Mutex to some kind of cache that is empty to begin with, but gets filled in when a request is first made:

use async_std::sync::{Arc, Mutex};

#[derive(Clone)]
struct State {
    tera: Tera,
    expensive_cache: Arc<Mutex<Option<ExpensiveResponse>>>
}

// ...
#[async_std::main]
async fn main() -> tide::Result<()> {
    // ...
    let state = State {
        tera: Tera::new("templates/**/*").unwrap(),
        expensive_cache: Arc::new(Mutex::new(None))
    };
    /// ...
}

async fn index(req: Request<State>) -> tide::Result {
    let state = req.state();

    // Lock_arc "Acquires the mutex and clones a reference to it."
    let mut expensive_cache = state.expensive_cache.lock_arc().await;

    // We dereference the mutex but then take a reference so that the data
    // is not moved
    let response: webdav::DirectoryListing = match &*expensive_cache {
        None => {
            // Perform the expensive call if no cache was found
            let mut expensive_response = upstream.expensive_request().await;

            // Dereferencing the mutex lets you modify the contents inside,
            // allowing us to store a copy of the expensive response
            *expensive_cache = Some(expensive_response.clone());

            expensive_response
        }
        Some(response) => response.clone()
    };
    // ...
}

Breaking this down into steps, we:

  1. Create our interior mutable field in State. Our data is an Option<ExpensiveResponse>, and the containers are Arc<Mutex<T>>.
  2. Use lock_arc() to create a new reference and lock the Mutex guard, giving this thread (and region of code) exclusive read/write access to the interior data.
  3. Finally, write to the Mutex by dereferencing it with *.

You can also (and probably should) use a RwLock instead of a Mutex. A RwLock allows you to have many readers but only one writer at any given time. Since a cache is most likely going to be read and not written to, this seems pretty ideal. Or, since the container can be safely shared between threads, you can even safely have a background thread update the cache, and then it never needs to be written to by the web app.