Chapters

Hide chapters

Server-Side Swift with Vapor

Third Edition · iOS 13 · Swift 5.2 - Vapor 4 Framework · Xcode 11.4

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section I: Creating a Simple Web API

Section 1: 13 chapters
Show chapters Hide chapters

4. Async
Written by Tim Condon

In this chapter, you’ll learn about asynchronous and non-blocking architectures. You’ll discover Vapor’s approach to these architectures and how to use it. Finally, the chapter provides a small overview of SwiftNIO, a core technology used by Vapor.

Async

One of Vapor’s most important features is Async. It can also be one of the most confusing. Why is it important?

Consider a scenario where your server has only a single thread and four client requests, in order:

  1. A request for a stock quote. This results in a call to an API on another server.
  2. A request for a static CSS style sheet. The CSS is available immediately without a lookup.
  3. A request for a user’s profile. The profile must be fetched from a database.
  4. A request for some static HTML. The HTML is available immediately without a lookup.

In a synchronous server, the server’s sole thread blocks until the stock quote is returned. It then returns the stock quote and the CSS style sheet. It blocks again while the database fetch completes. Only then, after the user’s profile is sent, will the server return the static HTML to the client.

On the other hand, in an asynchronous server, the thread initiates the call to fetch the stock quote and puts the request aside until it completes. It then returns the CSS style sheet, starts the database fetch and returns the static HTML. As the requests that were put aside complete, the thread resumes work on them and returns their results to the client.

“But, wait!”, you say, “Servers have more than one thread.” And you’re correct. However, there are limits to how many threads a server can have. Creating threads uses resources. Switching context between threads is expensive, and ensuring all your data accesses are thread-safe is time-consuming and error-prone. As a result, trying to solve the problem solely by adding threads is a poor, inefficient solution.

Futures and promises

In order to “put aside” a request while it waits for a response, you must wrap it in a promise to resume work on it when you receive the response.

In practice, this means you must change the return type of methods that can be put aside. In a synchronous environment, you might have a method:

func getAllUsers() -> [User] {
  // do some database queries
}

In an asynchronous environment, this won’t work because your database call may not have completed by the time getAllUsers() must return. You know you’ll be able to return [User] in the future but can’t do so now. In Vapor, you return the result wrapped in an EventLoopFuture. This is a future specific to SwiftNIO’s EventLoop. You’d write your method as shown below:

func getAllUsers() -> EventLoopFuture<[User]> {
  // do some database queries
}

Returning EventLoopFuture<[User]> allows you to return something to the method’s caller, even though there may be nothing to return at that point. But the caller knows that the method returns [User] at some point in the future. You’ll learn more about SwiftNIO at the end of the chapter.

Working with futures

Working with EventLoopFutures can be confusing at first but, since Vapor uses them extensively, they’ll quickly become second nature. In most cases, when you receive an EventLoopFuture from a method, you want to do something with the actual result inside the EventLoopFuture. Since the result of the method hasn’t actually returned yet, you provide a callback to execute when the EventLoopFuture completes.

In the example above, when your program reaches getAllUsers(), it makes the database request on the EventLoop. An EventLoop processes work and in simplistic terms can be thought of as a thread. getAllUsers() doesn’t return the actual data immediately and returns an EventLoopFuture instead. This means the EventLoop pauses execution of that code and works on any other code queued up on that EventLoop. For example, this could be another part of your code where a different EventLoopFuture result has returned. Once the database call returns, the EventLoop then executes the callback.

If the callback calls another method that returns an EventLoopFuture, you provide another callback inside the original callback to execute when the second EventLoopFuture completes. This is why you’ll end up chaining or nesting lots of different callbacks. This is the hard part about working with futures. Asynchronous methods require a complete shift in how to think about your code.

Resolving futures

Vapor provides a number of convenience methods for working with futures to avoid the necessity of dealing with them directly. However, there are numerous scenarios where you must wait for the result of a future. To demonstrate, imagine you have a route that returns the HTTP status code 204 No Content. This route fetches a list of users from a database using a method like the one described above and modifies the first user in the list before returning.

In order to use the result of that call to the database, you must provide a closure to execute when the EventLoopFuture has resolved. There are two main methods you’ll use to do this:

  • flatMap(_:): Executes on a future and returns another future. The callback receives the resolved future and returns another EventLoopFuture.
  • map(_:): Executes on a future and returns another future. The callback receives the resolved future and returns a type other than EventLoopFuture, which map(_:) then wraps in an EventLoopFuture.

Both choices take a future and produce a different EventLoopFuture, usually of a different type. To reiterate, the difference is that if the callback that processes the EventLoopFuture result returns an EventLoopFuture, use flatMap(_:). If the callback returns a type other than EventLoopFuture, use map(_:).

