Skip to content

Commit

Permalink
EventLoopFuture.waitSpinningRunLoop()
Browse files Browse the repository at this point in the history
  • Loading branch information
weissi committed Nov 21, 2024
1 parent 64eb8bf commit 6d5f73b
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 0 deletions.
69 changes: 69 additions & 0 deletions Sources/NIOFoundationCompat/WaitSpinningRunLoop.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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()")
@preconcurrency
@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
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)
let valueReady = ManagedAtomic(false)
self.whenComplete { result in
value.withLockedValue { value in
value = result
}
let (exchanged, _) = valueReady.compareExchange(
expected: false,
desired: true,
ordering: .sequentiallyConsistent
)
precondition(exchanged)
}

while !valueReady.load(ordering: .relaxed) {
runLoop.run(mode: mode, before: Date().addingTimeInterval(1))
}

return try value.withLockedValue { value in
switch value! {
case .success(let result):
return result
case .failure(let error):
throw error
}
}
}
}
49 changes: 49 additions & 0 deletions Tests/NIOFoundationCompatTests/WaitSpinningRunLoopTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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
myRunLoop.schedule(after: .init(Date().addingTimeInterval(0.01))) { [loop = self.loop] in
loop.scheduleTask(in: .microseconds(10)) {
promise.succeed("hello")
}
}
XCTAssertEqual("hello", try promise.futureResult.waitSpinningRunLoop())
}

}

0 comments on commit 6d5f73b

Please # to comment.