Lifecycle Trait

Summary

Idioms that that involve RAII guards to mark scopes do not always translate neatly from synchronous to asynchronous code. The most broad category are types which make use of drop guards which, in turn, expect stack-shaped holes. Consider the following use-cases.
  • An asynchronous mutex which releases the locked data whenever the future isn’t executing.
  • Task locals in select! might be another.
  • The tracing library, which makes use of RAII guards to track which unit of work is currently executing. Note that due to the author’s experience with tracing's internals, this RFC would be discussed in terms of tracing, but tracing shouldn’t be only motivating example. This RFC would have the greatest impact on debugging/profiling/instrumentation tooling.

Motivation

In the tracing library, spans are used to track a unit of work in code. Spans are defined axiomatically—spans are whatever the user considers to be a unit of work in their application. For the purposes of this proposal, spans have a few relevant lifecycle stages:
  • create
  • enter
  • exit
  • close
While create and close can only be called once for a given a span—whenever a span is created or completed—enter and exit can be called multiple times in the lifecycle of span. In the context of a span decorating a future, a span would be: 
  • entered whenever the executor polls the future.
  • exited whenever the future returns Poll::Pending.
tracing needs to track span entrance/exit events due to its data collection mechanism. If spans are not closed when a future is yielded, tracing will record another span’s events/data as part of the incorrect span, leading to incorrect and confusing diagnostics. Today, tracing is able to work around this limitation using the Instrument extension trait in tracing-futures and the #[instrumented] attribute macro, but this approach requires the user to explicitly opt-in to this behavior.

In this proposal, we suggest a new trait called  Lifecycle (name subject to change), which enables types that implement the Lifecycle trait to be notified of a wake/yield whenever the implementing type is a local within a future or generator that has been woken or yielded. The Lifecycle trait would be defined as:

trait Lifecycle {
   fn on_resume(self: Pin<&mut Self>, cx: &mut Context) {}
   fn on_suspend(self: Pin<&mut Self>, cx: &mut Context) {}
}

This trait would be implemented on tracing::span::Entered as:

impl Lifecycle for tracing::span::Entered {
   fn on_resume(self: Pin<&mut Self>, _: &mut Context) {
      // `Entered.span` is a private field on `tracing::span::Entered`.
      self.span.enter() // marks the span as "entered"
   }
   
   fn on_suspend(self: Pin<&mut Self>, _: &mut Context) {
      self.span.exit() // marks the span as "exited"
   }
}

Implementing the Lifecycle on tracing’s Entered guard, rather than on Span, allows async blocks to pass around unentered spans. In practice lets tracing's API for entering a span in asynchronous code to consistent with the API for entering a span in asynchronous code.

Guide-level explanation

Reference-level explanation

Today,  <expr>.await desugars to:
match <expr> {
    mut pinned => loop {
        match ::std::future::poll_with_tls_context(unsafe {
            <::std::pin::Pin>::new_unchecked(&mut pinned)
        }) {
               ::std::task::Poll::Ready(result) => break result,
               ::std::task::Poll::Pending => {}
           }
    yield ();