For example:

// 1
return database.getAllUsers().flatMap { users in
  // 2
  let user = users[0]
  user.name = "Bob"
  // 3
  return user.save(on: req.db).map {
    //4    
    return .noContent
  }
}

Here’s what this does:

  1. Fetch all users from the database. As you saw above, getAllUsers() returns EventLoopFuture<[User]>. Since the result of completing this EventLoopFuture is yet another EventLoopFuture (see step 3), use flatMap(_:) to resolve the result. The closure for flatMap(_:) receives the completed future users — an array of all the users from the database, type [User] — as its parameter. This .flatMap(_:) returns EventLoopFuture<HTTPStatus>.
  2. Update the first user’s name.
  3. Save the updated user to the database. This returns EventLoopFuture<Void> but the HTTPStatus value you need to return isn’t yet an EventLoopFuture so use map(_:).
  4. Return the appropriate HTTPStatus value.

As you can see, for the top-level promise you use flatMap(_:) since the closure you provide returns an EventLoopFuture. The inner promise, which returns a non-future HTTPStatus, uses map(_:).

Transform

Sometimes you don’t care about the result of a future, only that it completed successfully. In the above example, you don’t use the resolved result of save(on:) and are returning a different type. For this scenario, you can simplify step 3 by using transform(to:):

return database.getAllUsers().flatMap { users in
  let user = users[0]
  user.name = "Bob"
  return user
  	.save(on: req.db)
  	.transform(to: HTTPStatus.noContent)
}

This helps reduce the amount of nesting and can make your code easier to read and maintain. You’ll see this used throughout the book.

Flatten

There are times when you must wait for a number of futures to complete. One example occurs when you’re saving multiple models in a database. In this case, you use flatten(on:). For instance:

static func save(_ users: [User], request: Request)
    -> EventLoopFuture<HTTPStatus> {
  // 1
  var userSaveResults: [EventLoopFuture<Void>] = []
  // 2
  for user in users {
    userSaveResults.append(user.save(on: request.db))
  }
  // 3
  return userSaveResults
  	.flatten(on: request.eventLoop)
  	.map {
      // 4
      for user in users {
        print("Saved \(user.username)")
      }
      // 5
      return .created
    }
}

In this code, you:

  1. Define an array of EventLoopFuture<User>, the return type of save(on:) in step 2.
  2. Loop through each user in users and append the return value of user.save(on:) to the array.
  3. Use flatten(on:) to wait for all the futures to complete. This takes an EventLoop, essentially the thread that actually performs the work. This is normally retrieved from a Request in Vapor, but you’ll learn about this later. The closure for flatten(on:), if needed, takes the returned collection as a parameter. In this case it’s Void.
  4. Loop through each of the users now you’ve saved them and print out their usernames.
  5. Return a 201 Created status.

flatten(on:) waits for all the futures to return as they’re executed asynchronously by the same EventLoop.

Multiple futures

Occasionally, you need to wait for a number of futures of different types that don’t rely on one another. For example, you might encounter this situation when retrieving users from the database and making a request to an external API. SwiftNIO provides a number of methods to allow waiting for different futures together. This helps avoid deeply nested code or confusing chains.

If you have two futures — get all the users from the database and get some information from an external API — you can use and(_:) like this:

// 1
getAllUsers()
  // 2
  .and(req.client.get("http://localhost:8080/getUserData"))
  // 3
  .flatMap { users, response in
    // 4
    users[0].addData(response).transform(to: .noContent)
}

Here’s what this does:

  1. Call getAllUsers() to get the result of the first future.
  2. Use and(_:) to chain the second future to the first future.
  3. Use flatMap(_:) to wait for the futures to return. The closure takes the resolved results of the futures as parameters.
  4. Call addData(_:), which returns some future result and transform the return to .noContent.

If the closure returns a non-future result, you can use map(_:) on the chained futures instead:

// 1
getAllUsers()
  // 2
  .and(req.client.get("http://localhost:8080/getUserData"))
  // 3
  .map { users, response in
    // 4
    users[0].syncAddData(response)
    // 5
    return .content
}

Here’s what this does:

  1. Call getAllUsers() to get the result of the first future.
  2. Use and(_:) to chain the second future to the first future.
  3. Use map(_:) to wait for the futures to return. The closure takes the resolved results of the futures as parameters.
  4. Call the synchronous syncAddData(_:)
  5. Return .noContent.

Note: You can chain together as many futures as required with and(_:) but the flatMap or map closure returns the resolved futures in tuples. For instance, for three futures:

