/src/swift-nio/Sources/NIOCore/NIOScheduledCallback.swift
Line | Count | Source |
1 | | //===----------------------------------------------------------------------===// |
2 | | // |
3 | | // This source file is part of the SwiftNIO open source project |
4 | | // |
5 | | // Copyright (c) 2024 Apple Inc. and the SwiftNIO project authors |
6 | | // Licensed under Apache License v2.0 |
7 | | // |
8 | | // See LICENSE.txt for license information |
9 | | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors |
10 | | // |
11 | | // SPDX-License-Identifier: Apache-2.0 |
12 | | // |
13 | | //===----------------------------------------------------------------------===// |
14 | | |
15 | | /// A type that handles callbacks scheduled with `EventLoop.scheduleCallback(at:handler:)`. |
16 | | /// |
17 | | /// - Seealso: `EventLoop.scheduleCallback(at:handler:)`. |
18 | | public protocol NIOScheduledCallbackHandler { |
19 | | /// This function is called at the scheduled time, unless the scheduled callback is cancelled. |
20 | | /// |
21 | | /// - Parameter eventLoop: The event loop on which the callback was scheduled. |
22 | | func handleScheduledCallback(eventLoop: some EventLoop) |
23 | | |
24 | | /// This function is called if the scheduled callback is cancelled. |
25 | | /// |
26 | | /// The callback could be cancelled explictily, by the user calling ``NIOScheduledCallback/cancel()``, or |
27 | | /// implicitly, if it was still pending when the event loop was shut down. |
28 | | /// |
29 | | /// - Parameter eventLoop: The event loop on which the callback was scheduled. |
30 | | func didCancelScheduledCallback(eventLoop: some EventLoop) |
31 | | } |
32 | | |
33 | | extension NIOScheduledCallbackHandler { |
34 | | /// Default implementation of `didCancelScheduledCallback(eventLoop:)`: does nothing. |
35 | 0 | public func didCancelScheduledCallback(eventLoop: some EventLoop) {} |
36 | | } |
37 | | |
38 | | /// An opaque handle that can be used to cancel a scheduled callback. |
39 | | /// |
40 | | /// Users should not create an instance of this type; it is returned by `EventLoop.scheduleCallback(at:handler:)`. |
41 | | /// |
42 | | /// - Seealso: `EventLoop.scheduleCallback(at:handler:)`. |
43 | | public struct NIOScheduledCallback: Sendable { |
44 | | @usableFromInline |
45 | | enum Backing: Sendable { |
46 | | /// A task created using `EventLoop.scheduleTask(deadline:_:)` by the default implementation. |
47 | | case `default`(_ task: Scheduled<Void>) |
48 | | /// A custom callback identifier, used by event loops that provide a custom implementation. |
49 | | case custom(id: UInt64) |
50 | | } |
51 | | |
52 | | @usableFromInline |
53 | | var eventLoop: any EventLoop |
54 | | |
55 | | @usableFromInline |
56 | | var backing: Backing |
57 | | |
58 | | /// This initializer is only for the default implementation and is fileprivate to avoid use in EL implementations. |
59 | 0 | fileprivate init(_ eventLoop: any EventLoop, _ task: Scheduled<Void>) { |
60 | 0 | self.eventLoop = eventLoop |
61 | 0 | self.backing = .default(task) |
62 | 0 | } |
63 | | |
64 | | /// Create a handle for the scheduled callback with an opaque identifier managed by the event loop. |
65 | | /// |
66 | | /// - NOTE: This initializer is for event loop implementors only, end users should use `EventLoop.scheduleCallback`. |
67 | | /// |
68 | | /// - Seealso: `EventLoop.scheduleCallback(at:handler:)`. |
69 | | @inlinable |
70 | 0 | public init(_ eventLoop: any EventLoop, id: UInt64) { |
71 | 0 | self.eventLoop = eventLoop |
72 | 0 | self.backing = .custom(id: id) |
73 | 0 | } |
74 | | |
75 | | /// Cancel the scheduled callback associated with this handle. |
76 | | @inlinable |
77 | 0 | public func cancel() { |
78 | 0 | self.eventLoop.cancelScheduledCallback(self) |
79 | 0 | } |
80 | | |
81 | | /// The callback identifier, if the event loop uses a custom scheduled callback implementation; nil otherwise. |
82 | | /// |
83 | | /// - NOTE: This property is for event loop implementors only. |
84 | | @inlinable |
85 | 0 | public var customCallbackID: UInt64? { |
86 | 0 | guard case .custom(let id) = self.backing else { return nil } |
87 | 0 | return id |
88 | 0 | } |
89 | | } |
90 | | |
91 | | extension EventLoop { |
92 | | @preconcurrency |
93 | | /// This method is not part of the public API |
94 | | /// |
95 | | /// Should use `package` not `public` but then it won't compile in |
96 | | /// Xcode 15.4 if you run `swift build --arch x86_64 --arch arm64`. |
97 | | public func _scheduleCallback( |
98 | | at deadline: NIODeadline, |
99 | | handler: some (NIOScheduledCallbackHandler & Sendable) |
100 | 0 | ) -> NIOScheduledCallback { |
101 | 0 | let task = self.scheduleTask(deadline: deadline) { handler.handleScheduledCallback(eventLoop: self) } |
102 | 0 | task.futureResult.whenFailure { error in |
103 | 0 | if case .cancelled = error as? EventLoopError { |
104 | 0 | handler.didCancelScheduledCallback(eventLoop: self) |
105 | 0 | } |
106 | 0 | } |
107 | 0 | return NIOScheduledCallback(self, task) |
108 | 0 | } |
109 | | |
110 | | /// Default implementation of `scheduleCallback(at deadline:handler:)`: backed by `EventLoop.scheduleTask`. |
111 | | /// |
112 | | /// Ideally the scheduled callback handler should be called exactly once for each call to `scheduleCallback`: |
113 | | /// either the callback handler, or the cancellation handler. |
114 | | /// |
115 | | /// In order to support cancellation in the default implementation, we hook the future of the scheduled task |
116 | | /// backing the scheduled callback. This requires two calls to the event loop: `EventLoop.scheduleTask`, and |
117 | | /// `EventLoopFuture.whenFailure`, both of which queue onto the event loop if called from off the event loop. |
118 | | /// |
119 | | /// This can present a challenge during event loop shutdown, where typically: |
120 | | /// 1. Scheduled work that is past its deadline gets run. |
121 | | /// 2. Scheduled future work gets cancelled. |
122 | | /// 3. New work resulting from (1) and (2) gets handled differently depending on the EL: |
123 | | /// a. `SelectableEventLoop` runs new work recursively and crashes if not quiesced in some number of ticks. |
124 | | /// b. `EmbeddedEventLoop` and `NIOAsyncTestingEventLoop` will fail incoming work. |
125 | | /// |
126 | | /// `SelectableEventLoop` has a custom implementation for scheduled callbacks so warrants no further discussion. |
127 | | /// |
128 | | /// As a practical matter, the `EmbeddedEventLoop` is OK because it shares the thread of the caller, but for |
129 | | /// other event loops (including any outside this repo), it's possible that the call to shutdown interleaves |
130 | | /// with the call to create the scheduled task and the call to hook the task future. |
131 | | /// |
132 | | /// Because this API is synchronous and we cannot block the calling thread, users of event loops with this |
133 | | /// default implementation will have cancellation callbacks delivered on a best-effort basis when the event loop |
134 | | /// is shutdown and depends on how the event loop deals with newly scheduled tasks during shutdown. |
135 | | /// |
136 | | /// The implementation of this default conformance has been further factored out so we can use it in |
137 | | /// `NIOAsyncTestingEventLoop`, where the use of `wait()` is _less bad_. |
138 | | @preconcurrency |
139 | | @discardableResult |
140 | | public func scheduleCallback( |
141 | | at deadline: NIODeadline, |
142 | | handler: some (NIOScheduledCallbackHandler & Sendable) |
143 | 0 | ) -> NIOScheduledCallback { |
144 | 0 | self._scheduleCallback(at: deadline, handler: handler) |
145 | 0 | } |
146 | | |
147 | | /// Default implementation of `scheduleCallback(in amount:handler:)`: calls `scheduleCallback(at deadline:handler:)`. |
148 | | @preconcurrency |
149 | | @discardableResult |
150 | | @inlinable |
151 | | public func scheduleCallback( |
152 | | in amount: TimeAmount, |
153 | | handler: some (NIOScheduledCallbackHandler & Sendable) |
154 | 0 | ) throws -> NIOScheduledCallback { |
155 | 0 | try self.scheduleCallback(at: .now() + amount, handler: handler) |
156 | 0 | } |
157 | | |
158 | | /// Default implementation of `cancelScheduledCallback(_:)`: only cancels callbacks scheduled by the default implementation of `scheduleCallback`. |
159 | | /// |
160 | | /// - NOTE: Event loops that provide a custom scheduled callback implementation **must** implement _both_ |
161 | | /// `sheduleCallback(at deadline:handler:)` _and_ `cancelScheduledCallback(_:)`. Failure to do so will |
162 | | /// result in a runtime error. |
163 | | @inlinable |
164 | 0 | public func cancelScheduledCallback(_ scheduledCallback: NIOScheduledCallback) { |
165 | 0 | switch scheduledCallback.backing { |
166 | 0 | case .default(let task): |
167 | 0 | task.cancel() |
168 | 0 | case .custom: |
169 | 0 | preconditionFailure("EventLoop missing custom implementation of cancelScheduledCallback(_:)") |
170 | 0 | } |
171 | 0 | } |
172 | | } |
173 | | |
174 | | @usableFromInline |
175 | | struct LoopBoundScheduledCallbackHandlerWrapper<Handler: NIOScheduledCallbackHandler>: |
176 | | NIOScheduledCallbackHandler, Sendable |
177 | | { |
178 | | private let box: NIOLoopBound<Handler> |
179 | | |
180 | | @usableFromInline |
181 | 0 | init(wrapping handler: Handler, eventLoop: some EventLoop) { |
182 | 0 | self.box = .init(handler, eventLoop: eventLoop) |
183 | 0 | } |
184 | | |
185 | | @usableFromInline |
186 | 0 | func handleScheduledCallback(eventLoop: some EventLoop) { |
187 | 0 | self.box.value.handleScheduledCallback(eventLoop: eventLoop) |
188 | 0 | } |
189 | | |
190 | | @usableFromInline |
191 | 0 | func didCancelScheduledCallback(eventLoop: some EventLoop) { |
192 | 0 | self.box.value.didCancelScheduledCallback(eventLoop: eventLoop) |
193 | 0 | } |
194 | | } |