Skip to content
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

Merged
merged 11 commits into from
Oct 9, 2020
Merged

Conversation

LucioFranco
Copy link
Member

@LucioFranco LucioFranco commented Jul 8, 2020

Here is the initial work on a RFC to add support for a yield safe lint.

@bIgBV and I have been working on this draft and would love feedback :)

Reference #14

Rendered

RFC on rust-lang/rfcs: rust-lang/rfcs#3014

@LucioFranco LucioFranco self-assigned this Jul 8, 2020
@taiki-e
Copy link
Member

taiki-e commented Jul 8, 2020

related discussion: rust-lang/rfcs#2890

A comparison with marker trait based approach may be useful.


# Summary

Introduce a `#[yield_unsafe]` lint in the compiler that will warn the user when they unsafely hold an unsafe yield struct across a yield boundary.
Copy link
Member

@yoshuawuyts yoshuawuyts Jul 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[...] the compiler that will warn the user when they unsafely hold an unsafe yield struct across a yield boundary

It seems the term "unsafely" doesn't seem to refer to memory safety here? The terms "safe" and "unsafe" have very specific meanings in Rust. I would strongly caution against repurposing the terminology of safe and unsafe for anything unrelated to its current use.

Teaching people about unsafe Rust is pretty hard already, and it's often misunderstood. It would be bad for the language if the meaning of "unsafe" within Rust became dependent on context.

Instead I would suggest using terminology such as:

  • #[no_yield] / "can be yielded" / "cannot be yielded"
  • #[disallow_yield] / "allowed to be yielded" / "not allowed to be yielded"
  • #[deny_yield] / "allowed to be yielded" / "not allowed to be yielded"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(actually, this seems mentioned in "Unresolved questions" section)

There is a case where "safe" is used as a safe API in places where it doesn't have to do with memory safety (std::panic::UnwindSafe), but it seems "unsafe" doesn't.

  • #[deny_yield] / "allowed to be yielded" / "not allowed to be yielded"

It seems odd that the lint name warned by default is "deny_*".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a case where "safe" is used as a safe API in places where it doesn't have to do with memory safety (std::panic::UnwindSafe), but it seems "unsafe" doesn't.

I don't believe that's quite true. From the explainer in the docs of what "unwind safety" is:

It is possible, however, for logical invariants to be broken in Rust, which can end up causing behavioral bugs. Another key aspect of unwind safety in Rust is that, in the absence of unsafe code, a panic cannot lead to memory unsafety.

Which means that if a type is !UnwindSafe, it's not memory safe to unwind and the code won't compile. This seems quite different than what the #[yield_unsafe] lint introduces?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I wanted to say is that the term "safe" is used in APIs that you can never violate memory safety without other unsafe code, even if implemented incorrectly.

https://github.com/rust-lang/rfcs/blob/master/text/1236-stabilize-catch-panic.md#background-what-is-exception-safety-in-rust

Despite these methods to subvert the mitigations placed by default in Rust, a key part of exception safety in Rust is that safe code can never lead to memory unsafety, regardless of whether it panics or not. Memory unsafety triggered as part of a panic can always be traced back to an unsafe block.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another point on naming: "yield" isn't a documented term in Rust today. While I would very much like to use terminology that's future-proof for when generators stabilize, it seems like it would be quite confusing in the meantime.

Maybe we could use "async" terminology for now, and simply add "yield" terminology as a synonym in the future. The RFC should discuss future options and potential drawbacks, if any.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I am not sold on unsafe it was just the best way I could think of explaining it to people that were familiar with async concepts. I would love to hear more proposals I am very open to naming.

@tmandry yeah, that is a really good point. We should probably use await then? But that also limits us when we do get generators out (which idk when that will happen) so not a huge deal we can duplicate the macro.

  • #[deny_hold_await]
  • #[disallow_across_await] this seems promising and similar to what @yoshuawuyts suggested.

@taiki-e
Copy link
Member

taiki-e commented Jul 8, 2020

Rendered
(You can also see it from "Display the rich diff" button in "Files changed")

- This is probab


# Rationale and alternatives
Copy link
Member

@yoshuawuyts yoshuawuyts Jul 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious why it was chosen to use an attribute instead of a trait, following std::panic::UnwindSafe.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @nikomatsakis wanted us to also take a look at what is going on with MustUse. I think we still want this to be some sort of attribute similar to must_use but we could also support a trait style too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would want to parallel #[must_use], I think? But I've shifted my opinion and am now open to using a trait for both I think.

# Rationale and alternatives


# Prior art
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A clippy lint was added: rust-lang/rust-clippy#5439

@LucioFranco
Copy link
Member Author

Thanks for the feedback everyone <3. Will update the draft sometime next week and apply all the suggestions. Thanks!

@LucioFranco
Copy link
Member Author

Do people have any opinions on calling this #[deny_await]? It feels short enough but conveys the correct message.

@LucioFranco LucioFranco changed the title Draft RFC to add a yield safe lint Draft RFC to add a deny await lint Jul 14, 2020
@tmandry
Copy link
Member

tmandry commented Jul 16, 2020

Do people have any opinions on calling this #[deny_await]? It feels short enough but conveys the correct message.

deny is already an attribute that takes a parameter, which would make it confusing to explain when talking aloud. I think someone pointed out above that you might be left with things like #[allow(deny_await)], which isn't the end of the world but can contribute to the confusion.

Another point of confusion is that it sounds like the thing you're trying to avoid is awaiting the object itself, instead of holding the object across an await point.

Still, I haven't seen a name that I really like yet, and this is short enough and gets the point across pretty well.

Other ideas:

  • #[must_not_await] (to go along with #[must_use])
  • #[lint_on_await]
  • #[never_suspend]

@LucioFranco
Copy link
Member Author

I think #[must_not_await] works extremely well!

@LucioFranco LucioFranco changed the title Draft RFC to add a deny await lint Draft RFC to add a must_not_await lint Jul 17, 2020
@betamos
Copy link

betamos commented Jul 20, 2020

I have been thinking of a related lint for blocking methods. With blocking, I mean both when yielding the current thread (primarily in sync IO & timers) and compute-heavy functions (e.g. generating prime numbers). Accidentally calling a blocking function in async can cause the executor to hickup for as long as the call takes, degrading the performance globally.

Blocking methods are not annotated, and rarely documented. Some multithreaded executors can work around this by work-stealing, but that cannot generally be assumed. There are no workarounds for singlethreaded executors. Hence, avoiding blocking code today requires a vigilant user in the loop. I believe that an annotation on blocking functions would enable Clippy to provide an important and actionable lint.

The relationship to this RFC is that sync mutices have blocking API methods. The lock() method is blocking (although the try_lock() method is not). As such, calling lock() in an async scope is, imo, a red/yellow flag by itself. The fact that lock() happens to be non-blocking if you never hold the guard across an await is AFAIK entirely specific to mutex and its siblings. A general annotation+lint for blocking APIs could alert users to switch to async APIs. This principle would apply to all synchronous IO, synchronization primitives, timers and compute-heavy operations.

I still think the proposed lint is useful, due to the deadlock risk, but there are legitimate use-cases where a mutex must be held across an await point. If we could only afford one annotation, I'd pick one that is more general. Apologies for derailing the conversation, but these seem related. Happy to open a new issue.

@LucioFranco
Copy link
Member Author

@betamos could you expand on this comment?

but there are legitimate use-cases where a mutex must be held across an await point.

I have yet to see a use-case for this, beyond "you should just use an async awaire one".

@LucioFranco
Copy link
Member Author

Even async-aware mutexes can deadlock if you hold their guard across an await, no?

It can deadlock in the normal manners that a std::sync::Mutex can deadlock. Rust doesn't provide any protection against that anyways so for me that is out of scope. The key here is that to acquire a new lock will poll and yield anyways if the async lock guard is held across a yield boundary. This is the problem that a sync mutex will deadlock in weird ways because of the implementation details of async/futures/runtimes.

locking a non-async-aware mutex anywhere in an async context