getAllUsers()
  .and(getAllAcronyms())
  .and(getAllCategories()).flatMap { result in
    // Use the different futures
}

result is of type (([User], [Acronyms]), [Categories]). And the more futures you chain with and(_:), the more nested tuples you get. This can get a bit confusing! :]

Creating futures

Sometimes you need to create your own futures. If an if statement returns a non-future and the else block returns an EventLoopFuture, the compiler will complain that these must be the same type. To fix this, you must convert the non-future into an EventLoopFuture using request.eventLoop.future(_:). For example:

// 1
func createTrackingSession(for request: Request)
    -> EventLoopFuture<TrackingSession> {
  return request.makeNewSession()
}

// 2
func getTrackingSession(for request: Request)
    -> EventLoopFuture<TrackingSession> {
  // 3
  let session: TrackingSession? =
    TrackingSession(id: request.getKey())
  // 4
  guard let createdSession = session else {
    return createTrackingSession(for: request)
  }
  // 5
  return request.eventLoop.future(createdSession)
}

Here’s what this does:

  1. Define a method that creates a TrackingSession from the request. This returns EventLoopFuture<TrackingSession>.
  2. Define a method that gets a tracking session from the request.
  3. Attempt to create a tracking session using the request’s key. This returns nil if the tracking session could not be created.
  4. Ensure the session was created successfully, otherwise create a new tracking session.
  5. Create an EventLoopFuture<TrackingSession> from createdSession using request.eventLoop.future(_:). This returns the future on the request’s EventLoop.

Since createTrackingSession(for:) returns EventLoopFuture<TrackingSession> you have to use request.eventLoop.future(_:) to turn the createdSession into an EventLoopFuture<TrackingSession> to make the compiler happy.

Dealing with errors

Vapor makes heavy use of Swift’s error handling throughout the framework. Many methods either throw or return a failed future, allowing you to handle errors at different levels. You may choose to handle errors inside your route handlers or by using middleware to catch the errors at a higher level, or both. You also need to deal with errors thrown inside the callbacks you provide to flatMap(_:) and map(_:).

Dealing with errors in the callback

The callbacks for map(_:) and flatMap(_:) are both non-throwing. This presents a problem if you call a method inside the closure that throws. When returning a non-future type with a closure that needs to throw, map(_:) has a throwing variant confusingly called flatMapThrowing(_:). To be clear, the callback for flatMapThrowing(_:) returns a non-future type.

For example:

// 1
req.client.get("http://localhost:8080/users")
   .flatMapThrowing { response in
  // 2
  let users = try response.content.decode([User].self)
  // 3
  return users[0]
}

Here’s what this example does:

  1. Make a request to an external API, which returns EventLoopFuture<Response>. You use flatMapThrowing(_:) to provide a callback to the future that can throw an error.
  2. Decode the response to [User]. This can throw an error, which flatMapThrowing converts into a failed future.
  3. Return the first user — a non-future type.

Things are different when returning a future type in the callback. Consider the case where you need to decode a response and then return a future:

// 1
req.client.get("http://localhost:8080/users/1")
   .flatMap { response in
  do {
    // 2
    let user = try response.content.decode(User.self)
    // 3
    return user.save(on: req.db)
  } catch {
    // 4
    return req.eventLoop.makeFailedFuture(error)
  }
}

Here’s what’s happening:

  1. Get a user from the external API. Since the closure will return an EventLoopFuture, use flatMap(_:).
  2. Decode the user from the response. As this throws an error, wrap this in do/catch to catch the error
  3. Save the user and return the EventLoopFuture.
  4. Catch the error if one occurs. Return a failed future on the EventLoop.

Since the callback for flatMap(_:) can’t throw, you must catch the error and return a failed future. The API is designed like this because returning something that can both throw synchronously and asynchronously is confusing to work with.

Dealing with future errors

Dealing with errors is a little different in an asynchronous world. You can’t use Swift’s do/catch as you don’t know when the promise will execute. SwiftNIO provides a number of methods to help handle these cases. At a basic level, you can chain whenFailure(_:) to your future:

let futureResult = user.save(on: req.db)
futureResult.map {
  print("User was saved")
}.whenFailure { error in
  print("There was an error saving the user: \(error)")
}

If save(on:) succeeds, the .map block executes with the resolved value of the future as its parameter. If the future fails, it’ll execute the .whenFailure block, passing in the Error.

In Vapor, you must return something when handling requests, even if it’s a future. Using the above map/whenFailure method won’t stop the error happening, but it’ll allow you to see what the error is. If save(on:) fails and you return futureResult, the failure still propagates up the chain. In most circumstances, however, you want to try and rectify the issue.

