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

JS - 1.4.0-M1 - Channel based Coroutine cancelling when it shouldn't #2318

Closed
ScottPierce opened this issue Oct 19, 2020 · 7 comments
Closed
Labels

Comments

@ScottPierce
Copy link
Contributor

ScottPierce commented Oct 19, 2020

I'm using Kotlin 1.4.10 and kotlinx.coroutines 1.4.0-M1. The below code replicates a race condition I'm seeing in a node process where the data sometimes tries to send after the channel is closed. I'm seeing the coroutine cancel and not complete, when it shouldn't.

I'm not able to reproduce this with a small number of parallel calls, but I'm able to regularly reproduce this, with ~20 calls. I've reproduced what I'm seeing on a smaller scale below.

Description of code:

I'm running separate coroutines (non-child coroutines) that all encounter errors where they try to send a value through a channel after it's been closed. The send statements themselves are wrapped in try/catch, and each coroutine is wrapped in an async/await with an additional try catch, returning null if there are any errors / exceptions.

The root coroutine should always finish, as it's protected from any of the coroutine errors that could happen. At the end of the root coroutine you can see a print out of Done that should happen, but when the error happens, Done isn't printed, and instead the test fails.

class ErrorReproTest {
    @Test fun jsFailure() = GlobalScope.async {
        val deferredList = mutableListOf<Deferred<Int>>()
        repeat(20) {
            deferredList += GlobalScope.async { lastValueFromFlow() }
        }

        val results = deferredList.map {
            try {
                it.await()
            } catch (e: Throwable) {
                e.printStackTrace()
                null
            }
        }

        println("Done: $results")
    }.asPromise()
}

fun channelFlow(): Flow<Int> {
    val channel = Channel<Int>()

    GlobalScope.launch {
        channel.send(0)
        delay(100)
        channel.cancel() // Cancel channel
        try {
            channel.send(2)
        } catch (e: Exception) {
        }
    }

    return channel.receiveAsFlow()
}

private suspend fun lastValueFromFlow(): Int {
    var last: Int? = null
    channelFlow().collect {
        last = it
    }
    return last!!
}

I end up with a bunch of these exceptions, and my code never completes:

