async-aws

Jérémy DERUSSÉ

Developer at Blackfire.io

@symfony core team

@jderusse

DX

Developer
eXperience

tooling

  • debug bar
  • maker bundle
  • flex
  • autowiring/configure
  • ...

simple and intuitive Interfaces

$response = $client->request(
    'GET',
    'https://api.github.com/users/jderusse');

// do something asynchronous

dump($response->getContent());
$promise = $client->requestAsync(
    'GET',
    'https://api.github.com/users/jderusse');
    
$promise->then(function(ResponseInterface $res) {
    dump($res->getBody());
});

// do something asynchronous

$promise->wait();

symfony/http-client

guzzle

aws/aws-sdk-php

  • 100% feature complete
  • maintained
  • documented

official

no auto-completion

unreadable code

/**
 * ...
 * @method \Aws\Result sendBulkEmail(array $args = [])
 * @method \GuzzleHttp\Promise\Promise sendBulkEmailAsync(array $args = [])
 * @method \Aws\Result sendCustomVerificationEmail(array $args = [])
 * @method \GuzzleHttp\Promise\Promise sendCustomVerificationEmailAsync(array $args = [])
 * ...
 * @method \Aws\Result sendEmail(array $args = [])
 * @method \GuzzleHttp\Promise\Promise sendEmailAsync(array $args = [])
 * ...
 */
class SesV2Client extends AwsClient
{
    public function __call($name, array $args)
    {
        // ...
    }
}

symfony     AWS

  • messenger SQS
  • mailer SES
  • notifier SNS

basic support

@Nyholm

Tobias Nyholm

Developer at Happyr.com

@symfony core team

@symfony CARE team

async-aws

how to maintain?

  • 276 services
  • 10,294 operations
  • new version ~every day

generated code


{
  "operations":{
    "SendEmail":{
      "name":"SendEmail",
      "http":{
        "method":"POST",
        "requestUri":"/v2/email/outbound-emails"
      },
      "input":{"shape":"SendEmailRequest"},
      "output":{"shape":"SendEmailResponse"},
      "errors":[
        {"shape":"TooManyRequestsException"},
        {"shape":"LimitExceededException"}
      ]
    }
  },
  "shapes": {
    "SendEmailRequest":{
      "type":"structure",
      "required":["Content"],
      "members":{
        "FromEmailAddress":{"shape":"EmailAddress"},
        "FromEmailAddressIdentityArn":{"shape":"AmazonResourceName"},
        "Destination":{"shape":"Destination"},
        "ReplyToAddresses":{"shape":"EmailAddressList"},
        "FeedbackForwardingEmailAddress":{"shape":"EmailAddress"},
        "FeedbackForwardingEmailAddressIdentityArn":{"shape":"AmazonResourceName"},
        "Content":{"shape":"EmailContent"},
        "EmailTags":{"shape":"MessageTagList"},
        "ConfigurationSetName":{"shape":"ConfigurationSetName"},
        "ListManagementOptions":{"shape":"ListManagementOptions"}
      }
    },
    "SendEmailResponse":{
      "type":"structure",
      "members":{
        "MessageId":{"shape":"OutboundMessageId"}
      }
    }
  }
}

official sdk

class SesClient
{
    public function sendEmail($input): SendEmailResponse { ... }
}

class SendEmailRequest
{
    /* var string|null */
    private $fromEmailAddress;
    /* var Destination|null */
    private $destination
    ...
}

class SendEmailResponse
{
    public function getMessageId(): ?string { ... }
}

class TooManyRequestsException extends Exception {}
class LimitExceededException extends Exception {}

final class Destination
{
    private $toAddresses;
    private $ccAddresses;
    private $bccAddresses;
}

...

async-aws

automated updates

how to provide great DX?

  • simple interfaces
  • allowing advanced usage

leverage symfony/http-client

$response = $sesClient->sendEmail([
    'Content' => $message,
]);









// do something asynchronous
class SesClient
{
  private HttpClientInterface $httpClient;

  public function sendEmail($input): SendEmailResponse
  {
    $req = $this->sign($input);
    $response = $this->httpClient->request(...$req);

    return new SendEmailResponse(
      $response,
      $this->client
    );
  }
}
class SesClient
{
  private HttpClientInterface $httpClient;

  public function sendEmail($input): SendEmailResponse
  {
    $req = $this->sign($input);
    $response = $this->httpClient->request(...$req);
$response = $sesClient->sendEmail([
    'Content' => $message,
]);

SesClient

HttpClient

HttpResponseInterface

SendEmailResponse

$response = $sesClient->sendEmail([
    'Content' => $message,
]);









// do something asynchronous


$response->getMessageId();

no magic

$response = $sesClient->sendEmail([
    'Content' => $message,
]);


// do something asynchronous

echo $response->getMessageId();
class SendEmailResponse
{
    private ResponseInterface $httpResponse;
    private HttpClientInterface $httpClient;

    public function getMessageId(): ?string
    {
        $this->resolve();
        $this->populateResponse();

        return $this->messageId;
    }
















}
class SendEmailResponse
{
    private ResponseInterface $httpResponse;
    private HttpClientInterface $httpClient;

    public function getMessageId(): ?string
    {
        $this->resolve();
        $this->populateResponse();

        return $this->messageId;
    }

    public function resolve(?float $timeout = null): bool
    {
        foreach ($this->httpClient->stream($this->httpResponse, $timeout) as $chunk) {
            if ($chunk->isTimeout()) {
                return false;
            }
            if ($chunk->isFirst()) {
                break;
            }
        }

        $this->handleErrors();

        return true;
    }
}

async first

$response = $sesClient->sendEmail([
    'Content' => $message,
]);




echo $response->getMessageId();

async

sync/blocking

$response = $sesClient->sendEmail([
    'Content' => $message,
]);


// do something asynchronous

echo $response->getMessageId();

streaming

$response = $s3Client->putObject([
    'Bucket' => 'my-bucket',
    'Key' => 'video.mkv',
    'Body' => fopen($filename, 'r'),
]);


while (!$response->resolve(0.1)) {
    $spinner->tick();
}


echo 'Done: '.$response->getEtag();

parallel processing

$results = [];
for ($users as $user) {
    $results[] = $sesClient->sendEmail([
        'Content' => $message,
        'Destination' => ['ToAddresses' => [$user->email]],
    ]);
}

foreach (Result::wait($results, null, true) as $result) {
    echo $result->getMessageId();
}

retry failed request

$httpClient = HttpClient::create();

$httpClient = new RetryableHttpClient(
    $httpClient,
    new GenericRetryStrategy()
);
namespace AsyncAws\Core\HttpClient;

class AwsRetryStrategy extends GenericRetryStrategy
{
    public function shouldRetry(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): ?bool
    {
        if (parent::shouldRetry($context, $responseContent, $exception)) {
            return true;
        }
        ...

        $error = $this->createError($responseContent, $context->getHeaders());

        return \in_array($error->getCode(), [
            'RequestLimitExceeded',
            'Throttling',
            'ThrottlingException',
            'ThrottledException',
            'LimitExceededException',
            'PriorRequestNotComplete',
            'ProvisionedThroughputExceededException',
            'RequestThrottled',
            // ...
        ], true);
    }
}

you already use it

  • bref/bref
  • bref/symfony-messenger
  • oneup/flysystem-bundle
  • league/flysystem-bundle
  • symfony/amazon-mailer
  • symfony/amazon-sqs-messenger

@jderusse

slides     

Thank you!

something is missing?

Contribute

async-aws

By Jérémy Derussé

async-aws

  • 1,782