-
Notifications
You must be signed in to change notification settings - Fork 7
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
Changes from 2 commits
e102770
7c183e9
40afca1
3ca7afc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,55 +16,54 @@ | |
|
||
package import NIOCore | ||
|
||
package struct Timer { | ||
/// The delay to wait before running the task. | ||
private let delay: TimeAmount | ||
/// The task to run, if scheduled. | ||
private var task: Kind? | ||
/// Whether the task to schedule is repeated. | ||
private let `repeat`: Bool | ||
/// A timer backed by `NIOScheduledCallback`. | ||
package final class Timer<Handler: NIOScheduledCallbackHandler> where Handler: Sendable { | ||
/// The event loop on which to run this timer. | ||
private let eventLoop: any EventLoop | ||
|
||
private enum Kind { | ||
case once(Scheduled<Void>) | ||
case repeated(RepeatedTask) | ||
/// The duration of the timer. | ||
private let duration: TimeAmount | ||
|
||
func cancel() { | ||
switch self { | ||
case .once(let task): | ||
task.cancel() | ||
case .repeated(let task): | ||
task.cancel() | ||
} | ||
} | ||
} | ||
/// Whether this timer should repeat. | ||
private let repeating: Bool | ||
|
||
/// The handler to call when the timer fires. | ||
private let handler: Handler | ||
|
||
/// The currently scheduled callback if the timer is running. | ||
private var scheduledCallback: NIOScheduledCallback? | ||
|
||
package init(delay: TimeAmount, repeat: Bool = false) { | ||
self.delay = delay | ||
self.task = nil | ||
self.repeat = `repeat` | ||
package init(eventLoop: any EventLoop, duration: TimeAmount, repeating: Bool, handler: Handler) { | ||
self.eventLoop = eventLoop | ||
self.duration = duration | ||
self.repeating = repeating | ||
self.handler = handler | ||
self.scheduledCallback = nil | ||
} | ||
|
||
/// Schedule a task on the given `EventLoop`. | ||
package mutating func schedule( | ||
on eventLoop: any EventLoop, | ||
work: @escaping @Sendable () throws -> Void | ||
) { | ||
self.task?.cancel() | ||
/// Cancel the timer, if it is running. | ||
package func cancel() { | ||
self.eventLoop.assertInEventLoop() | ||
guard let scheduledCallback = self.scheduledCallback else { return } | ||
scheduledCallback.cancel() | ||
} | ||
|
||
if self.repeat { | ||
let task = eventLoop.scheduleRepeatedTask(initialDelay: self.delay, delay: self.delay) { _ in | ||
try work() | ||
} | ||
self.task = .repeated(task) | ||
} else { | ||
let task = eventLoop.scheduleTask(in: self.delay, work) | ||
self.task = .once(task) | ||
} | ||
/// Start or restart the timer. | ||
package func start() { | ||
self.eventLoop.assertInEventLoop() | ||
self.scheduledCallback?.cancel() | ||
// Only throws if the event loop is shutting down, so we'll just swallow the error here. | ||
self.scheduledCallback = try? self.eventLoop.scheduleCallback(in: self.duration, handler: self) | ||
} | ||
} | ||
|
||
/// Cancels the task, if one was scheduled. | ||
package mutating func cancel() { | ||
self.task?.cancel() | ||
self.task = nil | ||
extension Timer: NIOScheduledCallbackHandler, @unchecked Sendable where Handler: Sendable { | ||
/// For repeated timer support, the timer itself proxies the callback and restarts the timer. | ||
/// | ||
/// - NOTE: Users should not call this function directly. | ||
package func handleScheduledCallback(eventLoop: some EventLoop) { | ||
self.eventLoop.assertInEventLoop() | ||
self.handler.handleScheduledCallback(eventLoop: eventLoop) | ||
if self.repeating { self.start() } | ||
Comment on lines
+64
to
+67
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couple of questions:
class
?Sendable
? The previousLoopBoundView
wasSendable
because it needed to be pass to theschedule(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 theassertInEventLoop
checks as well.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 forhandleScheduledCallback
is non-mutating.I've added a commit to address that.
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
:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for clarifying 👍