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.