Cryptic lifetime error with closure and Mutex


#1

Folks,

here I am again asking for help.

I have a resource that is contended between two thread, simple solution, wrap it into a Mutex, great!

At the end of the contention, some clean up is valuable, not strictly necessary but still, I would like to have it.
Create a custom type that wraps the Mutex above and defines a custom drop method, great!

Finally, the contention is fairly short, at least in LOC and I would like to have an ergonomic interface.
Here the idea is to have a function which takes as input the resource to be protected, and return a closure, when the closure is invoked it locks the mutex, set the resource and return the custom type mentioned above.
Here the advantage is that I now have only a simple closure to move and I can force the functions that need the resource to take as input the mentioned custom type.
The point is that I am not able to create such closure.

// here the custom type I was refering
pub struct RedisContextSet<'a>(MutexGuard<'a, Option<Context>>);

// when done reset the resource to a neutral state
impl<'a> Drop for RedisContextSet<'a> {
    fn drop(&mut self) {
        *self.0 = None
    }
}

#[derive(Clone)]
pub struct Loop {
    db: Arc<Mutex<sql::RawConnection>>,
    replication_book: ReplicationBook,
    redis_context: Arc<Mutex<Option<Context>>>, // <- the Option<Context> is the resource I am protecting
}

pub trait LoopData {
    ...    
    // below the function that return the closure
    fn set_rc_on_call<'a>(
        &self,
        ctx: Context,
    ) -> Box<Fn() -> RedisContextSet<'a>>;
}

impl LoopData for Loop {
    ...
    // here my attempt
    fn set_rc_on_call<'a>(
        &self,
        ctx: Context,
    ) -> Box<Fn() -> RedisContextSet<'a>> {
        Box::new(move || {
            let mut guard = self.redis_context.lock().unwrap();
            *guard = Some(ctx);
            RedisContextSet(guard)
        })
    }
}

This code return some problem with lifetimes, problem that I am not able to understand.

The error looks like this:

error[E0495]: cannot infer an appropriate lifetime due to conflicting requirements
   --> redisql_lib/src/redis.rs:246:18
    |
246 |           Box::new(move || {
    |  __________________^
247 | |             let mut guard = self.redis_context.lock().unwrap();
248 | |             *guard = Some(ctx);
249 | |             RedisContextSet(guard)
250 | |         })
    | |_________^
    |
note: first, the lifetime cannot outlive the anonymous lifetime #1 defined on the method body at 242:5...
   --> redisql_lib/src/redis.rs:242:5
    |
242 | /     fn set_rc_on_call<'a>(
243 | |         &self,
244 | |         ctx: Context,
245 | |     ) -> Box<Fn() -> RedisContextSet<'a>> {
...   |
250 | |         })
251 | |     }
    | |_____^
    = note: ...so that the types are compatible:
            expected &redis::Loop
               found &redis::Loop
    = note: but, the lifetime must be valid for the static lifetime...
    = note: ...so that the expression is assignable:
            expected std::boxed::Box<std::ops::Fn() -> redis::RedisContextSet<'a> + 'static>
               found std::boxed::Box<std::ops::Fn() -> redis::RedisContextSet<'_>>

error[E0495]: cannot infer an appropriate lifetime for lifetime parameter in function call due to conflicting requirements
   --> redisql_lib/src/redis.rs:247:48
    |
247 |             let mut guard = self.redis_context.lock().unwrap();
    |                                                ^^^^
    |
note: first, the lifetime cannot outlive the anonymous lifetime #1 defined on the method body at 242:5...
   --> redisql_lib/src/redis.rs:242:5
    |
242 | /     fn set_rc_on_call<'a>(
243 | |         &self,
244 | |         ctx: Context,
245 | |     ) -> Box<Fn() -> RedisContextSet<'a>> {
...   |
250 | |         })
251 | |     }
    | |_____^
note: ...so that reference does not outlive borrowed content
   --> redisql_lib/src/redis.rs:247:29
    |
247 |             let mut guard = self.redis_context.lock().unwrap();
    |                             ^^^^^^^^^^^^^^^^^^
note: but, the lifetime must be valid for the lifetime 'a as defined on the method body at 242:5...
   --> redisql_lib/src/redis.rs:242:5
    |
242 | /     fn set_rc_on_call<'a>(
243 | |         &self,
244 | |         ctx: Context,
245 | |     ) -> Box<Fn() -> RedisContextSet<'a>> {
...   |
250 | |         })
251 | |     }
    | |_____^
    = note: ...so that the expression is assignable:
            expected std::boxed::Box<std::ops::Fn() -> redis::RedisContextSet<'a> + 'static>
               found std::boxed::Box<std::ops::Fn() -> redis::RedisContextSet<'_>>

error: aborting due to 2 previous errors

The real issue is that I am reading the error, I understand every word, but I don’t understand the meaning.

It seems like the return type of the closure RedisContextSet should be static, but this is impossible.

Can some of you guys shed some lights on the whole thing?


#2

You’re getting bit by default object bounds. The error message mentions that your return type of set_rc_on_call() is Box<Fn() -> RedisContext<'a> + 'static>. As you’ve noted, this can’t work because you have a MutexGuard associated with a borrow from self. So, perhaps try this:

fn set_rc_on_call<'a>(
        &'a self,
        ctx: Context,
    ) -> Box<Fn() -> RedisContextSet<'a> + 'a> {
        Box::new(move || {
            let mut guard = self.redis_context.lock().unwrap();
            *guard = Some(ctx);
            RedisContextSet(guard)
        })
    }

There may be something else off but we can cross that bridge later :slight_smile:.

Also, maybe you can use impl trait instead of a boxed closure? You’d need similar lifetime ascription for it, so this is a tangent.

Edit: here is a playground I came up with (it’s brief - I’m on mobile) that I think demonstrates your issue; it already has my suggestion applied so it compiles. I also needed to make the closure type FnOnce so that Context can be consumed inside, and I think FnOnce makes sense for your case if I’m not mistaken.


#3

As always a huge thank you!

However, I am still unsure about the error and what is happening :frowning:

If I understand correctly in my code I was returning a RedisContextSet<'a> however this was not enough, since it was not assignable and it was required to return something like RedisContextSet<'a> + 'a_lifetime, but why the 'a_lifetime is needed? The compiler is not able to infer it by itself?

I am not sure I understand the bit: ...so that the expression is assignable why it is not assignable if I don’t explicit the lifetime of the struct?

The compiler was suggesting to use a 'static lifetime, which is not possible, so we fixed using the same lifetime of the MutexGuard.


#4

Well, you weren’t returning a RedisContext<'a> per say - you were returning a boxed closure, ie a trait object. Trait objects have something called default object bounds (more info on them); because the underlying type is erased, the compiler needs to know the scope over which the trait object is valid for.

The other issue you had, by the way, was the 'a lifetime parameter was not associated to any input lifetime - &self had its own (elided) lifetime parameter. So your signature was basically saying the 'a is whatever the caller wants, but that’s clearly a lie because you were returning a lifetime tied to self.

It’s just saying it wanted a Box<SomeTrait + 'static>, which is what the signature says, but received something different. Your closure was capturing a lifetime tied to borrow of self, so the compiler knows it’s not actually 'static.

It was suggesting 'static only because that’s what you (unknowingly) specified. I’d say it’s fixed by using the lifetime associated with borrow of self, which MutexGuard is also associated with, but in general, yes.