Skip to content

Commit c7c830a

Browse files
weissiLukasa
authored andcommitted
EventLoopFuture.waitSpinningRunLoop() (#2985)
### Motivation: In some (probably niche) scenarios, especially in pre-Concurrency UI applications on Darwin, it can be useful to wait for an a value whilst still running the current `RunLoop`. That allows the UI and other things to work whilst we're waiting for a future to complete. ### Modifications: - Add `NIOFoundationCompat.EventLoopFuture.waitSpinningRunLoop()`. ### Result: Better compatibility with Cocoa. (cherry picked from commit 2a8811a)
1 parent 61ee0e9 commit c7c830a

File tree

2 files changed

+136
-0
lines changed

2 files changed

+136
-0
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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+
import Atomics
16+
import Foundation
17+
import NIOConcurrencyHelpers
18+
import NIOCore
19+
20+
extension EventLoopFuture {
21+
/// Wait for the resolution of this `EventLoopFuture` by spinning `RunLoop.current` in `mode` until the future
22+
/// resolves. The calling thread will be blocked albeit running `RunLoop.current`.
23+
///
24+
/// If the `EventLoopFuture` resolves with a value, that value is returned from `waitSpinningRunLoop()`. If
25+
/// the `EventLoopFuture` resolves with an error, that error will be thrown instead.
26+
/// `waitSpinningRunLoop()` will block whatever thread it is called on, so it must not be called on event loop
27+
/// threads: it is primarily useful for testing, or for building interfaces between blocking
28+
/// and non-blocking code.
29+
///
30+
/// This is also forbidden in async contexts: prefer `EventLoopFuture/get()`.
31+
///
32+
/// - Note: The `Value` must be `Sendable` since it is shared outside of the isolation domain of the event loop.
33+
///
34+
/// - Returns: The value of the `EventLoopFuture` when it completes.
35+
/// - Throws: The error value of the `EventLoopFuture` if it errors.
36+
@available(*, noasync, message: "waitSpinningRunLoop() can block indefinitely, prefer get()", renamed: "get()")
37+
@inlinable
38+
public func waitSpinningRunLoop(
39+
inMode mode: RunLoop.Mode = .default,
40+
file: StaticString = #file,
41+
line: UInt = #line
42+
) throws -> Value where Value: Sendable {
43+
try self._blockingWaitForFutureCompletion(mode: mode, file: file, line: line)
44+
}
45+
46+
@inlinable
47+
@inline(never)
48+
func _blockingWaitForFutureCompletion(
49+
mode: RunLoop.Mode,
50+
file: StaticString,
51+
line: UInt
52+
) throws -> Value where Value: Sendable {
53+
self.eventLoop._preconditionSafeToWait(file: file, line: line)
54+
55+
let runLoop = RunLoop.current
56+
57+
let value: NIOLockedValueBox<Result<Value, any Error>?> = NIOLockedValueBox(nil)
58+
self.whenComplete { result in
59+
value.withLockedValue { value in
60+
value = result
61+
}
62+
}
63+
64+
while value.withLockedValue({ $0 }) == nil {
65+
_ = runLoop.run(mode: mode, before: Date().addingTimeInterval(0.01))
66+
}
67+
68+
return try value.withLockedValue { value in
69+
try value!.get()
70+
}
71+
}
72+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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+
import NIO
16+
import NIOFoundationCompat
17+
import XCTest
18+
19+
final class WaitSpinningRunLoopTests: XCTestCase {
20+
private let loop = MultiThreadedEventLoopGroup.singleton.any()
21+
22+
func testPreFailedWorks() {
23+
struct Dummy: Error {}
24+
let future: EventLoopFuture<Never> = self.loop.makeFailedFuture(Dummy())
25+
XCTAssertThrowsError(try future.waitSpinningRunLoop()) { error in
26+
XCTAssert(error is Dummy)
27+
}
28+
}
29+
30+
func testPreSucceededWorks() {
31+
let future = self.loop.makeSucceededFuture("hello")
32+
XCTAssertEqual("hello", try future.waitSpinningRunLoop())
33+
}
34+
35+
func testFailingAfterALittleWhileWorks() {
36+
struct Dummy: Error {}
37+
let future: EventLoopFuture<Never> = self.loop.scheduleTask(in: .milliseconds(10)) {
38+
throw Dummy()
39+
}.futureResult
40+
XCTAssertThrowsError(try future.waitSpinningRunLoop()) { error in
41+
XCTAssert(error is Dummy)
42+
}
43+
}
44+
45+
func testSucceedingAfterALittleWhileWorks() {
46+
let future = self.loop.scheduleTask(in: .milliseconds(10)) {
47+
"hello"
48+
}.futureResult
49+
XCTAssertEqual("hello", try future.waitSpinningRunLoop())
50+
}
51+
52+
func testWeCanStillUseOurRunLoopWhilstBlocking() {
53+
let promise = self.loop.makePromise(of: String.self)
54+
let myRunLoop = RunLoop.current
55+
let timer = Timer(timeInterval: 0.1, repeats: false) { [loop = self.loop] _ in
56+
loop.scheduleTask(in: .microseconds(10)) {
57+
promise.succeed("hello")
58+
}
59+
}
60+
myRunLoop.add(timer, forMode: .default)
61+
XCTAssertEqual("hello", try promise.futureResult.waitSpinningRunLoop())
62+
}
63+
64+
}

0 commit comments

Comments
 (0)