8000 Add `scheduleCallback` APIs to `NIOIsolatedEventLoop` by ptoffy · Pull Request #3263 · apple/swift-nio · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Add scheduleCallback APIs to NIOIsolatedEvent 8000 Loop #3263

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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions Sources/NIOCore/EventLoop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,11 @@ public protocol EventLoop: EventLoopGroup {

/// Schedule a callback at a given time.
///
/// - Parameters:
/// - deadline: The instant in time before which the task will not execute.
/// - handler: The handler that defines the behavior of the callback when executed or canceled.
/// - Returns: A ``NIOScheduledCallback`` that can be used to cancel the scheduled callback.
///
/// - NOTE: Event loops that provide a custom scheduled callback implementation **must** also implement
/// `cancelScheduledCallback`. Failure to do so will result in a runtime error.
@preconcurrency
Expand All @@ -382,6 +387,11 @@ public protocol EventLoop: EventLoopGroup {

/// Schedule a callback after given time.
///
/// - Parameters:
/// - amount: The amount of time before which the task will not execute.
/// - handler: The handler that defines the behavior of the callback when executed or canceled.
/// - Returns: A ``NIOScheduledCallback`` that can be used to cancel the scheduled callback.
///
/// - NOTE: Event loops that provide a custom scheduled callback implementation **must** also implement
/// `cancelScheduledCallback`. Failure to do so will result in a runtime error.
@preconcurrency
Expand Down Expand Up @@ -458,6 +468,40 @@ public protocol EventLoop: EventLoopGroup {
in: TimeAmount,
_ task: @escaping () throws -> T
) -> Scheduled<T>

/// Schedule a callback that is executed by this ``EventLoop`` at a given time, from a context where the caller
/// statically knows that the context is isolated.
///
/// This is an optional performance hook. ``EventLoop`` implementers are not required to implement
/// this witness, but may choose to do so to enable better performance of the isolated EL views. If
/// they do so, ``EventLoop/Isolated/scheduleCallback(at:_:)`` will perform better.
///
/// - Parameters:
/// - at: The instant in time before which the task will not execute.
/// - handler: The handler that defines the behavior of the callback when executed or canceled.
/// - Returns: A ``NIOScheduledCallback`` that can be used to cancel the scheduled callback.
@discardableResult
func _scheduleCallbackIsolatedUnsafeUnchecked(
at deadline: NIODeadline,
handler: some NIOScheduledCallbackHandler
) throws -> NIOScheduledCallback

/// Schedule a callback that is executed by this ``EventLoop`` after a given time, from a context where the caller
/// statically knows that the context is isolated.
///
/// This is an optional performance hook. ``EventLoop`` implementers are not required to implement
/// this witness, but may choose to do so to enable better performance of the isolated EL views. If
/// they do so, ``EventLoop/Isolated/scheduleCallback(in:_:)`` will perform better.
///
/// - Parameters:
/// - in: The amount of time before which the task will not execute.
/// - handler: The handler that defines the behavior of the callback when executed or canceled.
/// - Returns: A ``NIOScheduledCallback`` that can be used to cancel the scheduled callback.
@discardableResult
func _scheduleCallbackIsolatedUnsafeUnchecked(
in amount: TimeAmount,
handler: some NIOScheduledCallbackHandler
) throws -> NIOScheduledCallback
}

extension EventLoop {
Expand Down Expand Up @@ -533,6 +577,26 @@ extension EventLoop {
try unsafeTransfer.wrappedValue()
}
}

@inlinable
@discardableResult
public func _scheduleCallbackIsolatedUnsafeUnchecked(
at deadline: NIODeadline,
handler: some NIOScheduledCallbackHandler
) throws -> NIOScheduledCallback {
let unsafeHandlerWrapper = LoopBoundScheduledCallbackHandlerWrapper(wrapping: handler, eventLoop: self)
return try self.scheduleCallback(at: deadline, handler: unsafeHandlerWrapper)
}

@inlinable
@discardableResult
public func _scheduleCallbackIsolatedUnsafeUnchecked(
in amount: TimeAmount,
handler: some NIOScheduledCallbackHandler
) throws -> NIOScheduledCallback {
let unsafeHandlerWrapper = LoopBoundScheduledCallbackHandlerWrapper(wrapping: handler, eventLoop: self)
return try self.scheduleCallback(in: amount, handler: unsafeHandlerWrapper)
}
}

