Skip to content

Implement Async.unsupervised scope — core lifecycle #303

Description

@rcardin

Parent

#302

What to build

Implement Async.unsupervised[A](block: Async ?=> A)(using async: Async): A in yaes-core.

This is an Ox-style unsupervised scope: the block runs normally; when it completes, all still-running fibers are cancelled (not waited for); the method returns only after cancellation propagates. No new types or methods on Fiber[A] — the existing Async.fork is reused unchanged, as the supervision model is a property of the active scope stored in JvmAsync.scope, not of the fork call.

Key implementation decisions (from prototype):

def unsupervised[A](block: Async ?=> A)(using async: Async): A = {
  val scope = StructuredTaskScope.open[Any, ...](Joiner.awaitAll(), ...)
  val prev  = JvmAsync.scope.get()
  JvmAsync.scope.set(scope.asInstanceOf[StructuredTaskScope[Any, Any]])
  try {
    val result = block(using async)
    JvmAsync.ensureJoined(scope)   // interrupt trick: join() returns fast
    Thread.interrupted()           // clear interrupt flag
    result
  } catch {
    case t: Throwable =>
      JvmAsync.ensureJoined(scope)
      Thread.interrupted()         // must clear here too — mirrors Async.run
      throw t
  } finally {
    if (prev != null) JvmAsync.scope.set(prev)  // restore, do NOT remove
    else JvmAsync.scope.remove()
    scope.close()
  }
}

Joiner.awaitAll() is chosen because its only live effect is suppressing fail-fast: it does not cancel sibling fibers when one fails, unlike awaitAllSuccessfulOrThrow(). The ensureJoined interrupt trick (set interrupt flag → join() returns immediately → close() cancels remaining) is what prevents the block from waiting for natural fiber completion. Thread.interrupted() must be called in both the happy path and the catch path to clear the flag before returning or rethrowing.

Scope save/restore: Async.unsupervised is always nested inside an existing scope, so the previous JvmAsync.scope value must be saved and restored in finally — not removed — to avoid clobbering the parent scope.

Acceptance criteria

  • Async.unsupervised is implemented in yaes-core and compiles with Java 25 + Scala 3.8.1
  • Async.fork works inside Async.unsupervised without modification
  • Test: block returns promptly when an unjoined long-running fiber is still running; the fiber is confirmed cancelled (e.g. via a flag set in its finally or interrupt handler)
  • Test: an exception thrown from the main body of the block propagates to the caller
  • Scaladoc on Async.unsupervised following project conventions (brief description, @param, @return, usage example in {{{ }}})

Blocked by

None — can start immediately

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestready-for-agentFully specified, ready for an AFK agentyaes-coreYAES core effects

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions