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

Add support for async/await #2613

Merged
merged 71 commits into from
Oct 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
f9ad4c2
Add NIOConcurrency as a dependency
0xTim Apr 27, 2021
86136fb
Add async functions for Client
0xTim Apr 28, 2021
94891f1
Add some async routes for testing
0xTim Apr 28, 2021
73d55ed
Get the routes compiling
0xTim Apr 28, 2021
4d278b2
Add AsyncMiddleware for use in the Middleware chain
0xTim Apr 28, 2021
f9e4f57
Add TestAsyncMiddleware and async version of Responder for use in Asy…
0xTim Apr 28, 2021
94ebea5
Hook up TestAsyncMiddleware to routes
0xTim Apr 28, 2021
a0dac43
Remove unsafe flags from framework
0xTim Apr 28, 2021
4029a09
Run CI on 5.4 nightly to get access to async/await in the compiler
0xTim Apr 28, 2021
62c8287
Apply suggestions from code review
0xTim Apr 28, 2021
7b1ec2d
Add async API for AsyncPasswordHasher
0xTim Apr 28, 2021
119e5d4
Simplify AsyncPasswordHashed async APIs
0xTim Apr 28, 2021
bd110d8
Add async APIs for Cache
0xTim Apr 28, 2021
bc07009
Add async APIs for ViewRenderer
0xTim Apr 28, 2021
e18f3df
Merge in latest changes
0xTim Jun 8, 2021
801eed4
Update compiler check for Xcode 13
0xTim Jun 8, 2021
71d49a9
Add async websocket APIs (#2643)
madsodgaard Jun 15, 2021
946864c
Update availability checks for async/await now they work
0xTim Jul 14, 2021
dd77ac2
Merge branch 'main' into async-await
0xTim Jul 14, 2021
38a8a49
Remove manifest hack to get concurrency to work
0xTim Jul 14, 2021
299f079
Wrap dev async routes in availability check
0xTim Jul 14, 2021
34de956
Update the old completeWithAsync method name to completeWithTask (#2672)
hyerra Aug 19, 2021
7a02f89
Fix GitHub format (#2660)
eltociear Jul 18, 2021
99935fd
Added document comment to Middleware.swift and cleaned up comments in…
Taenerys Aug 3, 2021
72eaa44
Cleaned up Middleware comments and changed variable name (#2664)
Taenerys Aug 6, 2021
691c76b
Merge branch 'main' into async-await
0xTim Aug 19, 2021
9741efb
Merge branch 'main' into async-await
0xTim Sep 16, 2021
b0c299a
Wrap instances of concurrency in canImport(_Concurrency)
0xTim Sep 22, 2021
93f3207
Merge branch 'main' into async-await
0xTim Sep 24, 2021
23fcee6
Update concurrency dependencies to use NIOCore
0xTim Sep 24, 2021
63a41f3
Fix compilation errors in development
0xTim Sep 24, 2021
b896b1f
Add async/await FileIO APIs
0xTim Oct 7, 2021
4ec87ef
Add async APIS for authentcation
0xTim Oct 7, 2021
d87bd7b
Add inline docs for FileIO
0xTim Oct 7, 2021
41a7a7d
Add AsyncRequestDecodable and AsyncResponseEncodable
0xTim Oct 7, 2021
a974841
Add Content conformances to AsyncResponseEncodable
0xTim Oct 7, 2021
3c38496
Make more things conform to AsyncResponseEncodable by default
0xTim Oct 7, 2021
639c58d
Add AnyResponse async version
0xTim Oct 7, 2021
7f6513a
File rename
0xTim Oct 7, 2021
8e50b4e
Add an AsyncBasicResponder and push the ELF boundary down
0xTim Oct 7, 2021
9bfefdf
Noice
0xTim Oct 7, 2021
95310e1
Merge branch 'main' into async-await
0xTim Oct 7, 2021
9fa02c9
Fix typo
0xTim Oct 7, 2021
b81b79e
Fix a multitude of errors from pushing stuff down
0xTim Oct 7, 2021
8d060b4
Fix test async middleware
0xTim Oct 7, 2021
69c275e
Add missing availability check
0xTim Oct 7, 2021
8e06c2e
Use correct async authenticator types
0xTim Oct 8, 2021
9821848
Fix warning
0xTim Oct 8, 2021
14a63e3
Add AsyncSessionDriver
0xTim Oct 11, 2021
dbf708f
Fix infinite loop in `AsyncMiddleware` (#2710)
davdroman Oct 13, 2021
98404cd
Start testing async stuff
0xTim Oct 13, 2021
078d165
Merge branch 'main' into async-await
0xTim Oct 13, 2021
5931534
Fix using Content in async contexts
0xTim Oct 18, 2021
aaebce8
Merge branch 'main' into async-await
0xTim Oct 18, 2021
d10d786
Add opaque return type route
0xTim Oct 23, 2021
d02f714
Remove a load of redundant duplicated code
0xTim Oct 23, 2021
a43f768
Fix building concurrency on older platforms
0xTim Oct 23, 2021
0c818a1
Add async auth tests
0xTim Oct 26, 2021
94b3487
Ad async tests for all new stuff
0xTim Oct 26, 2021
eea8641
Update CI
0xTim Oct 26, 2021
e33db1a
Fix 5.2 build
0xTim Oct 26, 2021
4ed1fbd
Async tests don't work on Linux
0xTim Oct 26, 2021
69ca525
Move async tests to their own target
0xTim Oct 26, 2021
9994e14
Get the async tests conpiling
0xTim Oct 26, 2021
a5887bf
Fix the async tests
0xTim Oct 26, 2021
0f320f7
Tidy ups
0xTim Oct 26, 2021
1063d3b
Add test route for async auth
0xTim Oct 26, 2021
6fc6acd
Try and fix the tests
0xTim Oct 26, 2021
6e51855
Fix app test with filter
0xTim Oct 26, 2021
3253a5c
Fix CI
0xTim Oct 26, 2021
3400dfc
Don't run async tests on macOS
0xTim Oct 26, 2021
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
9 changes: 7 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ jobs:
if: ${{ matrix.env.toolchain != '' }}
- name: Check out Vapor
uses: actions/checkout@v2
- name: Run tests with Thread Sanitizer
- name: Run main tests with Thread Sanitizer
timeout-minutes: 30
run: swift test --enable-test-discovery --filter VaporTests --sanitize=thread
- name: Run async tests without Thread Sanitizer
# TSAN and async aren't playing nice at the moment
timeout-minutes: 30
run: swift test --enable-test-discovery --sanitize=thread
if: ${{ matrix.env.toolchain == '' }}
run: swift test --enable-test-discovery --filter AsyncTests
13 changes: 10 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ let package = Package(
dependencies: [
// HTTP client library built on SwiftNIO
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.2.0"),

// Sugary extensions for the SwiftNIO library
.package(url: "https://github.com/vapor/async-kit.git", from: "1.0.0"),

Expand All @@ -33,7 +33,7 @@ let package = Package(
.package(url: "https://github.com/swift-server/swift-backtrace.git", from: "1.1.1"),

// Event-driven network application framework for high performance protocol servers & clients, non-blocking.
.package(url: "https://github.com/apple/swift-nio.git", from: "2.18.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.33.0"),

// Bindings to OpenSSL-compatible libraries for TLS support in SwiftNIO
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.8.0"),
Expand Down Expand Up @@ -76,6 +76,7 @@ let package = Package(
.product(name: "Logging", package: "swift-log"),
.product(name: "Metrics", package: "swift-metrics"),
.product(name: "NIO", package: "swift-nio"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOExtras", package: "swift-nio-extras"),
.product(name: "NIOFoundationCompat", package: "swift-nio"),
.product(name: "NIOHTTPCompression", package: "swift-nio-extras"),
Expand All @@ -96,7 +97,9 @@ let package = Package(
// Enable better optimizations when building in Release configuration. Despite the use of
// the `.unsafeFlags` construct required by SwiftPM, this flag is recommended for Release
// builds. See <https://github.com/swift-server/guides#building-for-production> for details.
.unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
.unsafeFlags([
"-cross-module-optimization"
], .when(configuration: .release)),
]),

// Testing
Expand All @@ -107,5 +110,9 @@ let package = Package(
.product(name: "NIOTestUtils", package: "swift-nio"),
.target(name: "XCTVapor"),
]),
.testTarget(name: "AsyncTests", dependencies: [
.product(name: "NIOTestUtils", package: "swift-nio"),
.target(name: "XCTVapor"),
]),
]
)
2 changes: 2 additions & 0 deletions Sources/Development/configure.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Vapor

public func configure(_ app: Application) throws {
app.logger.logLevel = .debug

app.http.server.configuration.hostname = "127.0.0.1"
switch app.environment {
case .tls:
Expand Down
99 changes: 99 additions & 0 deletions Sources/Development/routes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,79 @@ public func routes(_ app: Application) throws {
return promise.futureResult
}
}

#if compiler(>=5.5) && canImport(_Concurrency)
if #available(macOS 12, *) {
let asyncRoutes = app.grouped("async").grouped(TestAsyncMiddleware(number: 1))
asyncRoutes.get("client") { req async throws -> String in
let response = try await req.client.get("https://www.google.com")
guard let body = response.body else {
throw Abort(.internalServerError)
}
return String(buffer: body)
}

func asyncRouteTester(_ req: Request) async throws -> String {
let response = try await req.client.get("https://www.google.com")
guard let body = response.body else {
throw Abort(.internalServerError)
}
return String(buffer: body)
}
asyncRoutes.get("client2", use: asyncRouteTester)

asyncRoutes.get("content", use: asyncContentTester)

func asyncContentTester(_ req: Request) async throws -> Creds {
return Creds(email: "name", password: "password")
}

asyncRoutes.get("content2") { req async throws -> Creds in
return Creds(email: "name", password: "password")
}

asyncRoutes.get("contentArray") { req async throws -> [Creds] in
let cred1 = Creds(email: "name", password: "password")
return [cred1]
}

func opaqueRouteTester(_ req: Request) async throws -> some AsyncResponseEncodable {
"Hello World"
}
asyncRoutes.get("opaque", use: opaqueRouteTester)

// Make sure jumping between multiple different types of middleware works
asyncRoutes.grouped(TestAsyncMiddleware(number: 2), TestMiddleware(number: 3), TestAsyncMiddleware(number: 4), TestMiddleware(number: 5)).get("middleware") { req async throws -> String in
return "OK"
}

let basicAuthRoutes = asyncRoutes.grouped(Test.authenticator(), Test.guardMiddleware())
basicAuthRoutes.get("auth") { req async throws -> String in
return try req.auth.require(Test.self).name
}
}

@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
struct Test: Authenticatable {
static func authenticator() -> AsyncAuthenticator {
TestAuthenticator()
}

var name: String
}

@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
struct TestAuthenticator: AsyncBasicAuthenticator {
typealias User = Test

func authenticate(basic: BasicAuthorization, for request: Request) async throws {
if basic.username == "test" && basic.password == "secret" {
let test = Test(name: "Vapor")
request.auth.login(test)
}
}
}
#endif
}

struct TestError: AbortError, DebuggableError {
Expand Down Expand Up @@ -262,3 +335,29 @@ struct TestError: AbortError, DebuggableError {
self.stackTrace = stackTrace
}
}

#if compiler(>=5.5) && canImport(_Concurrency)
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
struct TestAsyncMiddleware: AsyncMiddleware {
let number: Int

func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
request.logger.debug("In async middleware - \(number)")
let response = try await next.respond(to: request)
request.logger.debug("In async middleware way out - \(number)")
return response
}
}
#endif

struct TestMiddleware: Middleware {
let number: Int

func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture<Response> {
request.logger.debug("In non-async middleware - \(number)")
return next.respond(to: request).map { response in
request.logger.debug("In non-async middleware way out - \(self.number)")
return response
}
}
}
54 changes: 54 additions & 0 deletions Sources/Vapor/Concurrency/AnyResponse+Concurrency.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#if compiler(>=5.5) && canImport(_Concurrency)
import NIOCore

/// A type erased response useful for routes that can return more than one type.
///
/// router.get("foo") { req -> AnyAsyncResponse in
/// if /* something */ {
/// return AnyAsyncResponse(42)
/// } else {
/// return AnyAsyncResponse("string")
/// }
/// }
///
/// This can also be done using a `AsyncResponseEncodable` enum.
///
/// enum IntOrString: AsyncResponseEncodable {
/// case int(Int)
/// case string(String)
///
/// func encode(for req: Request) throws -> EventLoopFuture<Response> {
/// switch self {
/// case .int(let i): return try i.encode(for: req)
/// case .string(let s): return try s.encode(for: req)
/// }
/// }
/// }
///
/// router.get("foo") { req -> IntOrString in
/// if /* something */ {
/// return .int(42)
/// } else {
/// return .string("string")
/// }
/// }
///
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
public struct AnyAsyncResponse: AsyncResponseEncodable {
/// The wrapped `AsyncResponseEncodable` type.
private let encodable: AsyncResponseEncodable

/// Creates a new `AnyAsyncResponse`.
///
/// - parameters:
/// - encodable: Something `AsyncResponseEncodable`.
public init(_ encodable: AsyncResponseEncodable) {
self.encodable = encodable
}

public func encodeResponse(for request: Request) async throws -> Response {
return try await self.encodable.encodeResponse(for: request)
}
}

#endif
30 changes: 30 additions & 0 deletions Sources/Vapor/Concurrency/AsyncBasicResponder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#if compiler(>=5.5) && canImport(_Concurrency)
import NIOCore

/// A basic, async closure-based `Responder`.
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
public struct AsyncBasicResponder: AsyncResponder {
/// The stored responder closure.
private let closure: (Request) async throws -> Response

/// Create a new `BasicResponder`.
///
/// let notFound: Responder = BasicResponder { req in
/// let res = req.response(http: .init(status: .notFound))
/// return req.eventLoop.newSucceededFuture(result: res)
/// }
///
/// - parameters:
/// - closure: Responder closure.
public init(
closure: @escaping (Request) async throws -> Response
) {
self.closure = closure
}

public func respond(to request: Request) async throws -> Response {
return try await closure(request)
}
}

#endif
34 changes: 34 additions & 0 deletions Sources/Vapor/Concurrency/AsyncMiddleware.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#if compiler(>=5.5) && canImport(_Concurrency)
import NIOCore

/// `AsyncMiddleware` is placed between the server and your router. It is capable of
/// mutating both incoming requests and outgoing responses. `AsyncMiddleware` can choose
/// to pass requests on to the next `AsyncMiddleware` in a chain, or they can short circuit and
/// return a custom `Response` if desired.
///
/// This is an async version of `Middleware`
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
public protocol AsyncMiddleware: Middleware {
/// Called with each `Request` that passes through this middleware.
/// - parameters:
/// - request: The incoming `Request`.
/// - next: Next `Responder` in the chain, potentially another middleware or the main router.
/// - returns: An asynchronous `Response`.
func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response
}

@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
extension AsyncMiddleware {
public func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture<Response> {
let promise = request.eventLoop.makePromise(of: Response.self)
promise.completeWithTask {
let asyncResponder = AsyncBasicResponder { req in
return try await next.respond(to: req).get()
}
return try await respond(to: request, chainingTo: asyncResponder)
}
return promise.futureResult
}
}

#endif
30 changes: 30 additions & 0 deletions Sources/Vapor/Concurrency/AsyncPasswordHasher+Concurrency.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#if compiler(>=5.5) && canImport(_Concurrency)
import NIOCore

@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
extension AsyncPasswordHasher {
public func hash<Password>(_ password: Password) async throws -> [UInt8]
where Password: DataProtocol
{
try await self.hash(password).get()
}

public func verify<Password, Digest>(
_ password: Password,
created digest: Digest
) async throws -> Bool
where Password: DataProtocol, Digest: DataProtocol
{
try await self.verify(password, created: digest).get()
}

public func hash(_ password: String) async throws -> String {
try await self.hash(password).get()
}

public func verify(_ password: String, created digest: String) async throws -> Bool {
try await self.verify(password, created: digest).get()
}
}

#endif
50 changes: 50 additions & 0 deletions Sources/Vapor/Concurrency/AsyncSessionDriver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#if compiler(>=5.5) && canImport(_Concurrency)
import NIOCore

/// Capable of managing CRUD operations for `Session`s.
///
/// This is an async version of `SessionDriver`
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
public protocol AsyncSessionDriver: SessionDriver {
func createSession(_ data: SessionData, for request: Request) async throws -> SessionID
func readSession(_ sessionID: SessionID, for request: Request) async throws -> SessionData?
func updateSession(_ sessionID: SessionID, to data: SessionData, for request: Request) async throws -> SessionID
func deleteSession(_ sessionID: SessionID, for request: Request) async throws
}

@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
extension AsyncSessionDriver {
public func createSession(_ data: SessionData, for request: Request) -> EventLoopFuture<SessionID> {
let promise = request.eventLoop.makePromise(of: SessionID.self)
promise.completeWithTask {
try await self.createSession(data, for: request)
}
return promise.futureResult
}

public func readSession(_ sessionID: SessionID, for request: Request) -> EventLoopFuture<SessionData?> {
let promise = request.eventLoop.makePromise(of: SessionData?.self)
promise.completeWithTask {
try await self.readSession(sessionID, for: request)
}
return promise.futureResult
}

public func updateSession(_ sessionID: SessionID, to data: SessionData, for request: Request) -> EventLoopFuture<SessionID> {
let promise = request.eventLoop.makePromise(of: SessionID.self)
promise.completeWithTask {
try await self.updateSession(sessionID, to: data, for: request)
}
return promise.futureResult
}

public func deleteSession(_ sessionID: SessionID, for request: Request) -> EventLoopFuture<Void> {
let promise = request.eventLoop.makePromise(of: Void.self)
promise.completeWithTask {
try await self.deleteSession(sessionID, for: request)
}
return promise.futureResult
}
}

#endif