4

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?

Gilbert Nwaiwu
  • 687
  • 2
  • 13
  • 37
  • It's really hard to help you without you providing a [mre], including *full* error messages and all types required to compile the code, of course before pasting your whole code you shoud minify it, the linked article elaborates some more. Then [edit] your post to include it. In addition to the truncated error messages this question is missing definitions (or rather stubs thereof) of `UserCredentialsRepository`, `RegisterUseCase` (and their used methods) which I believe are all types you define or from a library you didn't mention. – cafce25 Jan 14 '23 at 22:01
  • My attempt at a bit psychic debugging: `UserCredentialsRepository::new` and `RegisterUseCase::new` should both probably be taking their argument by value rather than reference. – cafce25 Jan 14 '23 at 22:16
  • @cafce25 I modified my question as requested – Gilbert Nwaiwu Jan 14 '23 at 22:49
  • 1
    Change `new(shared_name: &'a mut String)` to `new(shared_name: String)` and remove the lifetime. – pigeonhands Jan 14 '23 at 23:00
  • @pigeonhands I want to be able to pass the string by reference to multiple entities. Say a UserCredentialsRepository and ClientRepository need to share the value – Gilbert Nwaiwu Jan 14 '23 at 23:16
  • If you want to pass it by reference (which seems questionable), if you also want to pass it to multiple entities, that reference cannot be mutable. That's just not allowed. – Peter Hall Jan 15 '23 at 01:11
  • @PeterHall that makes sense since you can't have multiple mut references. But it still doesn't work with a non-mut reference. Also, why would you say passing by reference is questionable? I feel like there is something fundamental I may not be getting. I just want to avoid creating multiple instances of a db instance. Like when you make a singleton – Gilbert Nwaiwu Jan 15 '23 at 01:51
  • Because this looks like config. There's no good reason why config can't own a string. What possible benefit do you get from using a reference? – Peter Hall Jan 15 '23 at 02:27
  • *"I just want to avoid creating multiple instances of a db instance"* - you are already achieving that by wrapping your `AppState` in an `Arc`, no? You create a single `AppState` where you then use multiple `Arc`s to access it. – kmdreko Jan 18 '23 at 16:57
  • 1
    @kmdreko say you have multiple repositories (e.g. UserRepository, ProductRepository) and you want all of them to reuse a DbConnection. – Gilbert Nwaiwu Jan 19 '23 at 08:57
  • Hey your quesstion is still missing full error messages. Please add the full text. – HrkBrkkl Jan 19 '23 at 14:51

1 Answers1

5

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.

drewtato
  • 6,783
  • 1
  • 12
  • 17
  • 1
    This makes sense and I was able to make it work on my end by getting rid of lifetimes (as you suggested). But if I can't use references, how do I get to reuse something like a db session? Not across threads but in two structs. Like say I have a method that returns a db_session and I want to use it to create a UserCredentialsRepository and a ProductRepository. My first thought would be to create a single db_session and pass it by reference. That wont work here – Gilbert Nwaiwu Jan 19 '23 at 23:06
  • 1
    This is what `Arc` does. You can keep the DB in an `Arc` and clone the `Arc` when you need another reference. – drewtato Jan 21 '23 at 00:33
  • To add to the last comment, it's possible the DB already uses `Arc` and you don't need to do it again. It'll say so in the documentation. – drewtato Jan 21 '23 at 01:20
  • 1
    @drewtato `First, you need to remove references` If the the state data struct containing references is from 3rd library, is that mean there's no way to use it in Axum state? – Saddle Point May 03 '23 at 10:16
  • @SaddlePoint you would need to ensure the references have `'static` lifetime – drewtato May 03 '23 at 18:41
  • @drewtato Sorry, I should be more clearer. I mean that reference is inside a 3rd library struct field but not my side. In this case, is that mean there's no way to use it inside the state since I can not change that lifetime? – Saddle Point May 04 '23 at 03:16
  • @SaddlePoint If you can't make the lifetime `'static`, then no – drewtato May 04 '23 at 03:48