I'm not convinced this is a red flag. Short critical sections and locking on that in a low contention environment is totally fine. Its something that just needs to be measured. I don't think we've totally explored all the possibilities for it either. But for example, in a very heavy read vs write work load a regular RwLock will do just fine.

The goal of this lint in general is to merely just inform the user that something weird may happen because the compiler transformed your code in a way you may not have expected. It's not here to protect a user from every deadlock.

@betamos
Copy link

betamos commented Jul 21, 2020

It can deadlock in the normal manners that a std::sync::Mutex can deadlock. Rust doesn't provide any protection against that anyways so for me that is out of scope.

+1. If a mutex is never held across an await, it will not deadlock. This is true for either type of mutex.

If you want to model something like transactionality, you may need to hold it across an await-point. This can be sound and used correctly, but puts more responsibility on the programmer. I like the current RFC since it shoos most users away from it when introduced by accident.

I'm not convinced this is a red flag. Short critical sections and locking on that in a low contention environment is totally fine.

std::Mutex::lock sometimes translates to yielding the current thread. Using an async lock from the get-go is the preferable option IMO, so if holding-across-await is needed later, it doesn't prompt a migration to an async lock. (As an example, a sync method that uses a sync mutex must be changed to async upon migration, which is a breaking change).

But for example, in a very heavy read vs write work load a regular RwLock will do just fine.

Why not use an async-aware RWLock? I can see the lack of library support but is there anything else?

To sum up: I'm not suggesting you must never call potentially blocking code in async. Of course you can override if you know what you're doing ™. It seems easier to teach "never call blocking methods" than "this particular method is blocking under certain circumstances, so avoid those circumstances".

@LucioFranco
Copy link
Member Author

std::Mutex::lock sometimes translates to yielding the current thread. Using an async lock from the get-go is the preferable option IMO,

Ah yeah, so this is where I think we are disconnected. I generally suggest if your critical section is small enough and doesn't need to last across a yield then always use a sync mutex. This is because the cost of creating the waker is a bit more expensive. (There may be opportunity here to investigate and make it faster). We have seen in benchmarks that the async versions will be slower. I don't think the yielding from the sync mutex creates that much of a problem though. I thought the yield is mostly a hint to the scheduler. That said, I am not 100% sure on that impact but is something worth measuring.

Why not use an async-aware RWLock? I can see the lack of library support but is there anything else?

Same issue as I said above it costs much more than a regular one.

To sum up: I'm not suggesting you must never call potentially blocking code in async. Of course you can override if you know what you're doing ™. It seems easier to teach "never call blocking methods" than "this particular method is blocking under certain circumstances, so avoid those circumstances".

Agreed, to me its mostly a case of poor docs. But we just published a new tokio site that has some docs on shared state. https://tokio.rs/tokio/tutorial/shared-state Hopefully this is a bit more explicit and easier for the user to understand the why. I would love to write some sort of blog on this with some actual numbers though instead of (my own) hand waving 😄

@tmandry
Copy link
Member

tmandry commented Jul 21, 2020

I don't think the yielding from the sync mutex creates that much of a problem though. I thought the yield is mostly a hint to the scheduler.

I think @betamos is referring to the contended case where your thread sleeps until the mutex is unlocked again. In that case the cost is high (and all progress on async tasks stops on your thread, of course).

This is because the cost of creating the waker is a bit more expensive. (There may be opportunity here to investigate and make it faster).

I agree there probably is; you should only need to create the waker in a contended case, in which case it should be less expensive than a futex.

@LucioFranco
Copy link
Member Author

I think @betamos is referring to the contended case where your thread sleeps until the mutex is unlocked again. In that case the cost is high (and all progress on async tasks stops on your thread, of course).

Right, if you have high contention that is true but also work-stealing can help in this case. There is some balance here that needs to be figured out but there are a lot of variables. So not sure exactly how we can communicate that.

I agree there probably is; you should only need to create the waker in a contended case, in which case it should be less expensive than a futex.

Would be good to benchmark, but agreed we could probably do better as well.

@bIgBV
Copy link
Contributor

bIgBV commented Jul 21, 2020

I don't think the yielding from the sync mutex creates that much of a problem though. I thought the yield is mostly a hint to the scheduler.

I think @betamos is referring to the contended case where your thread sleeps until the mutex is unlocked again. In that case the cost is high (and all progress on async tasks stops on your thread, of course).

Imo the problem of when it is safe to use a blocking lock vs a non-blocking lock is too complex to be solved by a single RFC or feature. I think we should focus on a smaller subset of the problem. This RFC initially came to be because of the unusual/unexpected failure modes that one might encounter when holding a blocking lock across await points. This is mostly due to the non-obvious way in which the rust compiler transforms async await code, which most newcomers to the async await world of rust would most likely not have a good understanding of. And they shouldn't need to!

The goal is to highlight the problem and steer the user towards a solution which they are most likely looking for. In this sense, I think a attribute which lints such types would help.

As far as the bigger problem on blocking code inside async code goes, I think that's a more general problem which an attribute lint can't necessarily solve. I think we need to tackle that separately.

@betamos
Copy link

betamos commented Jul 21, 2020

We have seen in benchmarks that the async versions will be slower.

Thanks for providing that context. As I side note, I'd be very interested in peeking at those.

This is because the cost of creating the waker is a bit more expensive.

Nod, thanks for confirming my suspicions. The waker overhead seems like an unfortunate fact-of-life for multi-threaded executors, and is concerning on a much broader scale. Anyway, I'm rambling off topic.

There may be opportunity here to investigate and make it faster

Yes, it seems like in the uncontended case there would be no reason to register/clone a waker? I don't know of anything inherit about an async mutex that demands a perf hit.

As far as the bigger problem on blocking code inside async code goes, I think that's a more general problem which an attribute lint can't necessarily solve.

What's the complexity? I'd classify blocking as:

  • The function may yield the thread or
  • The function is long-running, for some epsilon (subject to bike-shedding).

I think we need to tackle that separately.

That would be fine. Don't let this block. I just wanted to shed some light on the problem space.

@LucioFranco LucioFranco marked this pull request as ready for review August 28, 2020 22:55

