Skip to content

Commit

Permalink
EventLoopFuture.waitSpinningRunLoop() (#2985)
Browse files Browse the repository at this point in the history
### 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.
  • Loading branch information
weissi authored Nov 21, 2024
1 parent 74cf44e commit 2a8811a
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 0 deletions.
72 changes: 72 additions & 0 deletions Sources/NIOFoundationCompat/WaitSpinningRunLoop.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2024 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Atomics
import Foundation
import NIOConcurrencyHelpers
import NIOCore

extension EventLoopFuture {
/// Wait for the resolution of this `EventLoopFuture` by spinning `RunLoop.current` in `mode` until the future
/// resolves. The calling thread will be blocked albeit running `RunLoop.current`.
///
/// If the `EventLoopFuture` resolves with a value, that value is returned from `waitSpinningRunLoop()`. If
/// the `EventLoopFuture` resolves with an error, that error will be thrown instead.
/// `waitSpinningRunLoop()` will block whatever thread it is called on, so it must not be called on event loop
/// threads: it is primarily useful for testing, or for building interfaces between blocking
/// and non-blocking code.
///
/// This is also forbidden in async contexts: prefer `EventLoopFuture/get()`.
///
/// - Note: The `Value` must be `Sendable` since it is shared outside of the isolation domain of the event loop.
///
/// - Returns: The value of the `EventLoopFuture` when it completes.
/// - Throws: The error value of the `EventLoopFuture` if it errors.
@available(*, noasync, message: "waitSpinningRunLoop() can block indefinitely, prefer get()", renamed: "get()")
@inlinable
public func waitSpinningRunLoop(
inMode mode: RunLoop.Mode = .default,
file: StaticString = #file,
line: UInt = #line
) throws -> Value where Value: Sendable {
try self._blockingWaitForFutureCompletion(mode: mode, file: file, line: line)
}

@inlinable
@inline(never)
func _blockingWaitForFutureCompletion(
mode: RunLoop.Mode,
file: StaticString,
line: UInt
) throws -> Value where Value: Sendable {
self.eventLoop._preconditionSafeToWait(file: file, line: line)

let runLoop = RunLoop.current

let value: NIOLockedValueBox<Result<Value, any Error>?> = NIOLockedValueBox(nil)
self.whenComplete { result in
value.withLockedValue { value in
value = result
}
}

while value.withLockedValue({ $0 }) == nil {
_ = runLoop.run(mode: mode, before: Date().addingTimeInterval(0.01))
}

return try value.withLockedValue { value in
try value!.get()
}
}
}
64 changes: 64 additions & 0 deletions Tests/NIOFoundationCompatTests/WaitSpinningRunLoopTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2024 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import NIO
import NIOFoundationCompat
import XCTest

final class WaitSpinningRunLoopTests: XCTestCase {
private let loop = MultiThreadedEventLoopGroup.singleton.any()

func testPreFailedWorks() {
struct Dummy: Error {}
let future: EventLoopFuture<Never> = self.loop.makeFailedFuture(Dummy())
XCTAssertThrowsError(try future.waitSpinningRunLoop()) { error in
XCTAssert(error is Dummy)
}
}

func testPreSucceededWorks() {
let future = self.loop.makeSucceededFuture("hello")
XCTAssertEqual("hello", try future.waitSpinningRunLoop())
}

func testFailingAfterALittleWhileWorks() {
struct Dummy: Error {}
let future: EventLoopFuture<Never> = self.loop.scheduleTask(in: .milliseconds(10)) {
throw Dummy()
}.futureResult
XCTAssertThrowsError(try future.waitSpinningRunLoop()) { error in
XCTAssert(error is Dummy)
}
}

func testSucceedingAfterALittleWhileWorks() {
let future = self.loop.scheduleTask(in: .milliseconds(10)) {
"hello"
}.futureResult
XCTAssertEqual("hello", try future.waitSpinningRunLoop())
}

func testWeCanStillUseOurRunLoopWhilstBlocking() {
let promise = self.loop.makePromise(of: String.self)
let myRunLoop = RunLoop.current
let timer = Timer(timeInterval: 0.1, repeats: false) { [loop = self.loop] _ in
loop.scheduleTask(in: .microseconds(10)) {
promise.succeed("hello")
}
}
myRunLoop.add(timer, forMode: .default)
XCTAssertEqual("hello", try promise.futureResult.waitSpinningRunLoop())
}

}

0 comments on commit 2a8811a

Please sign in to comment.