Skip to content

Commit

Permalink
Mark all functions that use wait as noasync (#3168)
Browse files Browse the repository at this point in the history
* Add tests

* Update NIO dependency

* Migrate over to async APIs for FileIO

* Mark entrypoint noasync

* Update noasync code

* Fix the noasync errors in the tests

* Fix some test warnings

* Add async versions of DotEnvFile

* More async APIs

* Migrate Application init

* One more wait() function called with noasync

* Migrate last wait call to noasync and provide async API

* Make sure we tidy up the connection properly

* Change the deprecation to avoid breaking all our users

* Migrate the async tests to use the async Application

* Revert back to noasync

* Async tests should use async APIs

* More async tests should use async APIs

* Make BootCommand async

* Migrate RoutesCommand to async

* Non-breaking async init

* Fix the tests
  • Loading branch information
0xTim committed May 10, 2024
1 parent e69a55b commit d411635
Show file tree
Hide file tree
Showing 25 changed files with 353 additions and 182 deletions.
2 changes: 1 addition & 1 deletion Sources/Development/entrypoint.swift
Expand Up @@ -7,7 +7,7 @@ struct Entrypoint {
var env = try Environment.detect()
try LoggingSystem.bootstrap(from: &env)

let app = Application(env)
let app = await Application(env)
defer { app.shutdown() }

try configure(app)
Expand Down
48 changes: 30 additions & 18 deletions Sources/Vapor/Application.swift
Expand Up @@ -18,7 +18,7 @@ public final class Application: Sendable {
self._environment.withLockedValue { $0 = newValue }
}
}

public var storage: Storage {
get {
self._storage.withLockedValue { $0 }
Expand All @@ -27,11 +27,11 @@ public final class Application: Sendable {
self._storage.withLockedValue { $0 = newValue }
}
}

public var didShutdown: Bool {
self._didShutdown.withLockedValue { $0 }
}

public var logger: Logger {
get {
self._logger.withLockedValue { $0 }
Expand All @@ -40,18 +40,18 @@ public final class Application: Sendable {
self._logger.withLockedValue { $0 = newValue }
}
}

public struct Lifecycle: Sendable {
var handlers: [LifecycleHandler]
init() {
self.handlers = []
}

public mutating func use(_ handler: LifecycleHandler) {
self.handlers.append(handler)
}
}

public var lifecycle: Lifecycle {
get {
self._lifecycle.withLockedValue { $0 }
Expand All @@ -60,17 +60,17 @@ public final class Application: Sendable {
self._lifecycle.withLockedValue { $0 = newValue }
}
}

public final class Locks: Sendable {
public let main: NIOLock
// Is there a type we can use to make this Sendable but reuse the existing lock we already have?
private let storage: NIOLockedValueBox<[ObjectIdentifier: NIOLock]>

init() {
self.main = .init()
self.storage = .init([:])
}

public func lock<Key>(for key: Key.Type) -> NIOLock
where Key: LockKey {
self.main.withLock {
Expand All @@ -80,7 +80,7 @@ public final class Application: Sendable {
}
}
}

public var locks: Locks {
get {
self._locks.withLockedValue { $0 }
Expand All @@ -89,21 +89,21 @@ public final class Application: Sendable {
self._locks.withLockedValue { $0 = newValue }
}
}

public var sync: NIOLock {
self.locks.main
}

public enum EventLoopGroupProvider: Sendable {
case shared(EventLoopGroup)
@available(*, deprecated, renamed: "singleton", message: "Use '.singleton' for a shared 'EventLoopGroup', for better performance")
case createNew

public static var singleton: EventLoopGroupProvider {
.shared(MultiThreadedEventLoopGroup.singleton)
}
}

public let eventLoopGroupProvider: EventLoopGroupProvider
public let eventLoopGroup: EventLoopGroup
internal let isBooted: NIOLockedValueBox<Bool>
Expand All @@ -113,11 +113,18 @@ public final class Application: Sendable {
private let _logger: NIOLockedValueBox<Logger>
private let _lifecycle: NIOLockedValueBox<Lifecycle>
private let _locks: NIOLockedValueBox<Locks>

public init(

@available(*, noasync, message: "This initialiser cannot be used in async contexts, Application.makeApplication() instead")
public convenience init(
_ environment: Environment = .development,
_ eventLoopGroupProvider: EventLoopGroupProvider = .singleton
) {
self.init(environment, eventLoopGroupProvider, async: false)
DotEnvFile.load(for: environment, on: .shared(self.eventLoopGroup), fileio: self.fileio, logger: self.logger)
}

// async flag here is just to stop the compiler from complaining about duplicates
private init(_ environment: Environment = .development, _ eventLoopGroupProvider: EventLoopGroupProvider = .singleton, async: Bool) {
#if swift(<5.9)
Backtrace.install()
#endif
Expand Down Expand Up @@ -149,8 +156,13 @@ public final class Application: Sendable {
self.clients.initialize()
self.clients.use(.http)
self.commands.use(self.servers.command, as: "serve", isDefault: true)
self.commands.use(RoutesCommand(), as: "routes")
DotEnvFile.load(for: environment, on: .shared(self.eventLoopGroup), fileio: self.fileio, logger: self.logger)
self.asyncCommands.use(RoutesCommand(), as: "routes")
}

public static func make(_ environment: Environment = .development, _ eventLoopGroupProvider: EventLoopGroupProvider = .singleton) async throws -> Application {
let app = Application(environment, eventLoopGroupProvider, async: true)
await DotEnvFile.load(for: app.environment, fileio: app.fileio, logger: app.logger)
return app
}

/// Starts the ``Application`` using the ``start()`` method, then waits for any running tasks to complete.
Expand Down
10 changes: 5 additions & 5 deletions Sources/Vapor/Commands/BootCommand.swift
Expand Up @@ -5,22 +5,22 @@ import ConsoleKit
/// $ swift run Run boot
/// Done.
///
public final class BootCommand: Command {
/// See `Command`.
public final class BootCommand: AsyncCommand {
// See `AsyncCommand`.
public struct Signature: CommandSignature {
public init() { }
}

/// See `Command`.
// See `AsyncCommand`.
public var help: String {
return "Boots the application's providers."
}

/// Create a new `BootCommand`.
public init() { }

/// See `Command`.
public func run(using context: CommandContext, signature: Signature) throws {
// See `AsyncCommand`.
public func run(using context: ConsoleKitCommands.CommandContext, signature: Signature) async throws {
context.console.success("Done.")
}
}
6 changes: 3 additions & 3 deletions Sources/Vapor/Commands/RoutesCommand.swift
Expand Up @@ -14,7 +14,7 @@ import RoutingKit
/// is a parameter whose result will be discarded.
///
/// The path will be displayed with the same syntax that is used to register a route.
public final class RoutesCommand: Command {
public final class RoutesCommand: AsyncCommand {
public struct Signature: CommandSignature {
public init() { }
}
Expand All @@ -24,8 +24,8 @@ public final class RoutesCommand: Command {
}

init() { }

public func run(using context: CommandContext, signature: Signature) throws {
public func run(using context: ConsoleKitCommands.CommandContext, signature: Signature) async throws {
let routes = context.application.routes
let includeDescription = !routes.all.filter { $0.userInfo["description"] != nil }.isEmpty
let pathSeparator = "/".consoleText()
Expand Down
6 changes: 3 additions & 3 deletions Sources/Vapor/Core/Core.swift
Expand Up @@ -80,9 +80,9 @@ extension Application {

init() {
self.console = .init(Terminal())
var commands = Commands()
commands.use(BootCommand(), as: "boot")
self.commands = .init(commands)
self.commands = .init(Commands())
var asyncCommands = AsyncCommands()
asyncCommands.use(BootCommand(), as: "boot")
self.asyncCommands = .init(AsyncCommands())
let threadPool = NIOThreadPool(numberOfThreads: System.coreCount)
threadPool.start()
Expand Down
65 changes: 65 additions & 0 deletions Sources/Vapor/HTTP/Server/HTTPServer.swift
Expand Up @@ -320,6 +320,7 @@ public final class HTTPServer: Server, Sendable {
self.connection = .init(nil)
}

@available(*, noasync, message: "Use the async start() method instead.")
public func start(address: BindAddress?) throws {
var configuration = self.configuration

Expand Down Expand Up @@ -366,6 +367,52 @@ public final class HTTPServer: Server, Sendable {
self.didStart.withLockedValue { $0 = true }
}

public func start(address: BindAddress?) async throws {
var configuration = self.configuration

switch address {
case .none:
/// Use the configuration as is.
break
case .hostname(let hostname, let port):
/// Override the hostname, port, neither, or both.
configuration.address = .hostname(hostname ?? configuration.hostname, port: port ?? configuration.port)
case .unixDomainSocket:
/// Override the socket path.
configuration.address = address!
}

/// Print starting message.
let scheme = configuration.tlsConfiguration == nil ? "http" : "https"
let addressDescription: String
switch configuration.address {
case .hostname(let hostname, let port):
addressDescription = "\(scheme)://\(hostname ?? configuration.hostname):\(port ?? configuration.port)"
case .unixDomainSocket(let socketPath):
addressDescription = "\(scheme)+unix: \(socketPath)"
}

self.configuration.logger.notice("Server starting on \(addressDescription)")

/// Start the actual `HTTPServer`.
let serverConnection = try await HTTPServerConnection.start(
application: self.application,
server: self,
responder: self.responder,
configuration: configuration,
on: self.eventLoopGroup
).get()

self.connection.withLockedValue {
precondition($0 == nil, "You can't start the server connection twice")
$0 = serverConnection
}

self.configuration = configuration
self.didStart.withLockedValue { $0 = true }
}

@available(*, noasync, message: "Use the async shutdown() method instead.")
public func shutdown() {
guard let connection = self.connection.withLockedValue({ $0 }) else {
return
Expand All @@ -378,6 +425,24 @@ public final class HTTPServer: Server, Sendable {
}
self.configuration.logger.debug("HTTP server shutting down")
self.didShutdown.withLockedValue { $0 = true }
// Make sure we remove the connection reference in case we want to start up again
self.connection.withLockedValue { $0 = nil }
}

public func shutdown() async {
guard let connection = self.connection.withLockedValue({ $0 }) else {
return
}
self.configuration.logger.debug("Requesting HTTP server shutdown")
do {
try await connection.close(timeout: self.configuration.shutdownTimeout).get()
} catch {
self.configuration.logger.error("Could not stop HTTP server: \(error)")
}
self.configuration.logger.debug("HTTP server shutting down")
self.didShutdown.withLockedValue { $0 = true }
// Make sure we remove the connection reference in case we want to start up again
self.connection.withLockedValue { $0 = nil }
}

public var localAddress: SocketAddress? {
Expand Down

0 comments on commit d411635

Please sign in to comment.