dtolnay / async-trait

Type erasure for async trait methods
Apache License 2.0
1.81k stars 84 forks source link

Higher-ranked lifetime error when using streams #212

Closed Tuetuopay closed 2 years ago

Tuetuopay commented 2 years ago

I stumbled on some weird borrow checker issue when using streams in code called by a function implementation of an async trait (my use case being Tonic, where the server is defined as such). Interestingly, the issue disappears when relaxing the thread safety requirement of async trait (with ?Send passed to the proc macro).

Here you can find a playground with the faulty code: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=77493d971e9227e5dcd6bcf75f286335. I did add a few variations of the faulty code involving streams, end there are a few takeaways:

The actual error is pretty cryptic:

error: higher-ranked lifetime error
   --> src/main.rs:98:49
    |
98  |       async fn fn_trait_send(&self) -> Result<()> {
    |  _________________________________________________^
99  | |         let count = count_resources(&[Uuid::new_v4(), Uuid::new_v4()]).await?;
100 | |         println!("{count}");
101 | |         Ok(())
102 | |     }
    | |_____^
    |
    = note: could not prove for<'r, 's, 't0, 't1> Pin<Box<impl futures::Future<Output = Result<(), anyhow::Error>>>>: CoerceUnsized<Pin<Box<(dyn futures::Future<Output = Result<(), anyhow::Error>> + std::marker::Send + 't1)>>>

Interestingly, the above error is using Rust 1.63 with the new improved error messages. Yay borrow checker! If using 1.62, we get something more verbose that may give more info.

``` error[E0308]: mismatched types --> src/main.rs:78:49 | 39 | .flat_map(|region| iter(groups).map(move |group| (region, group))) | ---------------------------- | | | the expected closure | the found closure ... 78 | async fn fn_trait_send(&self) -> Result<()> { | _________________________________________________^ 79 | | let count = count_resources(&[Uuid::new_v4(), Uuid::new_v4()]).await?; 80 | | println!("{count}"); 81 | | Ok(()) 82 | | } | |_____^ one type is more general than the other | = note: expected struct `futures::stream::Map>, [closure@src/main.rs:39:45: 39:73]>` found struct `futures::stream::Map>, [closure@src/main.rs:39:45: 39:73]>` error[E0308]: mismatched types --> src/main.rs:78:49 | 78 | async fn fn_trait_send(&self) -> Result<()> { | _________________________________________________^ 79 | | let count = count_resources(&[Uuid::new_v4(), Uuid::new_v4()]).await?; 80 | | println!("{count}"); 81 | | Ok(()) 82 | | } | |_____^ one type is more general than the other | = note: expected tuple `(&String, &Uuid)` found tuple `(&String, &Uuid)` error: implementation of `FnOnce` is not general enough --> src/main.rs:78:49 | 78 | async fn fn_trait_send(&self) -> Result<()> { | _________________________________________________^ 79 | | let count = count_resources(&[Uuid::new_v4(), Uuid::new_v4()]).await?; 80 | | println!("{count}"); 81 | | Ok(()) 82 | | } | |_____^ implementation of `FnOnce` is not general enough | = note: closure with signature `fn(&'0 String) -> futures::stream::Map>, [closure@src/main.rs:39:45: 39:73]>` must implement `FnOnce<(&String,)>`, for any lifetime `'0`... = note: ...but it actually implements `FnOnce<(&String,)>` error: implementation of `FnOnce` is not general enough --> src/main.rs:78:49 | 78 | async fn fn_trait_send(&self) -> Result<()> { | _________________________________________________^ 79 | | let count = count_resources(&[Uuid::new_v4(), Uuid::new_v4()]).await?; 80 | | println!("{count}"); 81 | | Ok(()) 82 | | } | |_____^ implementation of `FnOnce` is not general enough | = note: closure with signature `fn(&'0 Uuid) -> (&String, &Uuid)` must implement `FnOnce<(&Uuid,)>`, for any lifetime `'0`... = note: ...but it actually implements `FnOnce<(&Uuid,)>` For more information about this error, try `rustc --explain E0308`. error: could not compile `hkl-issue` due to 4 previous errors ```

Thanks!

dtolnay commented 2 years ago

I don't think this has anything to do with async-trait. A future is not Send and the error is bad. The only relevance of async-trait in your code is that's where you get the Send bound.

Here is a smaller repro:

use futures::StreamExt;

fn require_send<T: Send>(_: T) {}

fn main() {
    require_send(async {
        futures::stream::empty()
            .map(std::future::ready::<&()>)
            .buffered(1)
            .next()
            .await;
    });
}
error: higher-ranked lifetime error
   --> src/main.rs:242:5
    |
242 | /     require_send(async {
243 | |         futures::stream::empty()
244 | |             .map(std::future::ready::<&()>)
245 | |             .buffered(1)
246 | |             .next()
247 | |             .await;
248 | |     });
    | |______^
    |
    = note: could not prove `impl futures::Future<Output = ()>: std::marker::Send`
Tuetuopay commented 2 years ago

Wow thanks for the smaller repro!

Do you have any pointers on how to fix this? And if this is a bug somewhere? I guess the root cause is futures's futures made from stream don't implement Send, while I think they should if the enclosed futures do.

Should I raise an issue to futures (for them to be Send) / rustc (to improve the error message)?