Optical
Aberration

Hummingbird and Swift concurrency

Swift 5.5 has been released. This new release includes a series of new concurrency features including, async/await, structured concurrency and actors. Probably one of the most exciting series of additions to Swift in a long time (and this is from a language with new features appearing, what seems like, every other week).

The recently released version of Hummingbird (v0.13.0) includes new APIs that allow you to take advantage of some of these new concurrency features. You can add async route handlers functions, add middleware using async functions and various systems have been augmented with new async apis.

Async route handlers

Where previously an asynchronous route handler function would return an EventLoopFuture it is now possible instead to use an async handler function. Adding an async handler is as easy as adding any other handler.

app.router.get("test-async") { request -> String in
    // the await means this is an async function
    let result = await asyncOperation()
    return result.description
}

Async middleware

There is a new protocol HBAsyncMiddleware. This requires an async apply method be implemented instead of the standard HBMiddleware which requires a method returning an EventLoopFuture. Below is an example session middleware using HBAsyncMiddleware, HummingbirdAuth framework and persist system for storing persistent data between requests. A session cookie is extracted from the request, this is then used to get the authenticatable type Session from the persist system which is then attached to the request using authLogin.

struct AsyncSessionMiddleware: HBAsyncMiddleware {
    public func apply(to request: HBRequest, next: HBResponder) async throws -> HBResponse {
        var request = request
        if let sessionCookie = request.cookies["_SESSION_ID"] {
            // persist system has an async/await api
            let session = try await request.persist.get(key: sessionCookie.value, as: Session.self)
            if let session = session {
                request.authLogin(session)
            }
        }
        return try await next.respond(to: request)
    }
}

Normally when using the HummingbirdAuth package for building authentication middleware you would use an HBAuthenticator. The HummingbirdAuth package also includes the HBAsyncAuthenticator protocol to implement authenticators with async/await.

Streaming request body

Streamed request bodies can now be access via an AsyncSequence of ByteBuffers. You can get access to this via HBRequest.body.stream?.sequence. There has also been a change to the flag to indicate you want a streamed request. The body parameter has been changed into an options parameter that can include other route options. The example below uses the new AsyncSequence to return the size of the buffer supplied.

app.router.post("stream", options: .streamBody) { request -> String in
    guard let stream = request.body.stream else { throw HBHTTPError(.badRequest) }
    var size = 0
    // for buffers in AsyncSequence
    for try await buffer in stream.sequence {
        size += buffer.readableBytes
    }
    return size.description
}

Request and Response struct

The new release of Hummingbird includes a couple of version breaking changes. Unfortunate as this is I believe they are necessary to ensure Hummingbird is used in a thread/task safe manner.

Previously both HBRequest and HBResponse were classes. These have now been changed to structs. Making them value types instead of reference types removes a whole class of race conditions that could occur. These changes don’t come without some cost though. You can only modify a struct from mutating methods that are members of that struct. This affected HummingbirdAuth which used an internal type Auth to mutate the HBRequest. This is no longer possible so the internal type has been removed and calls like HBRequest.auth.login have been replaced with HBRequest.authLogin.

Performance

The performance of the new concurrency features are not in line with what the SwiftNIO is capable of. At the moment there is no way to ensure what EventLoop you are running on and tasks can move about threads. This has a performance impact. This hopefully will be resolved over time though as custom executors are implemented, allowing SwiftNIO to implement an EventLoop model within Swift concurrency. Nothing is guaranteed though.

At the same time it is a lot easier writing async/await code than getting lost in chaining various EventLoopFuture.flatMaps together. The loss in code performance has to be measured against the gain in development performance. Many people won't need the code performance and should probably move across to async/await now, but there will be systems that still need the performance of SwiftNIO and should stick with the EventLoopFuture routes for the moment.