Lifetime of object captured in future: a solution?


#1

I’m writing a Hyper-based web service, and I ran into a lifetime-related problem that I apparently solved. But I’m not totally if the solution is good practice.

My struct that implements Hyper’s Service trait contains a database which gets manipulated in response to incoming requests. One of these requests needs to read PUT data before it can do the manipulation. Using Hyper, this means the database operation happens on resolution of the future that represents reading the request data. Doing the database manipulation inside the and_then() closure (when the future resolves) proved difficult to compile. I got it working, but I’m wondering if it’s the best solution.

The structure has this element:

struct MyService {
  pub db: Arc<RwLock<HashMap<String,MyRecord>>>,
  /* Other stuff .... */
 }

This is the function handling this particular request (it’s called from the Service trait’s call() function). This version doesn’t compile:

fn put_response (&self, uri_path: &str, put_body: Body) -> Box<Future<Item=Response, Error=hyper::Error>> {

let fqdn = String::from(uri_path.trim_left_matches("/"));

Box::new(put_body.concat2().and_then(move |body| {
    match serde_json::from_slice::<Record>(&body.to_vec()) {
        Ok(record) => {
	    /* Below is the problem line. Removing it allows compilation. */
            self.db.write().unwrap().remove(&fqdn);

            futures::future::ok(Response::new().with_status(StatusCode::Accepted))
        },
        Err(e) => {
            let error = String::from(e.description());
            futures::future::ok(Response::new().with_status(StatusCode::BadRequest)
                                .with_body(error))
        },
    }
}))

}

The error is this. It’s very long, sorry:

error[E0495]: cannot infer an appropriate lifetime due to conflicting requirements
   --> src/service.rs:326:9
    |
326 | /         Box::new(put_body.concat2().and_then(move |body| {
327 | |             match serde_json::from_slice::<Record>(&body.to_vec()) {
328 | |                 Ok(record) => {
329 | |                     /* Below is the problem line. Removing it allows compilation. */
...   |
338 | |             }
339 | |         }))
    | |___________^
    |
note: first, the lifetime cannot outlive the anonymous lifetime #1 defined on the body at 322:110...
   --> src/service.rs:322:111
    |
322 |       fn put_response (&self, uri_path: &str, put_body: Body) -> Box<Future<Item=Response, Error=hyper::Error>> {
    |  _______________________________________________________________________________________________________________^
323 | |
324 | |         let fqdn = String::from(uri_path.trim_left_matches("/"));
325 | |
...   |
339 | |         }))
340 | |     }
    | |_____^
note: ...so that the type `futures::AndThen<futures::stream::Concat2<hyper::Body>, futures::FutureResult<hyper::Response, hyper::Error>, [closure@src/service.rs:326:46: 339:10 self:&service::MyService, fqdn:std::string::String]>` will meet its required lifetime bounds
   --> src/service.rs:326:9
    |
326 | /         Box::new(put_body.concat2().and_then(move |body| {
327 | |             match serde_json::from_slice::<Record>(&body.to_vec()) {
328 | |                 Ok(record) => {
329 | |                     /* Below is the problem line. Removing it allows compilation. */
...   |
338 | |             }
339 | |         }))
    | |___________^
    = note: but, the lifetime must be valid for the static lifetime...
note: ...so that expression is assignable (expected std::boxed::Box<futures::Future<Error=hyper::Error, Item=hyper::Response> + 'static>, found std::boxed::Box<futures::Future<Error=hyper::Error, Item=hyper::Response>>)
   --> src/service.rs:326:9
    |
326 | /         Box::new(put_body.concat2().and_then(move |body| {
327 | |             match serde_json::from_slice::<Record>(&body.to_vec()) {
328 | |                 Ok(record) => {
329 | |                     /* Below is the problem line. Removing it allows compilation. */
...   |
338 | |             }
339 | |         }))
    | |___________^

The function returns a Box containing an unresolved future that has a closure that contains a reference to the service. I think the problem is that the compiler can’t know if the object pointed to by that reference ("self, the MyService instance) will live as long as the future.

However, here is a revised version of put_response() that compiles and works correctly. The key difference is using to_owned() to convert the referenced db to an owned structure whose lifetime is tied to the future:

    fn put_response(&self, uri_path: &str, put_body: Body) -> Box<Future<Item=Response, Error=hyper::Error>> {

        let fqdn = String::from(uri_path.trim_left_matches("/"));
        let db = self.db.to_owned();

        Box::new(put_body.concat2().and_then(move |body| {
            match serde_json::from_slice::<Redirect>(&body.to_vec()) {
                Ok(record) => {                    
                    let mut db_mut = db.write().unwrap();
                    db_mut.remove(&fqdn);
                    db_mut.insert(fqdn, record);
                    futures::future::ok(Response::new().with_status(StatusCode::Accepted))
                },
                Err(e) => {
                    let error = String::from(e.description());
                    futures::future::ok(Response::new().with_status(StatusCode::BadRequest)
                                        .with_body(error))
                },
            }
        }))
    }

This seems workable, but:

  1. Is the the right way (or at least a good way) to solve this? I only stumbled on the technique via the miracle of Google.

  2. Are there weird side effects, like a memory leak? The reference-counted object created by to_owned() gets moved into the closure and I’m pretty sure will be dropped when it ends and goes out of scope. But not totally sure.

Any commentary on all this is much appreciated.

Chuck


#2

Yes, I believe this is the correct way. The only thing is people will typically call clone on the Arc directly rather than calling to_owned on it (it ends up being a call to clone anyway).

There won’t be a memory leak - the closure you’re returning will end up dropping the Arc it owns (when the closure goes away) and that will decrement the refcount appropriately.


#3

Ok, great, thanks for the info.

I attempted to call clone(), during my earlier fooling around, but I’d made a mistake: I did it inside the closure block, at which point it’s too late. However, as you say, simply replacing to_owned() with clone() in the “fixed” version I posted earlier works fine.

Thanks!

Chuck