# 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. Some examples of these side effects are holding a `MutexGuard` across an await bound in a single threaded runtime. In this case the resulting generated future will resolve to `!Send` but could still hold the lock when the future yields back to the executor. This opens up for the possibility of causing a deadlock since the future holding onto the lock did not relinquish it back before it yielded control. This can become even more problematic for futures that run on single-threaded runtimes (`!Send`) where holding a local after a yield will result in a deadlock.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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. Some examples of these side effects are holding a `MutexGuard` across an await bound in a single threaded runtime. In this case the resulting generated future will resolve to `!Send` but could still hold the lock when the future yields back to the executor. This opens up for the possibility of causing a deadlock since the future holding onto the lock did not relinquish it back before it yielded control. This can become even more problematic for futures that run on single-threaded runtimes (`!Send`) where holding a local after a yield will result in a deadlock.
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`.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want to hint in this section that other kinds of RAII guards may want this (which you talk about below). Someone who only reads to here might think this doesn't have enough use cases for a general mechanism.

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]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like must_use we should allow a custom error message to be provided.


```rust
#[must_not_await]
struct MyStruct {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can you un-indent these code blocks?

struct MyStruct {}
```

This struct if held across an await boundary would cause a warning:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This struct if held across an await boundary would cause a warning:
This struct if held across an await boundary would cause a deny-by-default warning:

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deny-by-default is a new term for me. Do you have any links to any documentation about this type of behavior?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://doc.rust-lang.org/rustc/lints/listing/deny-by-default.html

They are basically errors that you can disable with #[allow].

- [`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 to the user about unintended side-effects of using certain types across an await boundary.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This will be a best effort lint to signal to the user about unintended side-effects of using certain types across an await boundary.
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

Going throuogh the prior are we see two systems currently which provide simailar/semantically similar behavior:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems more like it belongs in either the motivation or the rationale and alternatives section, to me at least.

rfc-drafts/must_not_await_lint.md Show resolved Hide resolved
`#[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.


## High level design overview
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there's precedent for this that I don't know about, but I don't feel that implementation notes like this belong in an RFC.

- Reference link on how mir transfroms async fn https://tmandry.gitlab.io/blog/posts/optimizing-await-2/

# Drawbacks
- There is a possibility it can produce a false positive warning and it could get noisy. We likely want to allow overriding via some sort of module level `allow` attribute.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we do (and it should work at the item level as well), leveraging the same mechanism as other deny-by-default lints.


# Future possibilities

- Propagate the lint in nested structs/enums. Similar to the use case for the `must_use` attribute. These likely should be solved together.
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do that it'll be inconsistent with #[must_use]. If that's the end goal we want we should explain why they should be different, or discuss the possibility of changing #[must_use] in the future.

Strawman: This is about what types are safe to include (transitively or not) in a Future object. #[must_use] is more about how individual values are handled in the code. The state of things today is that new types already have to think about #[must_use], but rarely do they think about yielding and async/await, and we shouldn't expect them to.

Copy link
Contributor

Choose a reason for hiding this comment

The 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).

Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member

@tmandry tmandry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good, I think this is almost ready to merge.


The compiler might output something along the lines of:

TODO: Write a better error message.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can show your custom error message here.

The generic default could be as straightforward as "MyStruct should not be held across an await"

[trait declaration]: https://doc.rust-lang.org/reference/items/traits.html
[impl trait]: https://doc.rust-lang.org/reference/types/impl-trait.html

### Auto trait vs attribute
Copy link
Member

@tmandry tmandry Sep 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this belongs in the reference section. Is this an unresolved question, or does it belong in the rational section?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This answers the question of why we chose to go the attribute route instead of creating an auto-trait. I think this should be in the rationale section.


# Future possibilities

- Propagate the lint in nested structs/enums. Similar to the use case for the `must_use` attribute. These likely should be solved together.
Copy link
Member

Choose a reason for hiding this comment

The 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?

rfc-drafts/must_not_await_lint.md Show resolved Hide resolved
@LucioFranco
Copy link
Member Author

Looks like most questions have been resolved, I'd like to give a call for last comments and see if we can get this merged and opened as an actual RFC 😄

Copy link
Member

@tmandry tmandry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a few comments, otherwise looks good to me.


- 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:
Copy link
Member

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.


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.
Copy link
Member

Choose a reason for hiding this comment

The 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 await point" is the best we can do for now. If we want to get more formal than "value".. "value expression, temporary, or variable"?

}
```

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.
Copy link
Member

Choose a reason for hiding this comment

The 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 #[must_use], though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, there's two aspects to this

1.) Being consistent with #[must_use]
2.) Being able to mark the result of any expression as #[must_not_await] and I think this was the reason why the #[must_use] attribute works this way in the first place.

}
```

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto


Going through the prior are we see two systems currently which provide simailar/semantically similar behavior:

## Clippy `#[await_holding_lock]` lint
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
## Clippy `#[await_holding_lock]` lint
## Clippy `await_holding_lock` lint

That's the name of the lint but I don't think it's an attribute

@bIgBV
Copy link
Contributor

bIgBV commented Oct 9, 2020

@tmandry I've addressed your comments and as discussed on zuplip, I'm merging this PR. Thanks again for all your help and @nikomatsakis as well.

PS: I'll be opening an RFC on the RFCs repo over the weekend.

@bIgBV bIgBV merged commit 90b3668 into rust-lang:master Oct 9, 2020
@jonhoo
Copy link
Contributor

jonhoo commented Nov 9, 2020

@bIgBV Did you open an RFC PR for this? I couldn't find it with a quick search. Would probably be useful to link to it here (maybe in the first post @LucioFranco?)

@LucioFranco
Copy link
Member Author

@jonhoo I think somethings have come up for both @bIgBV and I, but we should be opening this soon. Sorry for the delay, life has kinda been crazy the last few weeks :)

@jimblandy
Copy link

Probably RefMut should be included in this class. Holding one across an await will cause the next attempt to acquire any sort of reference to the RefCell to panic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants