Skip to content

Add a DocC article on promise and event ordering in Channel implementations - #3693

Open
fabianfett wants to merge 2 commits into
apple:mainfrom
fabianfett:ff-docc-promise-and-event-ordering
Open

Add a DocC article on promise and event ordering in Channel implementations#3693
fabianfett wants to merge 2 commits into
apple:mainfrom
fabianfett:ff-docc-promise-and-event-ordering

Conversation

@fabianfett

Copy link
Copy Markdown
Member

Motivation:

The order in which a Channel fulfills an operation's promise and fires the matching pipeline event is a guarantee handlers depend on, but it was only written down implicitly, spread across NIOPosix and ChannelNotificationTest. Anyone implementing ChannelCore had to reverse engineer it.

Modifications:

Add Sources/NIOCore/Docs.docc/channel-notification-order.md, documenting the state -> promise -> pipeline event -> closeFuture ordering, with per-operation walkthroughs for registration, activation, closing, half-closure and writability, plus sections on error reporting, re-entrancy, guards and finally an short implementation checklist. Link it from the NIOCore article index.

Result:

  • ChannelCore implementers have a written reference for the notification order.
  • No functional change.

…ations

Motivation:

The order in which a `Channel` fulfills an operation's promise and fires the matching pipeline event is a guarantee handlers depend on, but it was only written down implicitly, spread across NIOPosix and ChannelNotificationTest. Anyone implementing ChannelCore had to reverse engineer it.

Modifications:

Add Sources/NIOCore/Docs.docc/channel-notification-order.md, documenting the state -> promise -> pipeline event -> closeFuture ordering, with per-operation walkthroughs for registration, activation, closing, half-closure and writability, plus sections on error reporting, re-entrancy, guards and finally an short implementation checklist.
Link it from the NIOCore article index.

Result:

- `ChannelCore` implementers have a written reference for the notification order.
- No functional change.
@fabianfett
fabianfett requested a review from Lukasa August 5, 2026 13:22
@fabianfett fabianfett added the semver/none No version bump required. label Aug 5, 2026
its promises and fire its pipeline events.

Almost every operation on a ``Channel`` has two visible outcomes. The first one
is private to the caller: the ``EventLoopPromise`` that was passed to the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This framing isn't quite right. This object isn't private to the caller: it's passed up and down the channel pipeline.

protocol. Its methods are public, but they exist for the use of the ``Channel``
implementation itself and should only ever be called from the channel's
``EventLoop``. If you are writing a ``ChannelHandler``, read this article as a
description of the guarantees you are given, and keep using

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is not just a description of the guarantees you're given, but the ones you are expected to continue to uphold if you, for example, interact with the promises or the events.


The rest of this article works through each operation in turn.

### Registration

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We may want to leverage this section in future to talk about what these events mean, but for now this is totally fine.

performs the `bind` and `listen` syscalls, and only in the success continuation
does it drive the activation, which in turn fulfills the user's bind promise.
The comment in that code is blunt about it: it is important to call the state
changing methods before notifying the original promise, for ordering reasons.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure that this last note about the comment adds much.

- A **registration** failure does both: it fires `errorCaught`, closes the
channel, and fails the register promise. A failing re-registration does the
same, minus the promise, because there is none.
- A failing **deregistration** during a close is fired as `errorCaught`, whereas

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Might be worth calling out that it isn't really possible for deregistration to fail in a meaningful way.

@glbrntt glbrntt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Neat! This is helpful.

Comment on lines +40 to +42
4. **Fire ``Channel/closeFuture`` last of all**, and only when the channel is
being torn down. This happens after the pipeline has been dismantled, on a
later event loop tick.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Closing is a special case so it may be worth removing it from here and the single sentence below, and instead calling it out as special and covered in more detail below.

promise?.fail(ChannelError.ioOnClosedChannel)
return
}
guard !self.isRegistered else {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Complete nit, but IMO if X is more readable than guard !X else

Suggested change
guard !self.isRegistered else {
if self.isRegistered {

while doing this.
2. **Fulfill the operation's promise.** Succeed or fail the ``EventLoopPromise``
that the caller handed to the ``ChannelCore`` method.
3. **Fire the matching pipeline event.** Only now announce the new state to the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor: examples below use "broadcast", I think it's helpful to have consistent terminology here.


First, if your activation is asynchronous — a `connect` that returns
`EINPROGRESS`, for example — you should hold on to the connect promise and only
fulfill it at the point where you actually become active. NIO stores it in a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: "you" don't become active, the channel does

Comment on lines +120 to +121
`pendingConnect` property and passes it into the activation path once the
connection has been established. Avoid succeeding the connect promise early and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think it's worth being a bit more concrete here, "NIO stores it in a pendingConnect property..." sounds like NIO does this automatically for all channels which may be misleading if you are implementing a Channel which needs to support connect.

is the one NIO uses for a full close, i.e. ``CloseMode/all``. The half-closure
modes are covered further down.

| Step | What happens |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: seems odd to use a table here instead of an ordered list

| 5 | Succeed (or fail) the **close** promise |
| 6 | Fire ``ChannelInboundInvoker/fireChannelInactive()`` |
| 7 | Fire ``ChannelInboundInvoker/fireChannelUnregistered()`` |
| 8 | On a **later event loop tick**: remove all handlers from the pipeline, then succeed ``Channel/closeFuture`` |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

later or next loop tick?

//
// 2. Do the work and reconcile the state. Any error that we discover in
// here is recorded and only fired once the state is consistent again.
var errorCallouts: [(ChannelPipeline) -> Void] = []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We only have at most one callout in this example: perhaps the array overcomplicates things here?

Comment on lines +252 to +253
guard !self.inputShutdown else {
promise?.fail(ChannelError.inputClosed)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same nit as about re: guard !x else

private func flushNow() {
// Re-entrancy protection: a write issued from one of the callouts below is
// picked up by this loop, rather than starting a nested flush.
guard !self.inFlushNow else { return }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same guard !X else nit

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

semver/none No version bump required.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants