[solved] Lifetime issues when trying to cache Futures

See full example - please comment in the commented-out lines to see my issue.

I'm trying to write a actix-web based server to serve views into relatively slow-to-parse data via a JSON API. In order to not re-parse the data on every access, I want to use a shared state to keep a LRU cache of previously-parsed data sets. Let's pretend that the parse_data function used to generate the data looks like this:

#[derive(Debug)]
struct Datum {
    value: String,
}

fn parse_data(id: &str) -> Box<dyn Future<Item = Datum, Error = Error> + Send> {
    Box::new(ok(Datum {
        value: format!("Data for ID '{}'", id),
    }))
}

To cache the "slow" parse_data function, I want to use the actix-web Data functionality by building up a shared State object that contains an lru_cache::LruCache to keep already-parsed objects in shared memory:

struct State {
    data: Mutex<LruCache<String, Box<dyn Future<Item = Arc<Datum>, Error = Error> + Send>>>,
}

I'm now trying to implement a get function for State that will either get a pre-existing future from the LruCache or create a new future and store that in the cache:

impl State {
    fn get(&self, id: &str) -> Box<dyn Future<Item = Arc<Datum>, Error = Error> + Send> {
        let locked = self.data.lock().expect("Locking mutex");

        if let Some(fut) = locked.get_mut(id) {
            println!("Cache hit on ID '{}'", id);
            return Box::new(fut);
        }

        println!("Cache miss on ID '{}", id);

        let mut fut_parse = Box::new(parse_data(id).map(Arc::new));

        locked.insert(id.to_owned(), fut_parse);

        fut_parse
    }
}

However, rustc rightly complains about an issue with the lifetimes on the Mutex lock:

error[E0495]: cannot infer an appropriate lifetime for autoref due to conflicting requirements
  --> src/main.rs:29:32
   |
29 |         let locked = self.data.lock().expect("Locking mutex");
   |                                ^^^^
   |
note: first, the lifetime cannot outlive the anonymous lifetime #1 defined on the method body at 28:5...
  --> src/main.rs:28:5
   |
28 | /     fn get(&self, id: &str) -> Box<dyn Future<Item = Arc<Datum>, Error = Error> + Send> {
29 | |         let locked = self.data.lock().expect("Locking mutex");
30 | |
31 | |         if let Some(fut) = locked.get_mut(id) {
...  |
42 | |         fut_parse
43 | |     }
   | |_____^
note: ...so that reference does not outlive borrowed content
  --> src/main.rs:29:22
   |
29 |         let locked = self.data.lock().expect("Locking mutex");
   |                      ^^^^^^^^^
   = note: but, the lifetime must be valid for the static lifetime...
   = note: ...so that the expression is assignable:
           expected std::boxed::Box<(dyn futures::future::Future<Error = failure::error::Error, Item = std::sync::Arc<Datum>> + std::marker::Send + 'static)>
              found std::boxed::Box<dyn futures::future::Future<Error = failure::error::Error, Item = std::sync::Arc<Datum>> + std::marker::Send>

error: aborting due to previous error

error: Could not compile `async-cache-test`.

To learn more, run the command again with --verbose.

I understand that the Futures returned by the method are affected by the lifetime of locking the Mutex, but I've been struggling with how to change my design to get it working.

Please be aware that in my full example, I've commented out my caching code to show that everything else is working. If you want to reproduce my error message, please remove comments on all commented-out lines or go to the non-commented-out version instead.

Thanks in advance for any hints!

You can't store or return data from under the lock. If the data could escape the lock, and be used outside the function that holds the lock, it'd defeat the purpose of the lock.

It's not a lifetime problem, but a good old bug that Rust has prevented you from making:

if let Some(fut) = locked.get_mut(id) {
   return Box::new(fut);
}

fut is locked. If you box and return it, the locked data would be accessible without the lock, leading to memory corruption.


With Actix general rule for lifetime problems is you need more stuff wrapped in Arc, and then more Arc and then some more layers of Arc<Mutex<>> around it again.

If you had just any kind of data under the lock, then the solution would be to use Arc, and then return another Arc:

if let Some(data) = locked.get(id) {
    return Arc::clone(data);
}

but Futures are special. They're very different from JS Promise that could be cached. Futures are one-time-use only. As soon as they return their result, once, they're allowed to self-destruct.

If you want to cache a Future, you have to use the special shareable one:

1 Like

Thank you so much! The hint about Shared helped me fix my mistake. I didn't know that Futures were single-use in Rust, thanks for teaching me that.

I have uploaded a working version of my original example here:

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.