Skip to content

launchMolecule can call setContent on a disposed Composition during startup cancellation #760

Description

@jmalpasuto

Description

launchMolecule can throw IllegalStateException("The composition is disposed") when the supplied coroutine context is cancelled during startup.

The current implementation creates a Recomposer and Composition, launches recomposer.runRecomposeAndApplyChanges() with CoroutineStart.UNDISPATCHED, disposes the composition in that coroutine's finally, and only then calls composition.setContent { ... }.

If the recomposer coroutine completes due to cancellation before setContent runs, setContent is invoked on an already-disposed composition.

I originally saw this in production Android crash reports with stacks like:

java.lang.IllegalStateException: The composition is disposed
  at androidx.compose.runtime.CompositionImpl.ensureRunning(Composition.kt:1470)
  at androidx.compose.runtime.CompositionImpl.setContent(Composition.kt:634)
  at app.cash.molecule.MoleculeKt.launchMolecule(Molecule.kt:259)
  at app.cash.molecule.MoleculeKt.launchMolecule$default(Molecule.kt:217)
  at app.cash.molecule.MoleculeKt$immediateClockFlow$1$1$1.invokeSuspend(molecule.kt:85)

Repro

This test demonstrates the failure on current trunk and also matches the same ordering in 2.2.0:

@Test fun cancelledContextClockLaunch() = runTest {
  val job = Job()
  val clock = BroadcastFrameClock()
  val scope = CoroutineScope(coroutineContext + clock)

  job.cancel()

  assertFailure {
    scope.launchMolecule(ContextClock, emitter = { fail("emitter should not be called") }, context = job) {
      fail("body should not be composed")
    }
  }.hasMessage("The composition is disposed")
}

Expected behavior

Launching with an already-cancelled context, or one that is cancelled during startup, should fail with normal coroutine cancellation semantics (CancellationException) and should not attempt Composition.setContent after disposal.

Possible fix direction

A small fix might be to check finalContext.ensureActive() before creating/launching the composition, and/or reorder startup so setContent happens before the recomposer coroutine can dispose the composition. If reordering, startup failures before the recomposer coroutine is launched should still dispose the composition and snapshot observer before rethrowing.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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