Chapters

Hide chapters

Server-Side Swift with Vapor

Third Edition - Early Acess 1 · 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

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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.

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

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.

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.

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

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)
}

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<User>] = []
  // 2
  for user in users {
    userSaveResults.append(user.save(on: request.db))
  }
  // 3
  return userSaveResults
  	.flatten(on: request.eventLoop)
  	.map { savedUsers in
      // 4
      for user in savedUser {
        print("Saved \(user.username)")
      }
      // 5
      return .created
    }
}

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.

// 1
getAllUsers()
  // 2
  .and(req.client.get("http://localhost:8080/getUserData"))
  // 3
  .flatMap { users, response in
    // 4
    allUsers[0].addData(response).transform(to: .noContent)
}
// 1
getAllUsers()
  // 2
  .and(req.client.get("http://localhost:8080/getUserData"))
  // 3
  .map { users, response in
    // 4
    allUsers[0].syncAddData(response)
    // 5
    return .content
}
getAllUsers()
  .and(getAllAcronyms())
  .and(getAllCategories()).flatMap { result in
    // Use the different futures
}

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)
}

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.

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

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)
futureResult.map { user in
  print("User was saved")
}.whenFailure { error in
  print("There was an error saving the user: \(error)")
}
// 1
return saveUser(on: req.db)
 .flatMapErrorThrowing { error -> User in
    // 2
    print("Error saving the user: \(error)")
    // 3
    return User(name: "Default User")
}
return user.save(on: req).flatMapError { 
  error -> EventLoopFuture<User> in
    print("Error saving the user: \(error)")
    return User(name: "Default User").save(on: req)
}

Chaining futures

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

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

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.

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

Waiting

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

let savedUser = try user.save(on: database).wait()

SwiftNIO

Vapor is built on top of Apple’s SwiftNIO library. SwiftNIO is a cross-platform, asynchronous networking library, like Java’s Netty. It’s open-source, just like Swift itself!

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 or SwiftNIO’s API documentation. Vapor’s documentation site also has a large section 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.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now