Search code examples
asynchronousrustlifetimeactix-web

Rust Actix Web use Hashmap in both middleware and app data lifetime error


I was trying to have a Hashmap that can be used in both app data and in the middleware, but I am getting lifetime errors.

I can do that with Atomics but there is no Atomic for hashmap, and a hashmap cannot be static, how do I make sure that it lives longer than the big chunky function below?

#[derive(Clone, Debug)]
struct HashMapContainer(pub Arc<Mutex<HashMap<String, u32>>>);

#[actix_web::main]
async fn main() -> io::Result<()> {
    // Here's what I want to do, but with a hashmap instead
    static COUNTER: AtomicU32 = AtomicU32::new(1);

    let hashmap = HashMapContainer(Arc::new(Mutex::new(HashMap::new())));

    HttpServer::new(move || {
        App::new()
            // Middleware function called every time when there is a request no matter what is the path
            .wrap_fn(|req, srv| {
                // increase counter by 1
                COUNTER.store(COUNTER.load(Relaxed) + 1, Relaxed);

                // So I can do stuff with the hashmap (should be mutable)
                dbg!(&hashmap.0.lock().unwrap());
                srv.call(req)
            })
            // I also want to use those 2 variables as app data
            .app_data(web::Data::new(SessionStats::new()))
            .app_data(web::Data::new(hashmap.clone()))
    })
    .bind(("0.0.0.0", 8080))?
    .run()
    .await?;

    Ok(())
}

Error:

error: lifetime may not live long enough
  --> src/main.rs:17:9
   |
16 |       HttpServer::new(move || {
   |                       ------- lifetime `'1` represents this closure's body
17 | /         App::new()
19 | |             .wrap_fn(|req, srv| {
...  |
23 | |                 srv.call(req)
24 | |             })
   | |______________^ argument requires that `'1` must outlive `'static`
   |
   = note: closure implements `Fn`, so references to captured variables can't escape the closure

Solution

  • Your approach of using an Arc<Mutex> is 100% correct.

    There are just some detail problems with your code.

    All the problems you see come from ownership. The closures all have to be move to own a Arc object. But then they consume the outer one, so you have to clone it beforehand. That makes the code a little ugly, but well, that's how it is.

    Here you go:

    use std::{
        collections::HashMap,
        io,
        sync::{
            atomic::{AtomicU32, Ordering::Relaxed},
            Arc, Mutex,
        },
    };
    
    use actix_web::{dev::Service, web, App, HttpServer};
    
    struct SessionStats;
    
    impl SessionStats {
        pub fn new() -> Self {
            Self
        }
    }
    
    #[derive(Clone, Debug)]
    struct HashMapContainer(pub Arc<Mutex<HashMap<String, u32>>>);
    
    #[actix_web::main]
    async fn main() -> io::Result<()> {
        // Here's what I want to do, but with a hashmap instead
        static COUNTER: AtomicU32 = AtomicU32::new(1);
    
        let hashmap = HashMapContainer(Arc::new(Mutex::new(HashMap::new())));
        // Clone here, this one will be owned by the first closure
        let hashmap_for_httpserver = hashmap.clone();
    
        HttpServer::new(move || {
            // Clone the hashmap owned by the closure.
            // The one that the closure owns is only borrowable, but we want to have an owned one so we can
            // move it into the next closure
            let hashmap_for_app = hashmap_for_httpserver.clone();
    
            App::new()
                // Middleware function called every time when there is a request no matter what is the path
                .wrap_fn(move |req, srv| {
                    // increase counter by 1
                    COUNTER.store(COUNTER.load(Relaxed) + 1, Relaxed);
    
                    // So I can do stuff with the hashmap (should be mutable)
                    dbg!(&hashmap_for_app.0.lock().unwrap());
                    srv.call(req)
                })
                // I also want to use those 2 variables as app data
                .app_data(web::Data::new(SessionStats::new()))
                .app_data(web::Data::new(hashmap_for_httpserver.clone()))
        })
        .bind(("0.0.0.0", 8080))?
        .run()
        .await?;
    
        Ok(())
    }