SwiftNIO provides flatMapError(_:) and flatMapErrorThrowing(_:) to handle this type of failure. This allows you to handle the error and either fix it or throw a different error. For example:

// 1
return saveUser(on: req.db)
 .flatMapErrorThrowing { error -> User in
    // 2
    print("Error saving the user: \(error)")
    // 3
    return User(name: "Default User")
}

Here’s what this does:

  1. Attempt to save the user. Use flatMapErrorThrowing(_:) to handle the error, if one occurs. The closure takes the error as the parameter and must return the type of the resolved future — in this case User.
  2. Log the error received.
  3. Create a default user to return.

Vapor also provides the related flatMapError(_:) for when the associated closure returns a future:

return saveUser(on: req.db).flatMapError { 
  error -> EventLoopFuture<User> in
    print("Error saving the user: \(error)")
    return User(name: "Default User").save(on: req)
}

Since saveUser(on:) returns a future, you must call flatMapError(_:) instead. Note: The closure for flatMapError(_:) cannot throw an error — you must catch the error and return a new failed future, similar to flatMap(_:) above.

flatMapError and flatMapErrorThrowing only execute their closures on a failure. But what if you want both to handle errors and handle the success case? Simple! Simply chain to the appropriate method!

Chaining futures

Dealing with futures can sometimes seem overwhelming. It’s easy to end up with code that’s nested multiple levels deep.

Vapor allows you to chain futures together instead of nesting them. For example, consider a snippet that looks like the following:

return database
  .getAllUsers()
  .flatMap { users in
    let user = users[0]
    user.name = "Bob"
    return user.save(on: req.db)
      .map {
        return .noContent
  }
}

map(_:) and flatMap(_:) can be chained together to avoid nesting like this:

return database
  .getAllUsers()
  // 1
  .flatMap { users in
    let user = users[0]
    user.name = "Bob"
    return user.save(on: req.db)
  // 2
  }.map {
    return .noContent
  }

Changing the return type of flatMap(_:) allows you to chain the map(_:), which receives the EventLoopFuture<Void>. The final map(_:) then returns the type you returned originally. Chaining futures allows you to reduce the nesting in your code and may make it easier to reason about, which is especially helpful in an asynchronous world. However, whether you nest or chain is completely personal preference.

Always

Sometimes you want to execute something no matter the outcome of a future. You may need to close connections, trigger a notification or just log that the future has executed. For this, use the always callback.

For example:

// 1
let userResult: EventLoopFuture<Void> = user.save(on: req.db)
// 2
userResult.always {
  // 3
  print("User save has been attempted")
}

Here’s what this does:

  1. Save a user and save the result in userResult. This is of type EventLoopFuture<Void>.
  2. Chain an always to the result.
  3. Print a string when the app executes the future.

The always closure gets executed no matter the result of the future, whether it fails or succeeds. It also has no effect on the future. You can combine this with other chains as well.

Waiting

In certain circumstances, you may want to actually wait for the result to return. To do this, use wait().

Note: There’s a large caveat around this: You can’t use wait() on the main event loop, which means all request handlers and most other circumstances.

However, as you’ll see in Chapter 11, “Testing”, this can be especially useful in tests, where writing asynchronous tests is difficult. For example:

let savedUser = try saveUser(on: database).wait()

Instead of savedUser being an EventLoopFuture<User>, because you use wait(), savedUser is a User object. Be aware wait() throws an error if executing the promise fails. It’s worth saying again: This can only be used off the main event loop!

SwiftNIO

Vapor is built on top of Apple’s SwiftNIO library (https://github.com/apple/swift-nio). SwiftNIO is a cross-platform, asynchronous networking library, like Java’s Netty. It’s open-source, just like Swift itself!

SwiftNIO handles all HTTP communications for Vapor. It’s the plumbing that allows Vapor to receive requests and send responses. SwiftNIO manages the connections and the transfer of data.

It also manages all the EventLoops for your futures that perform work and execute your promises. Each EventLoop has its own thread.

Vapor manages all the interactions with NIO and provides a clean, Swifty API to use. Vapor is responsible for the higher-level aspects of a server, such as routing requests. It provides the features to build great server-side Swift applications. SwiftNIO provides a solid foundation to build on.

Where to go from here?

While it isn’t necessary to know all the details about how EventLoopFutures and EventLoops work under the hood, you can find more information in Vapor’s API documentation (https://api.vapor.codes/async-kit/main/AsyncKit/Extensions/EventLoopFuture.html) or SwiftNIO’s API documentation (https://apple.github.io/swift-nio/docs/current/NIO/Classes/EventLoopFuture.html). Vapor’s documentation site also has a large section (https://docs.vapor.codes/4.0/async/) on async and futures.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.