Cancellables and Scoping rules

In the section about Structured Concurrency, we learned that Async contexts can manage linked Futures. Let's explore this concept in details in this section.

CompletionGroup

Every Async context created from all sources (Async.blocking, Async.group and Future.apply) creates a brand new CompletionGroup, which is a set of Cancellables. Upon the end of the Async context's body, this CompletionGroup handles calling .cancel on all its Cancellables and wait for all of them to be removed from the group.

Adding a Cancellable to a Group is done by calling add on the Group, or calling link on the Cancellable. Since every Async context contains a CompletionGroup, an overload of link exists that would add the Cancellable to the Async context's group.

Async-scoped values

If you have data that needs to be cleaned up when the Async scope ends, implement Cancellable and link the data to the Async scope:

case class CloseCancellable(c: Closeable) extends Cancellable:
  def cancel() =
    c.close()
    unlink()

extension (closeable: Closeable)
  def link()(using Async) =
    CloseCancellable(closeable).link()

However, note that this would possibly create dangling resources that links to the passed-in Async context of a function:

def f()(using Async) =
  def resource = new Resource()
  resource.link() // links to Async

def main() =
  Async.blocking:
    f()
    // resource is still alive
    g()
    // ...
    0
    // resource is cancelled *here*

Unlinking

Just as Cancellables can be linked to a group, you can also unlink them from their current group. This is one way to create a dangling active Future that is indistinguishable from a passive Future. (Needless to say this is very much not recommended).

To write a function that creates and returns an active Future, write a function that takes an Async.Spawn context:

def returnsDangling()(using Async.Spawn): Future[Int] =
  Future:
    longComputation()

Again, this is a pattern that breaks structured concurrency and should not be used carelessly! One should avoid exposing this pattern of functions in a Gears-using public API of a library.