Soto icon

Soto

Soto and Swift concurrency

The new release of Swift 5.5 comes with the much anticipated Swift concurrency model. The changes include the async/await syntax, structured concurrency, async sequences plus much more. This is an exciting time to be working in Swift. With the release of v5.9.0 of Soto we have added support for some of these new features.

Service API files

First, all the service commands which return an EventLoopFuture of the response from AWS now have an async/await equivalent. If you are running in an async context then the following code from previous versions of Soto

let request = S3.GetObjectRequest(bucket: "my-bucket", key: "my-file")
let responseFuture = s3.getObject(request).whenComplete { result in
    switch result {
    case .success(let response):
        processResponse(response)
    case .failure(let error):
        processError(error)
    }
}

Can now be replaced with

do {
    let request = S3.GetObjectRequest(bucket: "my-bucket", key: "my-file")
    let response = try await s3.getObject(request)
    processResponse(response)
} catch {
    processError(error)
}

The new code is cleaner and easier to read. It looks more like your standard synchronous code and doesn't rely on closures for processing results of asynchronous functions.

Paginators

Swift 5.5 adds the protocol AsyncSequence. This is similar to the protocol Sequence in that you can create an iterator to iterate through all the elements of the AsyncSequence. The exception is to get next element of an AsyncIterator is an async operation. Soto uses these to represent paginators. Each call to next() will get the next page of results from AWS. For an operation that can be paginated you can create an AsyncSequence paginator and then access all the results in a similar way to how you would with a Sequence, as you see below.

var results: [TestObject] = []
let queryRequest = DynamoDB.QueryInput(
    expressionAttributeValues: [":pk": .s("id"), ":sk": .n("2")],
    keyConditionExpression: "id = :id and version >= :version",
    tableName: tableName
)
let paginator = Self.dynamoDB.queryPaginator(queryRequest, type: TestObject.self)
// treat paginator as sequence of results
for try await response in paginator {
    results.append(contentsOf: response.items ?? [])
}

Waiters

Waiters are a relatively new feature of Soto but they also get an async/await API. In a similar manner to the service API commands, we add an equivalent async/await function for every waiter function that returns an EventLoopFuture. Below the code from previous versions of Soto creates a DynamoDB table and then waits for it to be ready using the waiter waitUntilTableExists.

let request = DynamoDB.CreateTableInput(
    attributeDefinitions: [.init(attributeName: "pk", attributeType: .s)],
    keySchema: [.init(attributeName: "pk", keyType: .hash)],
    tableName: "my-table"
)
dynamoDB.createTable(request)
    .flatMap { _ in
        return dynamoDB.waitUntilTableExists(.init(tableName: "my-table"))
    }
    .whenComplete { result in
        ...
    }

can be replaced with

let request = DynamoDB.CreateTableInput(
    attributeDefinitions: [.init(attributeName: "pk", attributeType: .s)],
    keySchema: [.init(attributeName: "pk", keyType: .hash)],
    tableName: "my-table"
)
_ = try await dynamoDB.createTable(request)
try await dynamoDB.waitUntilTableExists(.init(tableName: "my-table"))

In the original code you can see that two commands have been chained together using flatMap. Chaining of EventLoopFutures has been one of the harder things to get right when working with Swift NIO. When there were issues with your code, the compiler did not always produce very helpful error messages. The new async/await code removes the chaining and thus is easier to work with.

Future

Currently the Soto support for the new Swift concurrency is a thin layer that fits on top of the existing APIs. The internals of Soto are still the same Swift NIO code that was there before. This is to ensure the existing EventLoopFuture APIs are still available for users who cannot upgrade to Swift 5.5, or who would still prefer to work with these APIs.

At some point though there will be a version of Soto that replaces the Swift NIO internals with a Swift concurrency implementation. This will be a breaking change and from that point the EventLoopFuture APIs will no longer be available.