Coverage Report

Created: 2026-06-01 06:32

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}