extension EventLoop {
Expand Down
43 changes: 42 additions & 1 deletion Sources/NIOCore/EventLoopFuture+AssumeIsolated.swift
< 8000 /tr>
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
/// domains.
///
/// Using this type relaxes the need to have the closures for ``EventLoop/execute(_:)``,
/// ``EventLoop/submit(_:)``, and ``EventLoop/scheduleTask(in:_:)`` to be `@Sendable`.
/// ``EventLoop/submit(_:)``, ``EventLoop/scheduleTask(in:_:)``,
/// and ``EventLoop/scheduleCallback(in:handler:)`` to be `@Sendable`.
public struct NIOIsolatedEventLoop {
@usableFromInline
let _wrapped: EventLoop
Expand Down Expand Up @@ -125,6 +126,46 @@ public struct NIOIsolatedEventLoop {
return .init(promise: promise, cancellationTask: { scheduled.cancel() })
}

/// Schedule a callback at a given time.
///
/// - Parameters:
/// - deadline: The instant in time before which the task will not execute.
/// - handler: The handler that defines the behavior of the callback when executed or canceled.
/// - Returns: A ``NIOScheduledCallback`` that can be used to cancel the scheduled callback.
@discardableResult
@available(*, noasync)
@inlinable
public func scheduleCallback(
at deadline: NIODeadline,
handler: some NIOScheduledCallbackHandler
) throws -> NIOScheduledCallback {
try self._wrapped._scheduleCallbackIsolatedUnsafeUnchecked(at: deadline, handler: handler)
}

/// Schedule a callback after given time.
///
/// - Parameters:
/// - amount: The amount of time before which the task will not execute.
/// - handler: The handler that defines the behavior of the callback when executed or canceled.
/// - Returns: A ``NIOScheduledCallback`` that can be used to cancel the scheduled callback.
@discardableResult
@available(*, noasync)
@inlinable
public func scheduleCallback(
in amount: TimeAmount,
handler: some NIOScheduledCallbackHandler
) throws -> NIOScheduledCallback {
try self._wrapped._scheduleCallbackIsolatedUnsafeUnchecked(in: amount, handler: handler)
}

/// Cancel a scheduled callback.
@inlinable
@available(*, noasync)
public func cancelScheduledCallback(_ scheduledCallback: NIOScheduledCallback) {
self._wrapped.preconditionInEventLoop()
self._wrapped.cancelScheduledCallback(scheduledCallback)
}

/// Creates and returns a new `EventLoopFuture` that is already marked as success. Notifications
/// will be done using this `EventLoop` as execution `NIOThread`.
///
Expand Down
22 changes: 22 additions & 0 deletions
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,25 @@ extension EventLoop {
}
}
}

@usableFromInline
struct LoopBoundScheduledCallbackHandlerWrapper<Handler: NIOScheduledCallbackHandler>:
NIOScheduledCallbackHandler, Sendable
{
private let box: NIOLoopBound<Handler>

@usableFromInline
init(wrapping handler: Handler, eventLoop: some EventLoop) {
self.box = .init(handler, eventLoop: eventLoop)
}

@usableFromInline
func handleScheduledCallback(eventLoop: some EventLoop) {
self.box.value.handleScheduledCallback(eventLoop: eventLoop)
}

@usableFromInline
func didCancelScheduledCallback(eventLoop: some EventLoop) {
self.box.value.didCancelScheduledCallback(eventLoop: eventLoop)
}
}
170 changes: 167 additions & 3 deletions Tests/NIOPosixTests/NIOScheduledCallbackTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,140 @@ final class NIOAsyncTestingEventLoopScheduledCallbackTests: _BaseScheduledCallba
}
}

