Gilbert Nwaiwu
Gilbert Nwaiwu

Reputation: 728

Shared state doesn't work because of lifetimes

Im using the Axum framework to build a simple rest server. I want to have a sort of an "App State" that will have some reusable components for some of my handlers to reuse/consume.

#![allow(incomplete_features)]
#![feature(async_fn_in_trait)]

use axum::{routing::post, Router};
use std::{net::SocketAddr, sync::Arc};

mod modules;
use modules::auth::handlers::login;

pub struct AppState<'a> {
    user_credentials_repository: UserCredentialsRepository<'a>,
}

pub struct UserCredentialsRepository<'a> {
    shared_name: &'a mut String,
}

impl<'a> UserCredentialsRepository<'a> {
    pub fn new(shared_name: &'a mut String) -> UserCredentialsRepository<'a> {
        UserCredentialsRepository { shared_name }
    }
}

#[tokio::main]
async fn main() {
    let mut name = String::from("Tom");
    let mut user_credentials_repository = UserCredentialsRepository::new(&mut name);

    let shared_state = Arc::new(AppState {
        user_credentials_repository,
    });

    let app = Router::new()
        .route("/login", post(login))
        .with_state(shared_state);

    let addr = SocketAddr::from(([127, 0, 0, 1], 7777));

    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

Basically what I'm trying to achieve is to reuse a db session/connection instance. Im testing it out by trying to share a string first but it doesn't seem to work. I get the following errors:

`name` dropped here while still borrowed

and

argument requires that `name` is borrowed for `'static`

I have somewhat of an idea how lifetimes work but I'm guessing something inside ".with_state(...)" takes ownership over the value I'm passing and tries to drop it?

Upvotes: 4

Views: 1587

Answers (1)

drewtato
drewtato

Reputation: 12812

There's two problems: lifetimes and shared mutability.

  1. Because shared_state may be sent to another thread (by the tokio runtime), rust doesn't know if shared_state will still be around when the main function drops name. This would fail to compile even if it was & instead of &mut.
  2. Router state requires Clone, which &mut types do not implement. This is because if you receive multiple requests, you may have multiple handlers trying to access the same state at the same time. It's undefined behavior for more than one &mut to exist for the same variable at the same time, and that's enforced in safe code by not allowing &mut to be Clone.

Your attempt at solving #1 by putting the state in Arc isn't working here because it still contains a reference. You need to replace the reference with Arc.

And the solution to #2 is to use a shared mutability construct, such as Mutex or RwLock.

First, you need to remove references:


pub struct UserCredentialsRepository {
    shared_name: String,
}

impl UserCredentialsRepository {
    pub fn new(shared_name: String) -> UserCredentialsRepository {
        UserCredentialsRepository { shared_name }
    }
}

While you can directly replace the &mut with Mutex and get it working, I'd start with something simpler.

Let's leave the Arc in main and wrap user_credentials_repository with a Mutex:

pub struct AppState {
    user_credentials_repository: Mutex<UserCredentialsRepository>,
}

Then somewhere in your login function, you'll need to lock the Mutex to read and write to it:

let lock = state.user_credentials_repository.lock().unwrap();
lock.shared_name = "New name".to_string();

This should compile and work as expected.

Performance

If you have many separate items that may be accessed individually, you might want to put the Mutex on each one instead (similar to your original structure):

pub struct AppState {
    user_credentials_repository: UserCredentialsRepository,
}

pub struct UserCredentialsRepository {
    shared_name: Mutex<String>,
    other_state: Mutex<String>,
}

Then separate threads can lock separate items without one of them blocking.

If you expect to frequently read data and infrequently write data, you can use RwLock, which allows any number of reading locks as long as there are no writing locks:

pub struct AppState {
    user_credentials_repository: RwLock<UserCredentialsRepository>,
}

Usage is almost the same as Mutex, but instead of the lock method, there is read and write.

Upvotes: 6

Related Questions