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

Use NIO scheduleCallback API for connection handler timers #28

Merged

Conversation

simonjbeaumont
Copy link
Contributor

Motivation

NIO 2.75.0 added new APIs for scheduling callbacks, which can be used to implement timers with fewer allocations than the previous APIs1.

Modifications

Replace the timers used in the client and server connection handlers with a new implementation that uses these new NIO APIs.

Result

This should have no functional impact. However it reduces the allocations in the connection handler. How many allocations it reduces will depend on the exact scenario, but running the echo server under Instruments and sending 1000 requests suggests around 10%.

Footnotes

  1. https://github.com/apple/swift-nio/releases/tag/2.75.0

Copy link
Collaborator

@glbrntt glbrntt left a comment

Choose a reason for hiding this comment

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

This looks great, thanks for doing it. I left a few questions.

extension ClientConnectionHandler {
struct LoopBoundView: @unchecked Sendable {
final class MaxIdleTimerHandlerView: @unchecked Sendable, NIOScheduledCallbackHandler {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Couple of questions:

  1. Does this need to be a class?
  2. Does it need to be Sendable? The previous LoopBoundView was Sendable because it needed to be pass to the schedule(on:) closure, I don't think we have a similar constraint any more (?)

If it doesn't need to be Sendable then I think we can remove the assertInEventLoop checks as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  • Does this need to be a class?

These view types can be structs. That they are classes, is a hangover from before I pushed this logic into Timer. Timer itself needs to be a class because the protocol requirement for handleScheduledCallback is non-mutating.

I've added a commit to address that.

  • Does it need to be Sendable? The previous LoopBoundView was Sendable because it needed to be pass to the schedule(on:) closure, I don't think we have a similar constraint any more (?)

It wasn't the case when the API was added to NIO, but it looks like it got changed since and now it is a requirement that the handler be Sendable:

    @preconcurrency
    @discardableResult
    func scheduleCallback(
        in amount: TimeAmount,
        handler: some (NIOScheduledCallbackHandler & Sendable)
    ) throws -> NIOScheduledCallback

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for clarifying 👍

Comment on lines +64 to +67
package func handleScheduledCallback(eventLoop: some EventLoop) {
self.eventLoop.assertInEventLoop()
self.handler.handleScheduledCallback(eventLoop: eventLoop)
if self.repeating { self.start() }
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you remind me what happens w.r.t. the callback if it's cancelled? Just wondering if it works how we expect for the repeating timers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that the NIO scheduled callback API is always one-shot. The repeated timer API here is layered on top.

The semantics of this timer is that if you cancel it you cancel all future recurrences too, but it can then be reused/restarted by calling start again.

There's a test case that covers this behaviour.

loop.advanceTime(by: .milliseconds(999))
XCTAssertEqual(value.load(ordering: .acquiring), 0)
XCTAssertEqual(handler.counter.value, 0)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Completely optional: but feel free to write these using swift-testing (we're slowly rolling our tests over to it). No problem if you don't want to spend the time doing it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, forgot that was an option. I'll pass on it this time, but I'll remember it for next time.

@glbrntt glbrntt added the 🔨 semver/patch No public API change. label Nov 11, 2024
Copy link
Collaborator

@glbrntt glbrntt left a comment

Choose a reason for hiding this comment

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

Looks great, thanks a lot @simonjbeaumont

@glbrntt glbrntt enabled auto-merge (squash) November 12, 2024 13:03
@glbrntt glbrntt merged commit 565aae3 into grpc:main Nov 12, 2024
33 of 36 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.

2 participants