final class IsolatedEventLoopScheduledCallbackTests: XCTestCase {
struct Requirements: ScheduledCallbackTestRequirements {
let _loop = EmbeddedEventLoop()
var loop: (any EventLoop) { self._loop }

func advanceTime(by amount: TimeAmount) {
self._loop.advanceTime(by: amount)
}

func shutdownEventLoop() {
try! self._loop.syncShutdownGracefully()
}

func waitForLoopTick() {}
}

var requirements: Requirements! = nil
var loop: (any EventLoop) { self.requirements.loop }

func advanceTime(by amount: TimeAmount) {
self.requirements.advanceTime(by: amount)
}

func shutdownEventLoop() {
self.requirements.shutdownEventLoop()
}

override func setUp() {
self.requirements = Requirements()
}

func testScheduledCallbackNotExecutedBeforeDeadline() throws {
let handler = NonSendableMockScheduledCallbackHandler()

_ = try self.loop.assumeIsolated().scheduleCallback(in: .milliseconds(1), handler: handler)
handler.assert(callbackCount: 0, cancelCount: 0)

self.advanceTime(by: .microseconds(1))
handler.assert(callbackCount: 0, cancelCount: 0)
}

func testScheduledCallbackExecutedAtDeadline() throws {
let handler = NonSendableMockScheduledCallbackHandler()

_ = try self.loop.assumeIsolated().scheduleCallback(in: .milliseconds(1), handler: handler)
self.advanceTime(by: .milliseconds(1))
handler.assert(callbackCount: 1, cancelCount: 0)
}

func testMultipleScheduledCallbacksUsingSameHandler() throws {
let handler = NonSendableMockScheduledCallbackHandler()

_ = try self.loop.assumeIsolated().scheduleCallback(in: .milliseconds(1), handler: handler)
_ = try self.loop.assumeIsolated().scheduleCallback(in: .milliseconds(1), handler: handler)

self.advanceTime(by: .milliseconds(1))
handler.assert(callbackCount: 2, cancelCount: 0)

_ = try self.loop.assumeIsolated().scheduleCallback(in: .milliseconds(2), handler: handler)
_ = try self.loop.assumeIsolated().scheduleCallback(in: .milliseconds(3), handler: handler)

self.advanceTime(by: .milliseconds(3))
handler.assert(callbackCount: 4, cancelCount: 0)
}

func testCancelExecutesCancellationCallback() throws {
let handler = NonSendableMockScheduledCallbackHandler()

let scheduledCallback = try self.loop.assumeIsolated().scheduleCallback(in: .milliseconds(1), handler: handler)
scheduledCallback.cancel()
handler.assert(callbackCount: 0, cancelCount: 1)
}

func testCancelAfterDeadlineDoesNotExecutesCancellationCallback() throws {
let handler = NonSendableMockScheduledCallbackHandler()

let scheduledCallback = try self.loop.assumeIsolated().scheduleCallback(in: .milliseconds(1), handler: handler)
self.advanceTime(by: .milliseconds(1))
scheduledCallback.cancel()
self.requirements.waitForLoopTick()
handler.assert(callbackCount: 1, cancelCount: 0)
}

func testCancelAfterCancelDoesNotCallCancellationCallbackAgain() throws {
let handler = NonSendableMockScheduledCallbackHandler()

let scheduledCallback = try self.loop.assumeIsolated().scheduleCallback(in: .milliseconds(1), handler: handler)
scheduledCallback.cancel()
scheduledCallback.cancel()
handler.assert(callbackCount: 0, cancelCount: 1)
}

func testCancelAfterShutdownDoesNotCallCancellationCallbackAgain() throws {
let handler = NonSendableMockScheduledCallbackHandler()

let scheduledCallback = try self.loop.assumeIsolated().scheduleCallback(in: .milliseconds(1), handler: handler)
self.shutdownEventLoop()
handler.assert(callbackCount: 0, cancelCount: 1)

scheduledCallback.cancel()
handler.assert(callbackCount: 0, cancelCount: 1)
}

func testShutdownCancelsOutstandingScheduledCallbacks() throws {
let handler = NonSendableMockScheduledCallbackHandler()

_ = try self.loop.assumeIsolated().scheduleCallback(in: .milliseconds(1), handler: handler)
self.shutdownEventLoop()
handler.assert(callbackCount: 0, cancelCount: 1)
}

func testShutdownDoesNotCancelCancelledCallbacksAgain() throws {
let handler = NonSendableMockScheduledCallbackHandler()

let handle = try self.loop.assumeIsolated().scheduleCallback(in: .milliseconds(1), handler: handler)
handle.cancel()
handler.assert(callbackCount: 0, cancelCount: 1)

self.shutdownEventLoop()
handler.assert(callbackCount: 0, cancelCount: 1)
}

func testShutdownDoesNotCancelPastCallbacks() throws {
let handler = NonSendableMockScheduledCallbackHandler()

_ = try self.loop.assumeIsolated().scheduleCallback(in: .milliseconds(1), handler: handler)
self.advanceTime(by: .milliseconds(1))
handler.assert(callbackCount: 1, cancelCount: 0)

self.shutdownEventLoop()
handler.assert(callbackCount: 1, cancelCount: 0)
}
}

