-
Notifications
You must be signed in to change notification settings - Fork 88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft RFC to add a must_not_await
lint
#16
Changes from 9 commits
6d8aaf1
dac6980
031d93a
c027fb2
50ba6a3
7e0787e
7ce3b3e
0d51148
6b423e0
9b25a4a
82cb98b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,149 @@ | ||||||
# RFC: Must not await lint | ||||||
|
||||||
# Summary | ||||||
|
||||||
Introduce a `#[must_not_await]` lint in the compiler that will warn the user when they are incorrectly holding a struct across an await boundary. | ||||||
|
||||||
# Motivation | ||||||
|
||||||
Enable users to fearlessly write concurrent async code without the need to understand the internals of runtimes and how their code will be affected. The goal is to provide a best effort warning that will let the user know of a possible side effect that is not visible by reading the code right away. | ||||||
|
||||||
One example of these side effects is holding a `MutexGuard` across an await bound. This opens up the possibility of causing a deadlock since the future holding onto the lock did not relinquish it back before it yielded control. This is a problem for futures that run on single-threaded runtimes (`!Send`) where holding a local after a yield will result in a deadlock. Even on multi-threaded runtimes, it would be nice to provide a custom error message that explains why the user doesn't want to do this instead of only a generic message about their future not being `Send`. Any other kind of RAII guard which depends on behavior similar to that of a `MutexGuard` will have the same issue. | ||||||
|
||||||
The big reason for including a lint like this is because under the hood the compiler will automatically transform async fn into a state machine which can store locals. This process is invisible to users and will produce code that is different than what is in the actual rust file. Due to this it is important to inform users that their code may not do what they expect. | ||||||
|
||||||
# Guide-level explanation | ||||||
|
||||||
Provide a lint that can be attached to structs to let the compiler know that this struct can not be held across an await boundary. | ||||||
|
||||||
```rust | ||||||
#[must_not_await = "Your error message here"] | ||||||
struct MyStruct {} | ||||||
``` | ||||||
|
||||||
This struct if held across an await boundary would cause a deny-by-default warning: | ||||||
|
||||||
```rust | ||||||
async fn foo() { | ||||||
let my_struct = MyStruct {}; | ||||||
my_async_op.await; | ||||||
println!("{:?}", my_struct); | ||||||
} | ||||||
``` | ||||||
|
||||||
The compiler might output something along the lines of: | ||||||
|
||||||
``` | ||||||
warning: `MyStruct` should not be held across an await point. | ||||||
``` | ||||||
|
||||||
Example use cases for this lint: | ||||||
|
||||||
- `MutexGuard` holding this across a yield boundary in a single threaded executor could cause deadlocks. In a multi-threaded runtime the resulting future would become `!Send` which will stop the user from spawning this future and causing issues. But in a single threaded runtime which accepts `!Send` futures deadlocks could happen. | ||||||
|
||||||
- The same applies to other such synchronization primitives such as locks from `parking-lot`. | ||||||
|
||||||
- `tracing::Span` has the ability to enter the span via the `tracing::span::Entered` guard. While entering a span is totally normal, during an async fn the span only needs to be entered once before the `.await` call, which might potentially yield the execution. | ||||||
|
||||||
- Any RAII guard might possibly create unintended behavior if held across an await boundary. | ||||||
|
||||||
This lint will enable the compiler to warn the user that the generated MIR could produce unforeseen side effects. Some examples of this are: | ||||||
|
||||||
- [`std::sync::MutexGuard`](https://doc.rust-lang.org/std/sync/struct.MutexGuard.html) | ||||||
- [`tracing::span::Entered`](https://docs.rs/tracing/0.1.15/tracing/span/struct.Entered.html) | ||||||
|
||||||
This will be a best effort lint to signal the user about unintended side-effects of using certain types across an await boundary. | ||||||
|
||||||
# Reference-level explanation | ||||||
tmandry marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
The `must_not_await` attribute is used to issue a diagnostic warning when a value is not "used". It can be applied to user-defined composite types (structs, enums and unions), functions and traits. | ||||||
|
||||||
The `must_not_await` attribute may include a message by using the [`MetaNameValueStr`] syntax such as `#[must_not_await = "example message"]`. The message will be given alongside the warning. | ||||||
|
||||||
When used on a user-defined composite type, if the [expression] of an [expression statement] has this type and is used across an await point, then this lint is violated. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think expressions or expression statements are necessarily right here – a let binding that's in scope during an await could cause the same behavior. A temporary could also be held across an await point in the return expression of a function, for instance. The existing reference doesn't give a whole lot of scaffolding to work with. Maybe "if a value exists during an |
||||||
|
||||||
|
||||||
```rust | ||||||
#[must_not_await = "Your error message here"] | ||||||
struct MyStruct {} | ||||||
|
||||||
async fn foo() { | ||||||
let my_struct = MyStruct {}; | ||||||
my_async_op.await; | ||||||
println!("{:?}", my_struct); | ||||||
} | ||||||
``` | ||||||
|
||||||
When used on a function, if the [expression] of an [expression statement] is a [call expression] to that function, and the expression is held across an await point, this lint is violated. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also find the expression wording here a bit confusing. I think we can do something similar to the above here, and reference "the value returned by the function." Now that I think about it, it isn't obvious to me that we want to allow this attribute on functions. Are there any use cases where we'd want it on the function but not the type? I realize this is consistent with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, there's two aspects to this 1.) Being consistent with |
||||||
|
||||||
```rust | ||||||
#[must_not_await] | ||||||
fn foo() -> i32 { 5i32 } | ||||||
|
||||||
async fn foo() { | ||||||
let bar = foo(); | ||||||
my_async_op.await; | ||||||
println!("{:?}", bar); | ||||||
} | ||||||
``` | ||||||
|
||||||
When used on a [trait declaration], a [call expression] of an [expression statement] to a function that returns an [impl trait] of that trait and if the value is held across an await point, the lint is violated. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto |
||||||
|
||||||
```rust | ||||||
trait Trait { | ||||||
#[must_not_await] | ||||||
fn foo(&self) -> i32; | ||||||
} | ||||||
|
||||||
impl Trait for i32 { | ||||||
fn foo(&self) -> i32 { 0i32 } | ||||||
} | ||||||
|
||||||
async fn foo() { | ||||||
let bar = 5i32.foo(); | ||||||
my_async_op.await; | ||||||
println!("{:?}", bar); | ||||||
} | ||||||
``` | ||||||
|
||||||
When used on a function in a trait implementation, the attribute does nothing. | ||||||
|
||||||
[`MetaNameValueStr`]: https://doc.rust-lang.org/reference/attributes.html#meta-item-attribute-syntax | ||||||
[expression]: https://doc.rust-lang.org/reference/expressions.html | ||||||
[expression statement]: https://doc.rust-lang.org/reference/statements.html#expression-statements | ||||||
[call expression]: https://doc.rust-lang.org/reference/expressions/call-expr.html | ||||||
[trait declaration]: https://doc.rust-lang.org/reference/items/traits.html | ||||||
[impl trait]: https://doc.rust-lang.org/reference/types/impl-trait.html | ||||||
|
||||||
|
||||||
# Drawbacks | ||||||
- There is a possibility it can produce a false positive warning and it could get noisy. But using the `allow` attribute would work similar to other `deny-by-default` lints. | ||||||
|
||||||
# Rationale and alternatives | ||||||
|
||||||
Going through the prior are we see two systems currently which provide simailar/semantically similar behavior: | ||||||
|
||||||
## Clippy `#[await_holding_lock]` lint | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
That's the name of the lint but I don't think it's an attribute |
||||||
This lint goes through all types in `generator_interior_types` looking for `MutexGuard`, `RwLockReadGuard` and `RwLockWriteGuard`. While this is a first great step, we think that this can be further extended to handle not only the hardcoded lock guards, but any type which is should not be held across an await point. By marking a type as `#[must_not_await]` we can warn when any arbitrary type is being held across an await boundary. An additional benefit to this approach is that this behaviour can be extended to any type which holds a `#[must_not_await]` type inside of it. | ||||||
|
||||||
## `#[must_use]` attribute | ||||||
The `#[must_use]` attribute ensures that if a type or the result of a function is not used, a warning is displayed. This ensures that the user is notified about the importance of said value. Currently the attribute does not automatically get applied to any type which contains a type declared as `#[must_use]`, but the implementation for both `#[must_not_await]` and `#[must_use]` should be similar in their behavior. | ||||||
|
||||||
### Auto trait vs attribute | ||||||
`#[must_use]` is implemented as an attribute, and from prior art and [other literature][linear-types], we can gather that the decision was made due to the complexity of implementing true linear types in Rust. [`std::panic::UnwindSafe`][UnwindSafe] on the other hand is implemented as a marker trait with structural composition. | ||||||
|
||||||
[linear-types]: https://gankra.github.io/blah/linear-rust/ | ||||||
[UnwindSafe]: https://doc.rust-lang.org/std/panic/trait.UnwindSafe.html | ||||||
|
||||||
|
||||||
# Prior art | ||||||
|
||||||
* [Clippy lint for holding locks across await points](https://github.com/rust-lang/rust-clippy/pull/5439) | ||||||
* [Must use for functions](https://github.com/iopq/rfcs/blob/f4b68532206f0a3e0664877841b407ab1302c79a/text/1940-must-use-functions.md) | ||||||
* Reference link on how mir transfroms async fn https://tmandry.gitlab.io/blog/posts/optimizing-await-2/ | ||||||
|
||||||
# Unresolved questions | ||||||
|
||||||
- Propagate the lint in nested structs/enums. Similar to the use case for the `must_use` attribute. These likely should be solved together. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should answer this question before stabilizing. If we add this afterwards it'll create unnecessary breakage. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should include this in this change. In my opinion, this would be the best experience for the user of this attribute. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we do that it'll be inconsistent with Strawman: This is about what types are safe to include (transitively or not) in a Future object. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was brought up before by @nikomatsakis in an earlier comment. There was a PR which introduced the behavior we are speaking of for #[must_use] but was closed because of crater breakages. That particular conversation was biased towards having the same behavior across both attributes. It would certainly increase the scope of this project especially so if it's a breaking change we might have to ship it as a part of the 2021 edition (if it's happening). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe for now we can add an "Unresolved questions" section and add this to it? |
||||||
|
||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: The MIR is more of an implementation detail. It's just the defined semantics of async functions, namely the fact that they suspend execution.