Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mark all functions that use wait as noasync #3168

Merged
merged 26 commits into from May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e315f80
Add tests
0xTim Apr 3, 2024
0e56ad5
Update NIO dependency
0xTim Apr 3, 2024
bae2a29
Migrate over to async APIs for FileIO
0xTim Apr 4, 2024
aa612d6
Mark entrypoint noasync
0xTim Apr 4, 2024
ceaa806
Update noasync code
0xTim Apr 4, 2024
9287f2e
Fix the noasync errors in the tests
0xTim Apr 4, 2024
cafd27d
Fix some test warnings
0xTim Apr 4, 2024
2e24d89
Add async versions of DotEnvFile
0xTim Apr 4, 2024
a923ecb
More async APIs
0xTim Apr 4, 2024
2589c2f
Migrate Application init
0xTim Apr 4, 2024
6ba1757
One more wait() function called with noasync
0xTim Apr 4, 2024
4d9cb8c
Migrate last wait call to noasync and provide async API
0xTim Apr 4, 2024
dc881da
Make sure we tidy up the connection properly
0xTim Apr 4, 2024
5c471ae
Merge branch 'main' into noasync-waits
0xTim Apr 24, 2024
647597a
Merge branch 'main' into noasync-waits
0xTim Apr 29, 2024
18e2407
Merge branch 'main' into noasync-waits
0xTim May 9, 2024
7225ece
Change the deprecation to avoid breaking all our users
0xTim May 9, 2024
2347b5f
Migrate the async tests to use the async Application
0xTim May 9, 2024
a1a5709
Revert back to noasync
0xTim May 9, 2024
fa07cb9
Async tests should use async APIs
0xTim May 9, 2024
e6bb263
More async tests should use async APIs
0xTim May 9, 2024
830adfb
Make BootCommand async
0xTim May 9, 2024
915a61a
Migrate RoutesCommand to async
0xTim May 9, 2024
e5282dc
Non-breaking async init
0xTim May 9, 2024
ce90364
Fix the tests
0xTim May 9, 2024
40de0ed
Merge branch 'main' into noasync-waits
0xTim May 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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