class _BaseScheduledCallbackTests: XCTestCase {
// EL-specific test requirements.
var requirements: (any ScheduledCallbackTestRequirements)! = nil
Expand Down Expand Up @@ -114,7 +248,7 @@ extension _BaseScheduledCallbackTests {
handler.assert(callbackCount: 0, cancelCount: 0)
}

func testSheduledCallbackExecutedAtDeadline() async throws {
func testScheduledCallbackExecutedAtDeadline() async throws {
let handler = MockScheduledCallbackHandler()

_ = try self.loop.scheduleCallback(in: .milliseconds(1), handler: handler)
Expand All @@ -123,7 +257,7 @@ extension _BaseScheduledCallbackTests {
handler.assert(callbackCount: 1, cancelCount: 0)
}

func testMultipleSheduledCallbacksUsingSameHandler() async throws {
func testMultipleScheduledCallbacksUsingSameHandler() async throws {
let handler = MockScheduledCallbackHandler()

_ = try self.loop.scheduleCallback(in: .milliseconds(1), handler: handler)
Expand All @@ -143,7 +277,7 @@ extension _BaseScheduledCallbackTests {
handler.assert(callbackCount: 4, cancelCount: 0)
}

func testMultipleSheduledCallbacksUsingDifferentHandlers() async throws {
func testMultipleScheduledCallbacksUsingDifferentHandlers() async throws {
let handlerA = MockScheduledCallbackHandler()
let handlerB = MockScheduledCallbackHandler()

Expand Down Expand Up @@ -278,6 +412,36 @@ private final class MockScheduledCallbackHandler: NIOScheduledCallbackHandler, S
}
}

private final class NonSendableMockScheduledCallbackHandler: NIOScheduledCallbackHandler {
private(set) var callbackCount = 0
private(set) var cancelCount = 0

func handleScheduledCallback(eventLoop: some EventLoop) {
self.callbackCount += 1
}

func didCancelScheduledCallback(eventLoop: some EventLoop) {
self.cancelCount += 1
}

func assert(callbackCount: Int, cancelCount: Int, file: StaticString = #file, line: UInt = #line) {
XCTAssertEqual(
self.callbackCount,
callbackCount,
"Unexpected callback count",
file: file,
line: line
)
XCTAssertEqual(
self.cancelCount,
cancelCount,
"Unexpected cancel count",
file: file,
line: line
)
}
}

/// This function exists because there's no nice way of waiting in tests for something to happen in the handler
/// without an arbitrary sleep.
///
Expand Down
0