From 3c7f440cfac5ca060544f38b2083f171f9a9ee2d Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Fri, 6 Oct 2023 14:18:03 +0100 Subject: [PATCH] Add async version of wait, add run and asyncRun (#243) * Add async version of wait, add run and asyncRun `wait` stalls the thread it is running on. If that thread is used by the task executor it will mean running Tasks could stall. Added `asyncWait` to run application on a separate DispatchQueue. Also added a `run` and `asyncRun` which is a combination of `start` and `wait` * Update Sources/Hummingbird/Application.swift Co-authored-by: Joannis Orlandos * Update Sources/Hummingbird/Application.swift Co-authored-by: Joannis Orlandos * PR changes requested --------- Co-authored-by: Joannis Orlandos --- Sources/Hummingbird/Application.swift | 51 +++++++++++++++++++++++++-- Sources/PerformanceTest/main.swift | 5 ++- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/Sources/Hummingbird/Application.swift b/Sources/Hummingbird/Application.swift index 379a0021a..332c69a3c 100644 --- a/Sources/Hummingbird/Application.swift +++ b/Sources/Hummingbird/Application.swift @@ -174,7 +174,26 @@ public final class HBApplication: HBExtensible { // MARK: Methods - /// Run application + /// Start application and wait for it to stop + /// + /// This function can only be called from a non async context as it stalls + /// the current thread waiting for the application to finish + @available(*, noasync, message: "Use HBApplication.asyncRun instead.") + public func run() throws { + try self.start() + self.wait() + } + + /// Start application and wait for it to stop + /// + /// Version of `run` that can be called from asynchronous context + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + public func asyncRun() async throws { + try self.start() + await self.asyncWait() + } + + /// Start application public func start() throws { var startError: Error? let startSemaphore = DispatchSemaphore(value: 0) @@ -187,11 +206,25 @@ public final class HBApplication: HBExtensible { try startError.map { throw $0 } } - /// wait while server is running + /// Wait until server has stopped running + /// + /// This function can only be called from a non async context as it stalls + /// the current thread waiting for the application to finish + @available(*, noasync, message: "Use HBApplication.asyncWait instead.") public func wait() { self.lifecycle.wait() } + /// Wait until server has stopped running + /// + /// Version of `wait`` that can be called from asynchronous context + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + public func asyncWait() async { + await self.onExecutionQueue { app in + app.wait() + } + } + /// Shutdown application public func stop() { let stopSemaphore = DispatchSemaphore(value: 0) @@ -218,4 +251,18 @@ public final class HBApplication: HBExtensible { try self.eventLoopGroup.syncShutdownGracefully() } } + + /// Run closure on private execution queue + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + private func onExecutionQueue(_ process: @Sendable @escaping (HBApplication) -> Void) async { + let unsafeApp = HBUnsafeTransfer(self) + await withCheckedContinuation { continuation in + HBApplication.executionQueue.async { + process(unsafeApp.wrappedValue) + continuation.resume() + } + } + } + + private static let executionQueue = DispatchQueue(label: "hummingbird.execution") } diff --git a/Sources/PerformanceTest/main.swift b/Sources/PerformanceTest/main.swift index e4d5a848e..6589811dd 100644 --- a/Sources/PerformanceTest/main.swift +++ b/Sources/PerformanceTest/main.swift @@ -18,7 +18,7 @@ import NIOPosix // get environment let hostname = HBEnvironment.shared.get("SERVER_HOSTNAME") ?? "127.0.0.1" -let port = HBEnvironment.shared.get("SERVER_PORT", as: Int.self) ?? 8080 +let port = HBEnvironment.shared.get("SERVER_PORT", as: Int.self) ?? 8081 // create app let elg = MultiThreadedEventLoopGroup(numberOfThreads: 2) @@ -54,5 +54,4 @@ app.router.get("json") { _ in } // run app -try app.start() -app.wait() +try app.run()