Skip to content

Detect and prevent blocking functions in async code #19

Open
@betamos

Description

@betamos

In order to write reliable and performant async code today, the user needs to be aware of which functions are blocking, and either:

  • Find an async alternative
  • Schedule the blocking operation on a separate thread pool (supported by some executors)

Determining if a function may block is non-trivial: there are no compiler errors, warnings or lints, and blocking functions are not isolated to special crates but rather unpredictably interspersed with non-blocking, async-safe synchronous code. As an example, most of std can be used in async code, but much of std::fs and std::net cannot. To make matters worse, this failure mode is notoriously hard to detect: it often compiles and runs fine when the executor is under a small load (such as in unit tests), but can cause severe application-wide bottlenecks when load is increased (such as in production).

For the time being, we tell our users that if a sync function uses IO, IPC, timers or synchronization it may block, but such advice adds mental overhead and is prone to human error. I believe it's feasible to find an automated solution to this problem, and that such a solution delivers tangible value.

Proposed goal

  • Offer a way to declare that a piece of code is blocking.
  • Detect accidental use of blocking functions in async contexts.
  • Display actionable advice for how to mitigate the problem.
  • Offer an override so that users and library authors who "know what they're doing" can suppress detection.

Possible solutions

I am not qualified to say, and I would like your thoughts! A couple of possibilities:

  • A new annotation, perhaps #[may_block] that can be applied on a per-function level.
  • An auto-trait or other type system integration (although this would be much more invasive).

Challenge 1: Blocking is an ambiguous term

Typically, blocking is either the result of a blocking syscall or an expensive compute operation. This leaves some ambiguous cases:

  • Expensive compute is a gray area. Factoring prime numbers is probably blocking, but where's the line exactly? Fortunately, very few (if any) functions in std involve expensive computation by any reasonable definition.
  • A method like std::sync::Mutex::lock is blocking only under certain circumstances. The must_not_await lint is aimed at preventing those circumstances (instead of discouraging its use altogether).
  • TcpStream::write blocks by default, but can be overridden to not block using TcpStream::set_nonblocking.
  • The println! and eprintln! macros may technically block. Since they lack async-equivalents and rarely block in practice, they are widely used and relatively harmless.
  • Work-stealing executors may have a higher (but not an infinite) tolerance for sporadic blocking work.

Ambiguity aside, there are many clear-cut cases (e.g. std::thread::sleep, std::fs::File::write and so on) which can benefit from a path forward without the need for bike-shedding.

Challenge 2: Transitivity

If fn foo() { bar() } and bar() is blocking, foo() is also blocking. In other words, blocking is transitive. If we can apply the annotation transitively, more cases can be detected. OTOH, this can be punted for later.

Challenge 3: Traits

When dynamic dispatch is used, the concrete method is erased. Should annotations be applied to trait method declaration or in the implementation?

Challenge 4: What is an "async context"?

The detection should only apply in "async contexts". Does the compiler already have a strict definition for that? Examples from the top of my head:

  • Directly in an async block or function.
  • Indirectly in an async block or function, e.g. in a closure.
  • Inside the poll method of a custom future impl, or the poll_fn macro.

Background reading

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions