Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IllegalStateException when using Dispatchers.IO inside Composition.setContent #4562

Open
mgroth0 opened this issue Mar 31, 2024 · 1 comment
Assignees
Labels
bug Something isn't working desktop

Comments

@mgroth0
Copy link

mgroth0 commented Mar 31, 2024

Describe the bug

java.lang.IllegalStateException: Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied
	at androidx.compose.runtime.snapshots.SnapshotKt.readError(Snapshot.kt:1930)
	at androidx.compose.runtime.snapshots.SnapshotKt.current(Snapshot.kt:2275)
	at androidx.compose.runtime.SnapshotMutableStateImpl.setValue(SnapshotState.kt:308)
	at matt.compose.components.filetree.fttests.bug.ComposableSingletons$BugKt$lambda-1$1$1$1$1.invokeSuspend(bug.kt:28)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
	at androidx.compose.ui.platform.FlushCoroutineDispatcher$dispatch$2$1.invoke(FlushCoroutineDispatcher.skiko.kt:62)
	at androidx.compose.ui.platform.FlushCoroutineDispatcher$dispatch$2$1.invoke(FlushCoroutineDispatcher.skiko.kt:57)
	at androidx.compose.ui.platform.FlushCoroutineDispatcher.performRun(FlushCoroutineDispatcher.skiko.kt:99)
	at androidx.compose.ui.platform.FlushCoroutineDispatcher.access$performRun(FlushCoroutineDispatcher.skiko.kt:37)
	at androidx.compose.ui.platform.FlushCoroutineDispatcher$dispatch$2.invokeSuspend(FlushCoroutineDispatcher.skiko.kt:57)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:363)
	at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26)
	at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable$default(Cancellable.kt:21)
	at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:88)
	at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:123)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:52)
	at kotlinx.coroutines.BuildersKt.launch(Unknown Source)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch$default(Builders.common.kt:43)
	at kotlinx.coroutines.BuildersKt.launch$default(Unknown Source)
	at androidx.compose.ui.platform.FlushCoroutineDispatcher.dispatch(FlushCoroutineDispatcher.skiko.kt:56)
	at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:318)
	at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith$default(DispatchedContinuation.kt:274)
	at kotlinx.coroutines.DispatchedCoroutine.afterResume(Builders.common.kt:257)
	at kotlinx.coroutines.AbstractCoroutine.resumeWith(AbstractCoroutine.kt:99)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
	at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:111)
	at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:99)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:811)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:715)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:702)

Affected platforms

  • Desktop (macOS)

Versions

  • Kotlin version*: 2.0.0 Beta5
  • Compose Multiplatform version*: 1.6.1
  • OS version(s)* (required for Desktop and iOS issues): Mac 14.4.1
  • OS architecture (x86 or arm64): arm64
  • JDK (for desktop issues): 20

To Reproduce

Note that this bug is not 100% deterministic. Sometimes this has to be run 5 or so times before an exception is thrown

import androidx.compose.runtime.AbstractApplier
import androidx.compose.runtime.Composition
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCompositionContext
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.runComposeUiTest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.test.Test
import kotlin.test.assertFails

@OptIn(ExperimentalTestApi::class)
class SnapshotStateBug {
    @Test
    fun baseFailureCondition() {
        val exception = assertFails { runTest { withContext(Dispatchers.IO) { false } } }
        exception.printStackTrace()
    }
    @Test
    fun passesWithNoDispatch() {
        runTest { false }
    }

    @Test
    fun passesWithSleep() {
        runTest {
            withContext(Dispatchers.IO) {
                Thread.sleep(100)
                false
            }
        }
    }

    private fun runTest(
        valueProvider: suspend () -> Boolean
    ) {
        runComposeUiTest {
            setContent {
                val applier = remember { TestApplier() }
                val compositionContext = rememberCompositionContext()
                val composition = remember(applier, compositionContext) { Composition(applier, compositionContext) }
                val result = remember { mutableStateOf(true) }
                composition.setContent {
                    LaunchedEffect(Unit) {
                        result.value = valueProvider()
                    }
                }
            }
        }
    }
}


class TestApplier: AbstractApplier<Any?>(null) {
    override fun insertBottomUp(
        index: Int,
        instance: Any?
    ) = TODO("Not yet implemented")

    override fun insertTopDown(
        index: Int,
        instance: Any?
    ) = TODO("Not yet implemented")

    override fun move(
        from: Int,
        to: Int,
        count: Int
    ) = TODO("Not yet implemented")

    override fun onClear() = TODO("Not yet implemented")

    override fun remove(
        index: Int,
        count: Int
    ) = TODO("Not yet implemented")
}

Expected behavior

Additional context

The reason I found this bug is because I use the library Bonsai. In their file tree component, I am getting this error.

While creating a reproducer, I tried to simplify the reproducer as much as possible. Eventually, I broke down the function cafe.adriel.bonsai.core.tree.Tree and implemented it myself in the test you see above. This is when I disocvered that it was possible to reproduce this bug purely from Compose imports. Since I didn't have to import any Bonsai members, I thought this bug report belonged here and not in the Bonsai repository.

I barely have any understanding about how Applier or Composition work. I never use them myself. The only reason I used them here is because Bonsai uses them, and I copied the way that library used them to reproduce this bug.

I included 1 failure condition and 2 passing conditions in the reproducer to highlight how strange this behavior is. The way I would describe it is that the error only occurs if you use an IO dispatch that is super fast. If you have no IO dispatch at all, or an IO dispatch that takes some time, no error. Pretty stange, right?

@mgroth0 mgroth0 added bug Something isn't working submitted labels Mar 31, 2024
@mgroth0
Copy link
Author

mgroth0 commented Mar 31, 2024

For my actual application, my workaround (which is not very elegant) is to add Thread.sleep(100) before certain IO operations. This prevents the error from being thrown.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working desktop
Projects
None yet
Development

No branches or pull requests

3 participants