Skip to content

Conversation

@dkhalanskyjb
Copy link
Collaborator

Use case: collecting elements up until the point the channel is closed without losing the elements when toList when the exception is thrown.

This function is similar to Flow<T>.toList(destination), which we already have, so the addition makes sense from the point of view of consistency as well.

Use case: collecting elements up until the point the channel is
closed without losing the elements when `toList` when the exception
is thrown.

This function is similar to `Flow<T>.toList(destination)`,
which we already have, so the addition makes sense from the
point of view of consistency as well.
@dkhalanskyjb dkhalanskyjb requested a review from murfel September 10, 2025 08:16
Copy link
Contributor

@murfel murfel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dkhalanskyjb dkhalanskyjb force-pushed the dkhalanskyjb/Channel.toList(destination) branch from 63b1e1a to 2d8f52b Compare September 10, 2025 10:49
@dkhalanskyjb dkhalanskyjb force-pushed the dkhalanskyjb/Channel.toList(destination) branch from 4992f06 to aa1adf1 Compare September 10, 2025 11:04
@dkhalanskyjb dkhalanskyjb force-pushed the dkhalanskyjb/Channel.toList(destination) branch from aa1adf1 to 6039cf8 Compare September 10, 2025 11:05
@murfel murfel self-requested a review October 9, 2025 09:43
@murfel
Copy link
Contributor

murfel commented Oct 9, 2025

Could you also fix the Channel.toList docs to avoid saying "all elements" (two entries on this page)
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/to-list.html

@dkhalanskyjb
Copy link
Collaborator Author

dkhalanskyjb commented Oct 14, 2025

@murfel, that page is automatically generated from the documentation comments that are edited in this PR.

@murfel
Copy link
Contributor

murfel commented Oct 14, 2025

I'm aware.

@dkhalanskyjb dkhalanskyjb force-pushed the dkhalanskyjb/Channel.toList(destination) branch from 938369e to caed24d Compare October 14, 2025 10:41
*
* There is no way to recover channel elements if the channel gets closed with an exception
* or to apply additional transformations to the elements before building the resulting collection.
* Please use [consumeAsFlow] and [kotlinx.coroutines.flow.toCollection] for such advanced use-cases.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Please use [consumeAsFlow] and [kotlinx.coroutines.flow.toCollection] for such advanced use-cases.
* Please use either [consumeTo] or [consumeAsFlow] and [kotlinx.coroutines.flow.toCollection] for such advanced use-cases.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do want to direct the users to Flow here.

* check(channel.toList() == values)
* ```
*/
public suspend fun <E> ReceiveChannel<E>.toList(): List<E> = buildList {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We forgot about lost elements (onUndeliveredElement). It should be at least mentioned in the doc, and possibly onUndeliveredElement could be added as a parameter.

onUndeliveredElement overcomplicates the signature for no real use-case, but provides completeness. No strong opinion on my end.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain what behavior of onUndeliveredElement is missing from the description? We do say that the elements that did get delivered may still be lost if the channel is closed with a cause.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, found it.

It should be placed (or duplicated) at the end, near the description of its terminal behaviour, since it's logically connected.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how that's connected. Terminal only means that the operation cancels the channel, which does get mentioned.

* [consumeTo] attempts to receive elements and put them into the collection until the channel is
* [closed][SendChannel.close].
*
* If the channel is [closed][SendChannel.close] with a cause, [consumeTo] will rethrow that cause.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* If the channel is [closed][SendChannel.close] with a cause, [consumeTo] will rethrow that cause.
* If the channel is [closed][SendChannel.close] with a cause, [consumeTo] will rethrow that cause immediately.
* The channel could still contain elements that will never be received.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind, I short circuited (and also misread the docs). Please do add when exactly the exception will be rethrown, though.

The channel could still contain elements that will never be received.

Elaborate this whole scenario.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do add when exactly the exception will be rethrown, though.

What happens there is inherent to the behavior of closing the channel: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/close.html

Elaborate this whole scenario.

The text you quote is incorrect, closing the channel still allows accessing all elements there.

* channel.consumeTo(remainingElements)
* } finally {
* println("Remaining elements: $remainingElements")
* }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A more closer example would be to have a channel which is can be closed with a cause, to separate the usage of this function from just channel.toList()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also would be nice to show an example when consumeTo is used before the channel is definitely closed.

(This example could approach this by just removing delay on L251)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit wary that you are advocating exclusively for one particular use-case, here in the sample and in the kdoc.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's discuss the intention of this API and samples on Monday!

* check(channel.toList() == values)
* ```
*/
public suspend fun <E> ReceiveChannel<E>.toList(): List<E> = buildList {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, found it.

It should be placed (or duplicated) at the end, near the description of its terminal behaviour, since it's logically connected.

* [consumeTo] attempts to receive elements and put them into the collection until the channel is
* [closed][SendChannel.close].
*
* If the channel is [closed][SendChannel.close] with a cause, [consumeTo] will rethrow that cause.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind, I short circuited (and also misread the docs). Please do add when exactly the exception will be rethrown, though.

The channel could still contain elements that will never be received.

Elaborate this whole scenario.

@murfel
Copy link
Contributor

murfel commented Oct 28, 2025

Each consume-like function has a very similar description. Could you unify these descriptions? Since they are saying the same thing with different words. Or worse, give slightly different information.

E.g. consume and consumeEach both end with

consume

 * [consume] does not guarantee that new elements will not enter the channel after [block] finishes executing, so
 * some channel elements may be lost.
 * Use the `onUndeliveredElement` parameter of a manually created [Channel] to define what should happen with these
 * elements during [ReceiveChannel.cancel].

consumeEach

 * **Pitfall**: even though the name says "each", some elements could be left unprocessed if they are added after
 * this function decided to close the channel.
 * In this case, the elements will simply be lost.
 * If the elements of the channel are resources that must be closed (like file handles, sockets, etc.),
 * an `onUndeliveredElement` must be passed to the [Channel] on construction.
 * It will be called for each element left in the channel at the point of cancellation.

The way to trigger the pitfall that used to be described can be
boiled down to this pattern:
```kotlin
run {
    channel.consumeEach {
        if (channel.isEmpty) {
            // do something
            return@run
        } else {
            // do something else
        }
    }
}
```

However, here, `isEmpty` is already introducing a race condition,
so `consumeEach` itself does not cause any additional issues.

This does not seem like a pitfall in realistic scenarios after all.
`consumeEach` can perform an early return and thus erase
the elements present in the channel, but this also goes for
elements that possibly entered the channel long ago; without
explicitly checking if the channel is empty, there is no way to
distinguish these two scenarios.
@dkhalanskyjb
Copy link
Collaborator Author

Good job noticing the discrepancy, fixed it.

@dkhalanskyjb dkhalanskyjb requested a review from murfel October 29, 2025 06:22
Copy link
Contributor

@murfel murfel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The kdocs still have many discrepancies, the ones I mentioned and other, but that is a non-goal of the PR. Otherwise no concerns.

@dkhalanskyjb dkhalanskyjb merged commit fdb01da into develop Nov 5, 2025
1 check passed
@dkhalanskyjb dkhalanskyjb deleted the dkhalanskyjb/Channel.toList(destination) branch November 5, 2025 07:13
@dkhalanskyjb dkhalanskyjb changed the title Add ReceiveChannel.toList(destination) Add ReceiveChannel.consumeTo(destination) Nov 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants