Async_trait default impl vs concrete impl lifetimes

I am having trouble understanding the exact lifetime issues that are being reported by the compiler here. I have the following playground with the following excerpt that is of interest:

#[async_trait::async_trait]
pub trait EventStore<A>: Clone + Send + EventRepository
where
    A: Aggregate,
{async fn get_aggregates(                                                              
    &self,                                                                            
    stream_ids: &[StreamId]                                                           
) -> Result<Vec<A>, EventSourceError<A::DomainError>> {                               
    let mut threads: JoinSet<Result<A, Box<dyn std::error::Error + Send + Sync>>> =   
        JoinSet::new();                                                               
    let mut aggs: Vec<A> = Vec::with_capacity(stream_ids.len());                      
    for stream_id in stream_ids {                                                     
        let event_store = self.clone();                                               
        let stream_id = stream_id.clone();                                            
        threads.spawn(async move {                                                    
            event_store.get_aggregate(&stream_id).await.map_err(|x| {                 
                let e: Box<dyn std::error::Error + Send + Sync> = x.into();           
                e                                                                     
            })                                                                        
        });                                                                           
    }                                                                                 
                                                                                      
    while let Some(res) = threads.join_next().await {                                 
        let result = res.map_err(|err| {                                              
            EventSourceError::EventStoreError(                                        
                err.into(),                                                           
                "Couldn't join threads for eventstore reading",                       
            )                                                                         
        })?;                                                                          
        match result {                                                                
            Ok(paper) => aggs.push(paper),                                            
            Err(err) => return Err(EventSourceError::EventStoreError(err.into(), "")),
        };                                                                            
    }                                                                                 
    Ok(aggs)                                                                          
} 

The problem here is I am getting a bunch of lifetime errors ("&self,
| - the parameter type A must be valid for the lifetime 'life0 as defined here...") because of the spawned threads.

I am in the middle of trying to create this trait with default implementations as otherwise the concrete implementations are basically always the same. I do not get these compiler issues when removing the default impl and instead manually implementing it.
I suspect it is something to do with the fact that self in the trait call is similar to dyn EventStore<A> and cloning is unsized or something but I cant quite get there in understanding the issue.
Is this something that cant have a default impl or otherwise what do I need to understand to get it?

This has nothing to do with unsizedness. This is a lifetime problem. And the compiler tells you exactly what it is. In order to be usable in a thread, a type must be 'static, because a thread can run for an arbitrary, unbounded time compared to the validity of the spawning scope.

But an arbitrary type parameter (Self and A) isn't known (and can't assumed) to be 'static, because it can stand in for any type at all.

If you add 'static as a bound to both of the traits, then it compiles.

You weren't getting an error with your concrete-type impls because the compiler does know the lifetime bounds of concrete types, since they are, well, concrete.

(I also formatted the code and removed the useless PartialEq + Eq bound because it's just Eq, since Eq: PartialEq.)

2 Likes

I couldn't understand why I would be able to assign the aggregate and processor traits as 'static. After looking at your solution and taking another read I believe that I was thinking of &'static and T: 'static as synonymous which I can now see is wrong.
If I understand it, any T can be 'static so long as none of its internal fields are non-static. I think this means anything that is owned or is a &'static reference, is that correct?

They aren't.

'static is a lifetime bound, which means it's a requirement on the type of the form: "values of this type are valid to hold for the static lifetime". It's merely a requirement of being valid if/when an instance of the type is borrowed.

&'static T means a concrete type, which is a reference that actually borrows its referent T for the 'static lifetime. It implies the requirement T: 'static, otherwise you'd have a dangling pointer, but the two are not equivalent.

Yes, 'static values are sometimes called "owned" (with some degree of imprecision, as you noted).

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.