CancellationException: RendezvousChannel was cancelled
    at Object.captureStack (/Users/scott.pierce/workspace/dev.scottpierce/mobile-tester/mobile-tester-repository/build/js/node_modules/kotlin/kotlin/builtins.kt:103:21)
    at CancellationException.Exception [as constructor] (/Users/scott.pierce/workspace/dev.scottpierce/mobile-tester/mobile-tester-repository/build/js/node_modules/kotlin/kotlin.js:39444:14)
    at CancellationException.RuntimeException [as constructor] (/Users/scott.pierce/workspace/dev.scottpierce/mobile-tester/mobile-tester-repository/build/js/node_modules/kotlin/kotlin/exceptions.kt:35:101)
    at CancellationException.IllegalStateException [as constructor] (/Users/scott.pierce/workspace/dev.scottpierce/mobile-tester/mobile-tester-repository/build/js/node_modules/kotlin/kotlin/exceptions.kt:47:106)
    at CancellationException [as constructor] (/Users/scott.pierce/workspace/dev.scottpierce/mobile-tester/mobile-tester-repository/build/js/node_modules/kotlinx-coroutines-core/kotlinx-coroutines-core.js:39394:27)
    at CancellationException_init (/Users/scott.pierce/workspace/dev.scottpierce/mobile-tester/mobile-tester-repository/build/js/node_modules/kotlinx-coroutines-core/kotlinx-coroutines-core.js:39403:27)
    at RendezvousChannel.AbstractChannel.cancel_m4sck1$$default (/Users/scott.pierce/workspace/dev.scottpierce/mobile-tester/mobile-tester-repository/build/js/node_modules/kotlinx-coroutines-core/kotlinx-coroutines-core.js:5964:55)
    at RendezvousChannel.ReceiveChannel.cancel_m4sck1$ (/Users/scott.pierce/workspace/dev.scottpierce/mobile-tester/mobile-tester-repository/build/js/node_modules/kotlinx-coroutines-core/kotlinx-coroutines-core.js:7517:55)
    at Coroutine$channelFlow$lambda.doResume (/Users/scott.pierce/workspace/dev.scottpierce/mobile-tester/mobile-tester-repository/service/src/commonTest/kotlin/mobiletest/service/BasicInfoIosDataSourceTests.kt:113:17)
    at Coroutine$channelFlow$lambda.CoroutineImpl.resumeWith_tl1gpc$ (/Users/scott.pierce/workspace/dev.scottpierce/mobile-tester/mobile-tester-repository/build/js/node_modules/kotlin/kotlin/coroutines/CoroutineImpl.kt:47:35)
    at resume (/Users/scott.pierce/workspace/dev.scottpierce/mobile-tester/mobile-tester-repository/build/js/node_modules/kotlinx-coroutines-core/kotlinx-coroutines-core.js:36301:26)
    at dispatch (/Users/scott.pierce/workspace/dev.scottpierce/mobile-tester/mobile-tester-repository/build/js/node_modules/kotlinx-coroutines-core/kotlinx-coroutines-core.js:36290:7)
    at CancellableContinuationImpl.dispatchResume_0 (/Users/scott.pierce/workspace/dev.scottpierce/mobile-tester/mobile-tester-repository/build/js/node_modules/kotlinx-coroutines-core/kotlinx-coroutines-core.js:1555:3)
    at CancellableContinuationImpl.resumeImpl_0 (/Users/scott.pierce/workspace/dev.scottpierce/mobile-tester/mobile-tester-repository/build/js/node_modules/kotlinx-coroutines-core/kotlinx-coroutines-core.js:1587:18)
    at CancellableContinuationImpl.resumeUndispatched_hyuxa3$ (/Users/scott.pierce/workspace/dev.scottpierce/mobile-tester/mobile-tester-repository/build/js/node_modules/kotlinx-coroutines-core/kotlinx-coroutines-core.js:1658:8)
    at Timeout._onTimeout (/Users/scott.pierce/workspace/dev.scottpierce/mobile-tester/mobile-tester-repository/build/js/node_modules/kotlinx-coroutines-core/kotlinx-coroutines-core.js:39473:12)
    at listOnTimeout (internal/timers.js:549:17)
    at processTimers (internal/timers.js:492:7)
@LouisCAD
Copy link
Contributor

LouisCAD commented Oct 19, 2020

Duplicate of #974

@LouisCAD
Copy link
Contributor

What happens to offer can also happen to send.

@ScottPierce
Copy link
Contributor Author

ScottPierce commented Oct 19, 2020

This isn't a duplicate of #974. I've edited the above sample to surround the channel.send with a try / catch, and I can still reproduce the failure. I think I'm encountering a bug in the coroutines js code.

This isn't the first bug I've encountered with js channels - #2236

@ScottPierce
Copy link
Contributor Author

I found that by removing the e.printStackTrace() statement, the test passes, and the expected behavior is achieved. This doesn't make any sense as printing shouldn't impact the result of the test.

I've tried the same solution for my actual code, and I still encounter the issue.

@ScottPierce
Copy link
Contributor Author

ScottPierce commented Oct 20, 2020

I was able to work around this in my code by adding mutex locks around send and close of my channel.

@ScottPierce
Copy link
Contributor Author

This also happens on the IR compiler. Is this a Kotlin JS issue or a coroutines issue?

@qwwdfsad qwwdfsad added the js label Aug 5, 2021
@dkhalanskyjb
Copy link
Collaborator

After re-running the reproducer dozens of times, Done is always printed for me. So, closing the issue with a "could not reproduce." Given the reproducer's age, something likely got fixed in the meantime. If the issue resurfaces, we'll reopen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants