Promises in the AWS SDK for PHP Version 3
The AWS SDK for PHP uses promises to allow for asynchronous workflows, and
this asynchronicity allows HTTP requests to be sent concurrently. The promise
specification used by the SDK is Promises/A+
What is a promise?
A promise represents the eventual result of an asynchronous operation. The
primary way of interacting with a promise is through its then
method. This method
registers callbacks to receive either a promise’s eventual value or the reason
why the promise can’t be fulfilled.
The AWS SDK for PHP relies on the guzzlehttp/promises
Note
HTTP requests are sent concurrently in the AWS SDK for PHP using a single thread, in which non-blocking calls are used to transfer one or more HTTP requests while reacting to state changes (e.g., fulfilling or rejecting promises).
Promises in the SDK
Promises are used throughout the SDK. For example, promises are used in most high-level abstractions provided by the SDK: paginators, waiters, command pools, multipart uploads, S3 directory/bucket transfers, and so on.
All of the clients that the SDK provides return promises when you invoke any
of the Async
suffixed methods. For example, the following code shows how to
create a promise for getting the results of an Amazon DynamoDBDescribeTable
operation.
$client = new Aws\DynamoDb\DynamoDbClient([ 'region' => 'us-west-2', 'version' => 'latest', ]); // This will create a promise that will eventually contain a result $promise = $client->describeTableAsync(['TableName' => 'mytable']);
Notice that you can call either describeTable
or describeTableAsync
.
These methods are magic __call
methods on a client that are powered by the
API model and version
number associated with the client. By calling methods
like describeTable
without the Async
suffix, the client will block
while it sends an HTTP request and either return an Aws\ResultInterface
object or throw an Aws\Exception\AwsException
. By suffixing the operation
name with Async
(i.e., describeTableAsync
) the client will create a
promise that is eventually fulfilled with an Aws\ResultInterface
object or rejected with an Aws\Exception\AwsException
.
Important
When the promise is returned, the result might have already arrived (for example, when using a mock handler), or the HTTP request might not have been initiated.
You can register a callback with the promise by using the then
method. This
method accepts two callbacks, $onFulfilled
and $onRejected
, both of
which are optional. The $onFulfilled
callback is invoked if the promise
is fulfilled, and the $onRejected
callback is invoked if the promise is
rejected (meaning it failed).
$promise->then( function ($value) { echo "The promise was fulfilled with {$value}"; }, function ($reason) { echo "The promise was rejected with {$reason}"; } );
Executing commands concurrently
Multiple promises can be composed together such that they are executed concurrently. This can be achieved by integrating the SDK with a non-blocking event loop, or by building up multiple promises and waiting on them to complete concurrently.
use GuzzleHttp\Promise\Utils; $sdk = new Aws\Sdk([ 'version' => 'latest', 'region' => 'us-east-1' ]); $s3 = $sdk->createS3(); $ddb = $sdk->createDynamoDb(); $promises = [ 'buckets' => $s3->listBucketsAsync(), 'tables' => $ddb->listTablesAsync(), ]; // Wait for both promises to complete. $results = Utils::unwrap($promises); // Notice that this method will maintain the input array keys. var_dump($results['buckets']->toArray()); var_dump($results['tables']->toArray());
Note
The CommandPool provides a more powerful mechanism for executing multiple API operations concurrently.
Chaining promises
One of the best aspects of promises is that they are composable, allowing you
to create transformation pipelines. Promises are composed by chaining then
callbacks with subsequent then
callbacks. The return value of a then
method is a promise that is fulfilled or rejected based on the result of the
provided callbacks.
$promise = $client->describeTableAsync(['TableName' => 'mytable']); $promise ->then( function ($value) { $value['AddedAttribute'] = 'foo'; return $value; }, function ($reason) use ($client) { // The call failed. You can recover from the error here and // return a value that will be provided to the next successful // then() callback. Let's retry the call. return $client->describeTableAsync(['TableName' => 'mytable']); } )->then( function ($value) { // This is only invoked when the previous then callback is // fulfilled. If the previous callback returned a promise, then // this callback is invoked only after that promise is // fulfilled. echo $value['AddedAttribute']; // outputs "foo" }, function ($reason) { // The previous callback was rejected (failed). } );
Note
The return value of a promise callback is the $value
argument that
is supplied to downstream promises. If you want to provide a value to downstream
promise chains, you must return a value in the callback
function.
Rejection forwarding
You can register a callback to invoke when a promise is rejected. If an
exception is thrown in any callback, the promise is rejected with the
exception and the next promises in the chain are rejected with the
exception. If you return a value successfully from an $onRejected
callback,
the next promises in the promise chain is fulfilled with the return
value from the $onRejected
callback.
Waiting on promises
You can synchronously force promises to complete by using a promise’s wait
method.
$promise = $client->listTablesAsync(); $result = $promise->wait();
If an exception is encountered while invoking the wait
function of a promise,
the promise is rejected with the exception and the exception is thrown.
use Aws\Exception\AwsException; $promise = $client->listTablesAsync(); try { $result = $promise->wait(); } catch (AwsException $e) { // Handle the error }
Calling wait
on a promise that has been fulfilled doesn’t trigger the wait
function. It simply returns the previously delivered value.
$promise = $client->listTablesAsync(); $result = $promise->wait(); assert($result ### $promise->wait());
Calling wait
on a promise that has been rejected throws an exception. If
the rejection reason is an instance of \Exception
the reason is thrown.
Otherwise, a GuzzleHttp\Promise\RejectionException
is thrown and the reason
can be obtained by calling the getReason
method of the exception.
Note
API operation calls in the AWS SDK for PHP are rejected with subclasses of the
Aws\Exception\AwsException
class. However, it’s possible that the
reason delivered to a then
method is different because the addition of
a custom middleware that alters a rejection reason.
Canceling promises
Promises can be canceled using the cancel()
method of a promise. If a
promise has already been resolved, calling cancel()
will have no
effect. Canceling a promise cancels the promise and any promises that are
awaiting delivery from the promise. A canceled promise is rejected with a
GuzzleHttp\Promise\RejectionException
.
Combining promises
You can combine promises into aggregate promises to build more sophisticated
workflows. The guzzlehttp/promise
package contains various functions that
you can use to combine promises.
You can find the API documentation for all of the promise collection functions at namespace-GuzzleHttp.Promise.
each and each_limit
Use the CommandPool when you have a task queue of
Aws\CommandInterface
commands to perform concurrently with a fixed pool
size (the commands can be in memory or yielded by a lazy iterator). The
CommandPool
ensures that a fixed number of commands are sent concurrently
until the supplied iterator is exhausted.
The CommandPool
works only with commands that are executed by the same client.
You can use the GuzzleHttp\Promise\each_limit
function to perform send
commands of different clients concurrently using a fixed pool size.
use GuzzleHttp\Promise; $sdk = new Aws\Sdk([ 'version' => 'latest', 'region' => 'us-west-2' ]); $s3 = $sdk->createS3(); $ddb = $sdk->createDynamoDb(); // Create a generator that yields promises $promiseGenerator = function () use ($s3, $ddb) { yield $s3->listBucketsAsync(); yield $ddb->listTablesAsync(); // yield other promises as needed... }; // Execute the tasks yielded by the generator concurrently while limiting the // maximum number of concurrent promises to 5 $promise = Promise\each_limit($promiseGenerator(), 5); // Waiting on an EachPromise will wait on the entire task queue to complete $promise->wait();
Promise coroutines
One of the more powerful features of the Guzzle promises library is that it allows you to use promise coroutines that make writing asynchronous workflows seem more like writing traditional synchronous workflows. In fact, the AWS SDK for PHP uses coroutine promises in most of the high-level abstractions.
Imagine you wanted to create several buckets and upload a file to the bucket
when the bucket becomes available, and you’d like to do this all concurrently
so that it happens as fast as possible. You can do this easily by combining
multiple coroutine promises together using the all()
promise function.
use GuzzleHttp\Promise; $uploadFn = function ($bucket) use ($s3Client) { return Promise\coroutine(function () use ($bucket, $s3Client) { // You can capture the result by yielding inside of parens $result = (yield $s3Client->createBucket(['Bucket' => $bucket])); // Wait on the bucket to be available $waiter = $s3Client->getWaiter('BucketExists', ['Bucket' => $bucket]); // Wait until the bucket exists yield $waiter->promise(); // Upload a file to the bucket yield $s3Client->putObjectAsync([ 'Bucket' => $bucket, 'Key' => '_placeholder', 'Body' => 'Hi!' ]); }); }; // Create the following buckets $buckets = ['foo', 'baz', 'bar']; $promises = []; // Build an array of promises foreach ($buckets as $bucket) { $promises[] = $uploadFn($bucket); } // Aggregate the promises into a single "all" promise $aggregate = Promise\all($promises); // You can then() off of this promise or synchronously wait $aggregate->wait();