Skip to content

Conversation

@aryan-25
Copy link
Contributor

Motivation:

Currently, pending consumer closures remain in the {in}{out}boundBufferConsumer queues of EmbeddedChannelCore even after the channel closes. It is also possible to enqueue consumers after the channel closes. In these cases, the consumer closures will never be invoked and this can lead to unfavourable behaviour, as observed in NIOAsyncTestingChannel's waitFor{In}{Out}boundWrite methods (the only places these queues are currently used).

NIOAsyncTestingChannel's waitFor{In}{Out}boundWrite methods complete a continuation inside the consumer closure. In the cases described above, the continuation never completes and therefore waitFor{In}{Out}boundWrite never returns.

Modifications:

  • Updated the element type in EmbeddedChannelCore's {in}{out}boundBufferConsumer from (NIOAny) -> Void to (Result<NIOAny, Error>) -> Void.
    • This is so that the .failure case can be used to notify the consumer closure that the channel has closed.
  • Changed the visibility of the {in}{out}boundBufferConsumer properties from internal to private in order to prevent the queues from being accessed and being appended to without the call site considering whether the channel has been closed.
    • Added new internal methods named enqueue{In}{Out}boundBufferConsumer(_:) which take the consumer closure as an argument and only append to the corresponding queue if the channel isn't closed.
    • If the channel is closed, the consumer closure is invoked immediately with .failure(ChannelError.ioOnClosedChannel).
  • Updated EmbeddedChannelCore's close0 method to return a .failure(ChannelError.ioOnClosedChannel) result to each closure in {in}{out}boundBufferConsumer and empty both buffers.
  • Updated NIOAsyncTestingChannel's waitFor{In}{Out}boundWrite to throw an error in the continuation upon receiving a .failure result.
  • Added associated test cases.

Result:

EmbeddedChannelCore's {in}{out}boundBufferConsumer queues can be used more safely: all pending closures will be invoked upon channel close. As a result, NIOAsyncTestingChannel's waitFor{In}{Out}boundWrite no longer indefinitely blocks when the channel closes.

…on channel close.

### Motivation:
Currently, pending consumer closures remain in the `{in}{out}boundBufferConsumer` queues of `EmbeddedChannelCore` even after the channel closes. Also, it is possible to enqueue consumers *after* the channel closes. In these cases, the consumer closures will never be invoked and this can lead to unfavourable behaviour, as observed in `NIOAsyncTestingChannel`'s `waitFor{In}{Out}boundWrite` methods (the only place these queues are currently used).

`NIOAsyncTestingChannel`'s `waitFor{In}{Out}boundWrite` methods complete a continuation *inside* the consumer closure. In the cases described above, the continuation never completes and therefore `waitFor{In}{Out}boundWrite` never returns.

### Modifications:
- Updated the element type in `EmbeddedChannelCore`'s `{in}{out}boundBufferConsumer` from `(NIOAny) -> Void` to `(Result<NIOAny, Error>) -> Void`.
  - This is so that the `.failure` case can be used to notify the consumer closure that the channel has closed.
- Changed the visibility of the `{in}{out}boundBufferConsumer` properties from `internal` to `private` in order to prevent the queues from being accessed and being appended to without the call-site considering whether the channel has been closed.
  - Added new methods named `enqueue{In}{Out}boundBufferConsumer(_:)` which take the consumer closure as an argument and only append to the corresponding queue if the channel isn't closed. If the channel is closed, the consumer closure is invoked immediately with `.failure(ChannelError.ioOnClosedChannel)`.
- Updated `EmbeddedChannelCore`'s `close0` method to empty out all pending closures in `{in}{out}boundBufferConsumer` and return a `.failure(ChannelError.ioOnClosedChannel)` result to each closure.
- Updated `NIOAsyncTestingChannel`'s `waitFor{In}{Out}boundWrite` to throw an error in the continuation upon receiving a `.failure` result.
- Added associated test cases.

### Result:
`EmbeddedChannelCore`'s `{in}{out}boundBufferConsumer` queues can be used more safely and all pending closures are invoked upon channel close. As a result, `NIOAsyncTestingChannel`'s `waitFor{In}{Out}boundWrite` no longer indefinitely blocks when the channel closes.
@aryan-25 aryan-25 added the 🔨 semver/patch No public API change. label Dec 17, 2025
Copy link
Contributor

@josephnoir josephnoir left a comment

Choose a reason for hiding this comment

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

LGTM!

/// Enqueue a consumer closure that will be invoked upon the next pending inbound write.
/// - Parameter newElement: The consumer closure to enqueue. Returns a `.failure` result if the channel has already
/// closed.
func enqueueInboundBufferConsumer(_ newElement: @escaping (Result<NIOAny, Error>) -> Void) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: we typically annotate methods only supposed to be called on the EL with a leading underscore, and I recommend making this private if it can be.

Copy link
Contributor Author

@aryan-25 aryan-25 Jan 6, 2026

Choose a reason for hiding this comment

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

I've added a leading underscore to both methods (enqueue{In}{Out}boundBufferConsumer(_:)).

Those methods are defined in EmbeddedChannelCore and are also called from NIOAsyncTestingChannel, so unfortunately, they cannot be made private.

@Lukasa Lukasa merged commit 5dc3b4b into apple:main Jan 6, 2026
55 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 semver/patch No public API change.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants