├── .editorconfig ├── .gitignore ├── .spi.yml ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── Deadline │ └── Deadline.swift └── Tests └── DeadlineTests ├── DeadlineTests.swift └── TestClock.swift /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /.swiftpm 4 | /Packages 5 | /*.xcodeproj 6 | xcuserdata/ 7 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [Deadline] 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Philipp Gabriel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "swift-concurrency-deadline", 6 | platforms: [.iOS(.v16), .macOS(.v13), .macCatalyst(.v16), .tvOS(.v16), .watchOS(.v9), .visionOS(.v1)], 7 | products: [ 8 | .library(name: "Deadline", targets: ["Deadline"]), 9 | ], 10 | targets: [ 11 | .target( 12 | name: "Deadline" 13 | ), 14 | .testTarget( 15 | name: "DeadlineTests", 16 | dependencies: ["Deadline"] 17 | ) 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deadline 2 | A deadline algorithm for Swift Concurrency. 3 | 4 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fph1ps%2Fswift-concurrency-deadline%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/ph1ps/swift-concurrency-deadline) 5 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fph1ps%2Fswift-concurrency-deadline%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/ph1ps/swift-concurrency-deadline) 6 | 7 | ## Rationale 8 | As I've previously stated on the [Swift forums](https://forums.swift.org/t/my-experience-with-concurrency/73197): in my opinion deadlines or timeouts are a missing piece in Swift's Concurrency system. Since this algorithm is not easy to get right I decided to open-source my implementation. 9 | 10 | ## Details 11 | 12 | The library comes with two free functions, one with a generic clock and another one which uses the `ContinuousClock` as default. 13 | ```swift 14 | public func deadline( 15 | until instant: C.Instant, 16 | tolerance: C.Instant.Duration? = nil, 17 | clock: C, 18 | isolation: isolated (any Actor)? = #isolation, 19 | operation: @Sendable () async throws -> R 20 | ) async throws -> R where C: Clock, R: Sendable { ... } 21 | 22 | public func deadline( 23 | until instant: ContinuousClock.Instant, 24 | tolerance: ContinuousClock.Instant.Duration? = nil, 25 | isolation: isolated (any Actor)? = #isolation, 26 | operation: @Sendable () async throws -> R 27 | ) async throws -> R where R: Sendable { ... } 28 | ``` 29 | 30 | This function provides a mechanism for enforcing timeouts on asynchronous operations that lack native deadline support. It creates a `TaskGroup` with two concurrent tasks: the provided operation and a sleep task. 31 | 32 | - Parameters: 33 | - `instant`: The absolute deadline for the operation to complete. 34 | - `tolerance`: The allowed tolerance for the deadline. 35 | - `clock`: The clock used for timing the operation. 36 | - `isolation`: The isolation passed on to the task group. 37 | - `operation`: The asynchronous operation to be executed. 38 | 39 | - Returns: The result of the operation if it completes before the deadline. 40 | - Throws: `DeadlineExceededError`, if the operation fails to complete before the deadline and errors thrown by the operation or clock. 41 | 42 | > [!CAUTION] 43 | > The operation closure must support cooperative cancellation. Otherwise, the deadline will not be respected. 44 | 45 | ### Examples 46 | To fully understand this, let's illustrate the 3 outcomes of this function: 47 | 48 | #### Outcome 1 49 | The operation finishes in time: 50 | ```swift 51 | let result = try await deadline(until: .now + .seconds(5)) { 52 | // Simulate long running task 53 | try await Task.sleep(for: .seconds(1)) 54 | return "success" 55 | } 56 | ``` 57 | As you'd expect, result will be "success". The same applies when your operation fails in time: 58 | ```swift 59 | let result = try await deadline(until: .now + .seconds(5)) { 60 | // Simulate long running task 61 | try await Task.sleep(for: .seconds(1)) 62 | throw CustomError() 63 | } 64 | ``` 65 | This will throw `CustomError`. 66 | 67 | #### Outcome 2 68 | The operation does not finish in time: 69 | ```swift 70 | let result = try await deadline(until: .now + .seconds(1)) { 71 | // Simulate even longer running task 72 | try await Task.sleep(for: .seconds(5)) 73 | return "success" 74 | } 75 | ``` 76 | This will throw `DeadlineExceededError` because the operation will not finish in time. 77 | 78 | #### Outcome 3 79 | The parent task was cancelled: 80 | ```swift 81 | let task = Task { 82 | do { 83 | try await deadline(until: .now + .seconds(5)) { 84 | try await URLSession.shared.data(from: url) 85 | } 86 | } catch { 87 | print(error) 88 | } 89 | } 90 | 91 | task.cancel() 92 | ``` 93 | The print is guaranteed to print `URLError(.cancelled)`. 94 | 95 | ## Improvements 96 | - Only have one free function with a default expression of `ContinuousClock` for the `clock` parameter. 97 | - Blocked by: https://github.com/swiftlang/swift/issues/72199 98 | - Use `@isolated(any)` for synchronous task enqueueing support. 99 | - Blocked by: https://github.com/swiftlang/swift/issues/76604 100 | -------------------------------------------------------------------------------- /Sources/Deadline/Deadline.swift: -------------------------------------------------------------------------------- 1 | enum DeadlineState: Sendable where T: Sendable { 2 | case operationResult(Result) 3 | case sleepResult(Result) 4 | } 5 | 6 | /// An error indicating that the deadline has passed and the operation did not complete. 7 | public struct DeadlineExceededError: Error { } 8 | 9 | /// Race the given operation against a deadline. 10 | /// 11 | /// This function provides a mechanism for enforcing timeouts on asynchronous operations that lack native deadline support. It creates a `TaskGroup` with two concurrent tasks: the provided operation and a sleep task. 12 | /// 13 | /// - Parameters: 14 | /// - instant: The absolute deadline for the operation to complete. 15 | /// - tolerance: The allowed tolerance for the deadline. 16 | /// - clock: The clock used for timing the operation. 17 | /// - isolation: The isolation passed on to the task group. 18 | /// - operation: The asynchronous operation to be executed. 19 | /// 20 | /// - Returns: The result of the operation if it completes before the deadline. 21 | /// - Throws: `DeadlineExceededError`, if the operation fails to complete before the deadline and errors thrown by the operation or clock. 22 | /// 23 | /// ## Examples 24 | /// To fully understand this, let's illustrate the 3 outcomes of this function: 25 | /// 26 | /// ### Outcome 1 27 | /// The operation finishes in time: 28 | /// ```swift 29 | /// let result = try await deadline(until: .now + .seconds(5)) { 30 | /// // Simulate long running task 31 | /// try await Task.sleep(for: .seconds(1)) 32 | /// return "success" 33 | /// } 34 | /// ``` 35 | /// As you'd expect, result will be "success". The same applies when your operation fails in time: 36 | /// ```swift 37 | /// let result = try await deadline(until: .now + .seconds(5)) { 38 | /// // Simulate long running task 39 | /// try await Task.sleep(for: .seconds(1)) 40 | /// throw CustomError() 41 | /// } 42 | /// ``` 43 | /// This will throw `CustomError`. 44 | /// 45 | /// ## Outcome 2 46 | /// The operation does not finish in time: 47 | /// ```swift 48 | /// let result = try await deadline(until: .now + .seconds(1)) { 49 | /// // Simulate even longer running task 50 | /// try await Task.sleep(for: .seconds(5)) 51 | /// return "success" 52 | /// } 53 | /// ``` 54 | /// This will throw `DeadlineExceededError` because the operation will not finish in time. 55 | /// 56 | /// ## Outcome 3 57 | /// The parent task was cancelled: 58 | /// ```swift 59 | /// let task = Task { 60 | /// do { 61 | /// try await deadline(until: .now + .seconds(5)) { 62 | /// try await URLSession.shared.data(from: url) 63 | /// } 64 | /// } catch { 65 | /// print(error) 66 | /// } 67 | /// } 68 | /// 69 | /// task.cancel() 70 | /// ``` 71 | /// The print is guaranteed to print `URLError(.cancelled)`. 72 | /// - Important: The operation closure must support cooperative cancellation. Otherwise, the deadline will not be respected. 73 | public func deadline( 74 | until instant: C.Instant, 75 | tolerance: C.Instant.Duration? = nil, 76 | clock: C, 77 | isolation: isolated (any Actor)? = #isolation, 78 | operation: @Sendable () async throws -> R 79 | ) async throws -> R where C: Clock, R: Sendable { 80 | 81 | // NB: This is safe to use, because the closure will not escape the context of this function. 82 | let result = await withoutActuallyEscaping(operation) { operation in 83 | await withTaskGroup( 84 | of: DeadlineState.self, 85 | returning: Result.self, 86 | isolation: isolation 87 | ) { taskGroup in 88 | 89 | taskGroup.addTask { 90 | do { 91 | let result = try await operation() 92 | return .operationResult(.success(result)) 93 | } catch { 94 | return .operationResult(.failure(error)) 95 | } 96 | } 97 | 98 | taskGroup.addTask { 99 | do { 100 | try await Task.sleep(until: instant, tolerance: tolerance, clock: clock) 101 | return .sleepResult(.success(false)) 102 | } catch where Task.isCancelled { 103 | return .sleepResult(.success(true)) 104 | } catch { 105 | return .sleepResult(.failure(error)) 106 | } 107 | } 108 | 109 | defer { 110 | taskGroup.cancelAll() 111 | } 112 | 113 | for await next in taskGroup { 114 | switch next { 115 | case .operationResult(let result): 116 | return result 117 | case .sleepResult(.success(false)): 118 | return .failure(DeadlineExceededError()) 119 | case .sleepResult(.success(true)): 120 | continue 121 | case .sleepResult(.failure(let error)): 122 | return .failure(error) 123 | } 124 | } 125 | 126 | preconditionFailure("Invalid state") 127 | } 128 | } 129 | 130 | return try result.get() 131 | } 132 | 133 | /// Race the given operation against a deadline. 134 | /// 135 | /// This function provides a mechanism for enforcing timeouts on asynchronous operations that lack native deadline support. It creates a `TaskGroup` with two concurrent tasks: the provided operation and a sleep task. 136 | /// `ContinuousClock` will be used as the default clock. 137 | /// 138 | /// - Parameters: 139 | /// - instant: The absolute deadline for the operation to complete. 140 | /// - tolerance: The allowed tolerance for the deadline. 141 | /// - isolation: The isolation passed on to the task group. 142 | /// - operation: The asynchronous operation to be executed. 143 | /// 144 | /// - Returns: The result of the operation if it completes before the deadline. 145 | /// - Throws: `DeadlineExceededError`, if the operation fails to complete before the deadline and errors thrown by the operation or clock. 146 | /// 147 | /// ## Examples 148 | /// To fully understand this, let's illustrate the 3 outcomes of this function: 149 | /// 150 | /// ### Outcome 1 151 | /// The operation finishes in time: 152 | /// ```swift 153 | /// let result = try await deadline(until: .now + .seconds(5)) { 154 | /// // Simulate long running task 155 | /// try await Task.sleep(for: .seconds(1)) 156 | /// return "success" 157 | /// } 158 | /// ``` 159 | /// As you'd expect, result will be "success". The same applies when your operation fails in time: 160 | /// ```swift 161 | /// let result = try await deadline(until: .now + .seconds(5)) { 162 | /// // Simulate long running task 163 | /// try await Task.sleep(for: .seconds(1)) 164 | /// throw CustomError() 165 | /// } 166 | /// ``` 167 | /// This will throw `CustomError`. 168 | /// 169 | /// ## Outcome 2 170 | /// The operation does not finish in time: 171 | /// ```swift 172 | /// let result = try await deadline(until: .now + .seconds(1)) { 173 | /// // Simulate even longer running task 174 | /// try await Task.sleep(for: .seconds(5)) 175 | /// return "success" 176 | /// } 177 | /// ``` 178 | /// This will throw `DeadlineExceededError` because the operation will not finish in time. 179 | /// 180 | /// ## Outcome 3 181 | /// The parent task was cancelled: 182 | /// ```swift 183 | /// let task = Task { 184 | /// do { 185 | /// try await deadline(until: .now + .seconds(5)) { 186 | /// try await URLSession.shared.data(from: url) 187 | /// } 188 | /// } catch { 189 | /// print(error) 190 | /// } 191 | /// } 192 | /// 193 | /// task.cancel() 194 | /// ``` 195 | /// The print is guaranteed to print `URLError(.cancelled)`. 196 | /// - Important: The operation closure must support cooperative cancellation. Otherwise, the deadline will not be respected. 197 | public func deadline( 198 | until instant: ContinuousClock.Instant, 199 | tolerance: ContinuousClock.Instant.Duration? = nil, 200 | isolation: isolated (any Actor)? = #isolation, 201 | operation: @Sendable () async throws -> R 202 | ) async throws -> R where R: Sendable { 203 | try await deadline(until: instant, tolerance: tolerance, clock: ContinuousClock(), isolation: isolation, operation: operation) 204 | } 205 | -------------------------------------------------------------------------------- /Tests/DeadlineTests/DeadlineTests.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Testing) 2 | import Deadline 3 | import Testing 4 | 5 | @Test func testInTime() async { 6 | 7 | let testClock = TestClock() 8 | let task = Task { 9 | try await deadline(until: .init(offset: .milliseconds(200)), clock: testClock) { 10 | try await testClock.sleep(until: .init(offset: .milliseconds(100))) 11 | } 12 | } 13 | 14 | await testClock.advance(by: .milliseconds(200)) 15 | await #expect(throws: Never.self) { 16 | try await task.value 17 | } 18 | } 19 | 20 | @Test func testDeadline() async { 21 | 22 | let testClock = TestClock() 23 | let task = Task { 24 | try await deadline(until: .init(offset: .milliseconds(100)), clock: testClock) { 25 | try await testClock.sleep(until: .init(offset: .milliseconds(200))) 26 | } 27 | } 28 | 29 | await testClock.advance(by: .milliseconds(200)) 30 | await #expect(throws: DeadlineExceededError.self) { 31 | try await task.value 32 | } 33 | } 34 | 35 | @Test func testCancellation() async { 36 | 37 | struct CustomError: Error { } 38 | 39 | let testClock = TestClock() 40 | let task = Task { 41 | try await deadline(until: .init(offset: .milliseconds(100)), clock: testClock) { 42 | do { 43 | try await testClock.sleep(until: .init(offset: .milliseconds(200))) 44 | } catch { 45 | throw CustomError() 46 | } 47 | } 48 | } 49 | 50 | await testClock.advance(by: .milliseconds(50)) 51 | task.cancel() 52 | 53 | await #expect(throws: CustomError.self) { 54 | try await task.value 55 | } 56 | } 57 | 58 | @Test func testEarlyCancellation() async { 59 | 60 | struct CustomError: Error { } 61 | 62 | let testClock = TestClock() 63 | let task = Task { 64 | try await deadline(until: .init(offset: .milliseconds(100)), clock: testClock) { 65 | do { 66 | try await testClock.sleep(until: .init(offset: .milliseconds(200))) 67 | } catch { 68 | throw CustomError() 69 | } 70 | } 71 | } 72 | 73 | task.cancel() 74 | 75 | await #expect(throws: CustomError.self) { 76 | try await task.value 77 | } 78 | } 79 | 80 | @Test func testFailingClock() async { 81 | 82 | struct CustomError: Error { } 83 | struct CustomClock: Clock { 84 | var now: ContinuousClock.Instant { fatalError() } 85 | var minimumResolution: ContinuousClock.Duration { fatalError() } 86 | func sleep(until deadline: Instant, tolerance: Duration? = nil) async throws { throw CustomError() } 87 | } 88 | 89 | let customClock = CustomClock() 90 | let testClock = TestClock() 91 | let task = Task { 92 | try await deadline(until: .now.advanced(by: .milliseconds(200)), clock: customClock) { 93 | try await testClock.sleep(until: .init(offset: .milliseconds(100))) 94 | } 95 | } 96 | 97 | await #expect(throws: CustomError.self) { 98 | try await task.value 99 | } 100 | } 101 | #endif 102 | -------------------------------------------------------------------------------- /Tests/DeadlineTests/TestClock.swift: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2023 Point-Free 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | #if canImport(Testing) 24 | import Testing 25 | import Foundation 26 | 27 | final class TestClock: Clock, @unchecked Sendable { 28 | struct Instant: InstantProtocol { 29 | let offset: Duration 30 | 31 | init(offset: Duration = .zero) { 32 | self.offset = offset 33 | } 34 | 35 | func advanced(by duration: Duration) -> Self { 36 | .init(offset: self.offset + duration) 37 | } 38 | 39 | func duration(to other: Self) -> Duration { 40 | other.offset - self.offset 41 | } 42 | 43 | static func < (lhs: Self, rhs: Self) -> Bool { 44 | lhs.offset < rhs.offset 45 | } 46 | } 47 | 48 | var minimumResolution: Duration = .zero 49 | var now: Instant 50 | 51 | let lock = NSRecursiveLock() 52 | var suspensions: 53 | [( 54 | id: UUID, 55 | deadline: Instant, 56 | continuation: AsyncThrowingStream.Continuation 57 | )] = [] 58 | 59 | init(now: Instant = .init()) { 60 | self.now = now 61 | } 62 | 63 | func sleep(until deadline: Instant, tolerance: Duration? = nil) async throws { 64 | try Task.checkCancellation() 65 | let id = UUID() 66 | do { 67 | let stream: AsyncThrowingStream? = self.lock.sync { 68 | guard deadline >= self.now 69 | else { 70 | return nil 71 | } 72 | return AsyncThrowingStream { continuation in 73 | self.suspensions.append((id: id, deadline: deadline, continuation: continuation)) 74 | } 75 | } 76 | guard let stream = stream 77 | else { return } 78 | for try await _ in stream {} 79 | try Task.checkCancellation() 80 | } catch is CancellationError { 81 | self.lock.sync { self.suspensions.removeAll(where: { $0.id == id }) } 82 | throw CancellationError() 83 | } catch { 84 | throw error 85 | } 86 | } 87 | 88 | func checkSuspension() async throws { 89 | await Task.megaYield() 90 | guard self.lock.sync(operation: { self.suspensions.isEmpty }) 91 | else { throw SuspensionError() } 92 | } 93 | 94 | func advance(by duration: Duration = .zero) async { 95 | await self.advance(to: self.lock.sync(operation: { self.now.advanced(by: duration) })) 96 | } 97 | 98 | func advance(to deadline: Instant) async { 99 | while self.lock.sync(operation: { self.now <= deadline }) { 100 | await Task.megaYield() 101 | let `return` = { 102 | self.lock.lock() 103 | self.suspensions.sort { $0.deadline < $1.deadline } 104 | 105 | guard 106 | let next = self.suspensions.first, 107 | deadline >= next.deadline 108 | else { 109 | self.now = deadline 110 | self.lock.unlock() 111 | return true 112 | } 113 | 114 | self.now = next.deadline 115 | self.suspensions.removeFirst() 116 | self.lock.unlock() 117 | next.continuation.finish() 118 | return false 119 | }() 120 | 121 | if `return` { 122 | await Task.megaYield() 123 | return 124 | } 125 | } 126 | await Task.megaYield() 127 | } 128 | 129 | func run( 130 | timeout duration: Swift.Duration = .milliseconds(500), 131 | fileID: StaticString = #fileID, 132 | filePath: StaticString = #filePath, 133 | line: UInt = #line, 134 | column: UInt = #column 135 | ) async { 136 | do { 137 | try await withThrowingTaskGroup(of: Void.self) { group in 138 | group.addTask { 139 | try await Task.sleep(until: .now.advanced(by: duration), clock: .continuous) 140 | for suspension in self.suspensions { 141 | suspension.continuation.finish(throwing: CancellationError()) 142 | } 143 | throw CancellationError() 144 | } 145 | group.addTask { 146 | await Task.megaYield() 147 | while let deadline = self.lock.sync(operation: { self.suspensions.first?.deadline }) { 148 | try Task.checkCancellation() 149 | await self.advance(by: self.lock.sync(operation: { self.now.duration(to: deadline) })) 150 | } 151 | } 152 | try await group.next() 153 | group.cancelAll() 154 | } 155 | } catch { 156 | Issue.record( 157 | """ 158 | Expected all sleeps to finish, but some are still suspending after \(duration). 159 | 160 | There are sleeps suspending. This could mean you are not advancing the test clock far \ 161 | enough for your feature to execute its logic, or there could be a bug in your feature's \ 162 | logic. 163 | 164 | You can also increase the timeout of 'run' to be greater than \(duration). 165 | """ 166 | ) 167 | } 168 | } 169 | } 170 | 171 | struct SuspensionError: Error {} 172 | 173 | extension Task where Success == Never, Failure == Never { 174 | static func megaYield(count: Int = 20) async { 175 | for _ in 0...detached(priority: .background) { await Task.yield() }.value 177 | } 178 | } 179 | } 180 | 181 | extension NSRecursiveLock { 182 | @inlinable 183 | @discardableResult 184 | func sync(operation: () -> R) -> R { 185 | self.lock() 186 | defer { self.unlock() } 187 | return operation() 188 | } 189 | } 190 | 191 | extension TestClock where Duration == Swift.Duration { 192 | convenience init() { 193 | self.init(now: .init()) 194 | } 195 | } 196 | #endif 197 | --------------------------------------------------------------------------------