Skip to content

Flow<T>.first operator throws NoSuchElementException where CancellationException must be thrown #2051

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

Closed
denyshorman opened this issue May 24, 2020 · 2 comments
Assignees

Comments

@denyshorman
Copy link

Consider the following test

@Test
fun flowFirstOperatorTest() {
    assertDoesNotThrow {
        runBlocking(Dispatchers.Default) {
            launch {
                (0..10000).asFlow().flatMapMerge(Int.MAX_VALUE) {
                    channelFlow {
                        val v = channelFlow { send(1) }.first()
                        send(v)
                    }
                }.take(100)
            }
        }
    }
}

It is expected to take N elements and correctly finish the coroutine but actually it throws NoSuchElementException.

It happens because AbortFlowException, that is used inside first operator, extends CancellationException and if upstream flow is cancelled before calling collect, it must throw CancellationException instead of NoSuchElementException.

Test passes when AbortFlowException is extended from Throwable

@Test
fun flowFirstOperatorTest() {
    assertDoesNotThrow {
        runBlocking(Dispatchers.Default) {
            launch {
                (0..10000).asFlow().flatMapMerge(Int.MAX_VALUE) {
                    channelFlow {
                        val v = channelFlow { send(1) }.firstFixed()
                        send(v)
                    }
                }.take(100)
            }
        }
    }
}

object AbortFlowException : Throwable("", null, true, false)

suspend fun <T> Flow<T>.firstFixed(): T {
    var result: T? = null
    try {
        collect { value ->
            result = value
            throw AbortFlowException
        }
    } catch (e: AbortFlowException) {
        // Do nothing
    }

    if (result == null) throw NoSuchElementException("Expected at least one element")
    return result!!
}

This issue is relevant for firstOrNull operator and, perhaps, other similar ones.

@qwwdfsad qwwdfsad added the flow label May 24, 2020
@qwwdfsad qwwdfsad self-assigned this May 24, 2020
@qwwdfsad qwwdfsad added the bug label May 24, 2020
qwwdfsad added a commit that referenced this issue May 25, 2020
…erator.

It fixes two problems:
    * NoSuchElementException can be thrown during cancellation sequence (see FirstJvmTest that reproduces this problem with explanation)
    * Cancellation can be accidentally suppressed and flow activity can be prolonged

Fixes #2051
@qwwdfsad
Copy link
Collaborator

Thanks for the report with self-contained repro!

Fixed using a more future-proof approach with checking the identity of the caught exception

qwwdfsad added a commit that referenced this issue May 25, 2020
…erator.

It fixes two problems:
    * NoSuchElementException can be thrown during cancellation sequence (see FirstJvmTest that reproduces this problem with explanation)
    * Cancellation can be accidentally suppressed and flow activity can be prolonged

Fixes #2051
@denyshorman
Copy link
Author

@qwwdfsad, sounds good. Thanks!

@elizarov elizarov mentioned this issue Jul 16, 2020
1 task
recheej pushed a commit to recheej/kotlinx.coroutines that referenced this issue Dec 28, 2020
Kotlin#2057)

It fixes two problems:
    * NoSuchElementException can be thrown during cancellation sequence (see FirstJvmTest that reproduces this problem with explanation)
    * Cancellation can be accidentally suppressed and flow activity can be prolonged

Fixes Kotlin#2051
denyshorman added a commit to denyshorman/experimental-asset-trader that referenced this issue May 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants