Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Jongler en asynchrone avec Symfony HttpClient

a_guilhem
March 25, 2023

Jongler en asynchrone avec Symfony HttpClient

Si faire des requêtes HTTP est une pratique bien ancrée, la gestion de leurs réponses le semble un peu moins. Les blocages qu'elles peuvent engendrer peuvent très vite devenir un goulot d’étranglement dans les performances d’une application.

Je vous propose de voir comment, grâce à un outil un peu méconnu qui se présente sous forme d’un trait du composant Symfony HttpClient, nous avons pu gagner de la largesse dans la gestion et la manipulation des réponses de nos requêtes tout en préservant au maximum la notion d’asynchronicité.

Nous allons voir ensemble les contextes d’application auxquels l’AsyncDecoratorTrait serait destiné et quels en seraient ses avantages tout comme ses limites. Mais également de voir en quoi il se distingue de la décoration qu’on pourrait faire au niveau de la requête ou de la réponse reçue.

Alors prêt ? Traçons!

a_guilhem

March 25, 2023
Tweet

More Decks by a_guilhem

Other Decks in Programming

Transcript

  1. en asynchrone avec
    Symfony HttpClient

    View full-size slide

  2. Allison Guilhem
    @Les-Tilleuls.coop
    Alli_g83
    Allison E.Guilhem
    a_guilhem
    Qui suis je ?

    View full-size slide

  3. Jongler en asynchrone avec Symfony HttpClient
    Symfony HttpClient : quelques rappels
    L’AsyncDecoratorTrait et son « contexte »
    Et si on passait à la pratique ?

    View full-size slide

  4. Il était une fois Symfony 4.3
    ⾣ composer require symfony/http-client

    View full-size slide

  5. HttpClient: quelques rappels
    - un composant standalone

    - pour consommer des APIs

    - par des opérations synchrones ou asynchrones

    - « Native PHP Streams » et cURL.

    View full-size slide

  6. HttpClient: quelques rappels
    - http_client service - sans état -
    injecté automatiquement via type
    hint HttpClientInterface

    - Symfony/http-client-contracts:
    ensemble d’abstractions découplé du
    composant en lui même

    - Multitude d’options

    public function __construct(
    private HttpClientInterface $client,
    ) {
    }
    public function test(): array
    {
    $response = $this->client->request(
    'GET',
    ‘https://url..’,
    […]
    );

    View full-size slide

  7. HttpClient: quelques rappels
    - Réponses lazy - Requêtes concurrentes

    - Multiplexing : stream

    - Appréhension des erreurs par défaut

    - Inter-opérable et extensible

    foreach ($client->stream($responses)

    as $response => $chunk

    ) {
    if ($chunk->isFirst()) {

    } elseif ($chunk->isLast()) {

    } else {

    }
    }

    View full-size slide

  8. Le début d’une ré
    fl
    exion

    View full-size slide

  9. Con
    fi
    rmation d’un besoin ?

    View full-size slide

  10. - un réel besoin

    - décorateurs / middlewares / event ?

    - préserver la dimension asynchrone

    Manipulation des chunks

    View full-size slide

  11. Des ré
    fl
    exions, des ré
    fl
    exions… et puis

    View full-size slide

  12. AsyncDecoratorTrait
    - Pour des décorateurs de HttpClient

    - Qui ne consomment pas les réponses

    pour préserver le cadre asynchrone

    View full-size slide

  13. - Facilite et allège l’écriture de décorateurs

    => Symfony\...\HttpClient\DecoratorTrait

    - Pour la réponse qu’il nous indique de câbler

    => Symfony\...\HttpClient\Response\AsyncResponse

    AsyncDecoratorTrait

    View full-size slide

  14. AsyncDecoratorTrait
    Va venir se câbler sur la
    méthode stream de
    l’AsyncResponse
    Un point d’entrée
    Coeur de la
    logique métier

    View full-size slide

  15. Qui est cette AsyncResponse ?

    View full-size slide

  16. AsyncResponse: Qui suis je ?
    class MyExtendedHttpClient implements HttpClientInterface
    {
    use AsyncDecoratorTrait;
    public function request(string $method, string $url, array $options = []):
    ResponseInterface
    {
    $passthru = function (ChunkInterface $chunk, AsyncContext $context) {
    yield $chunk;
    };
    return new AsyncResponse($this->client, $method, $url, $options, $passthru);
    }
    }

    View full-size slide

  17. Trop de magie ?
    Démysti
    fi
    ons tout ça !

    View full-size slide

  18. AsyncResponse: comment je m’y prends?

    Le passthru callable
    /**
    * @param ?callable(ChunkInterface, AsyncContext): ?\Iterator
    $passthru
    */
    public function __construct(HttpClientInterface $client, string
    $method, string $url, array $options, callable $passthru = null)
    {
    - AsyncResponse: un dernier argument non banal

    View full-size slide

  19. AsyncResponse: comment je m’y prends?

    Le passthru callable
    - Une partie de logic « customizable »

    - Une dé
    fi
    nition dynamique, évolutive

    - Une dé
    fi
    nition qui doit respecter certaines règles

    $passthru = function (ChunkInterface $chunk, AsyncContext $context)
    {
    // do what you want with chunks
    yield $chunk;
    };

    View full-size slide

  20. AsyncResponse: comment je m’y prends?

    Le passthru callable
    Quid: Comment faire le lien entre cet itérateur et la
    réponse de l’AsyncResponse

    View full-size slide

  21. class AsyncContext
    {
    public function __construct(
    private ?callable &$passthru,
    private HttpClientInterface $client,
    private ResponseInterface &$response,
    private array &$info,
    private /*resource*/ $content,
    private int $offset,
    ) {
    }
    AsyncResponse: comment je m’y prends?

    Le passthru callable, un chunk et un context !

    View full-size slide

  22. AsyncResponse: comment je m’y prends?

    Le passthru callable, un chunk et un context !
    - DTO pour piloter l’AsyncResponse

    - Moyen de modi
    fi
    er le
    fl
    ux de réponse au niveau substantiel et temporel

    - Interroge la réponse sans pour autant la consommer

    - Agir sur la réponse (annulation, remplacement d’une réponse)

    - Agir sur l’itérateur maitrisant
    fl
    ux

    View full-size slide

  23. AsyncResponse: comment je m’y prends?

    Le passthru callable
    Quid du contrôle de ce qui a été manipulé

    View full-size slide

  24. Que fait AsyncResponse en interne ?
    Je suis gardien de la cohérence du

    fl
    ux de chunks :

    - pas de double
    fi
    rstChunk

    - Rien après le lastChunk

    Etc ..
    Générateur qui va émettre

    0, 1 ou N chunks

    View full-size slide

  25. Une simple question de timing et
    d’emboitement !
    AsyncDecoratorTrait

    View full-size slide

  26. AsyncDecoratorTrait
    - Symfony\Component\HttpClient\RetryableHttpClient

    - Symfony\Component\HttpClient\EventSourceHttpClient
    Décorateurs l’utilisant:
    Pour un aperçu des différentes pratiques:
    - Symfony\Component\HttpClient\Tests\AsyncDecoratorTraitTest

    View full-size slide

  27. A l’attaque ?

    View full-size slide

  28. Ne prenez pas peur !

    View full-size slide

  29. Transclusion de plusieurs points d’API
    - Requête principale sur une API privée pour récupérer des résultats avec
    pagination

    - Besoin d’y greffer des réponses de deux autres points d’API

    View full-size slide

  30. Transclusion de plusieurs points d’API
    public function index(MyExtendedHttpClient $client): Response
    {
    $response = $client->request(
    'GET',
    ‘https://my-url.com/collection',
    [
    // options you need etc
    'user_data' => [
    'add' => [
    'availabilities' => 'https://my-url.com/collections/{id}/something',
    'posts' => 'https://my-url.com/collections/{id}/somethingelse',
    ],
    'concurrency' => null
    ],
    ]
    );

    }

    View full-size slide

  31. Transclusion de plusieurs points d’API
    class MyExtendedHttpClient implements HttpClientInterface
    {
    use AsyncDecoratorTrait;

    public function request(string $method, string $url, array $options = []):
    ResponseInterface
    {

    $passthru = function (ChunkInterface $chunk, AsyncContext $context) use ($options) {

    }
    return new AsyncResponse($this->client, $method, $url, $options, $passthru);
    }

    }

    View full-size slide

  32. Transclusion de plusieurs points d’API
    - Recherche sur secteur de prédilection et si une 404 est retournée pour la page 1
    on relance pour rechercher secteur proche.

    - Si l’erreur est reçue sur une pagination haute, on laisse l’erreur.
    Premier enjeu:

    View full-size slide

  33. $passthru = function (ChunkInterface $chunk, AsyncContext $context) use ($options) {
    static $content = '';
    if ($chunk->isFirst()) {
    if (404 === $context->getStatusCode()) {
    $page = preg_split('/page=(\d)/', $context->getInfo()['original_url'], -1, PREG_SPLIT_DELIM_CAPTURE)[1];
    if (1 === (int) $page) {
    // look for another sector
    // do what you want
    $context->getResponse()->cancel();
    $context->replaceRequest(
    'GET',
    ‘https://my-url.com/collection', $options
    );
    return;
    }
    $context->passthru();
    }
    yield $chunk;
    }
    $content .= $chunk->getContent();
    if(!$chunk->isLast()) {
    return;
    }

    Annule la réponse
    Permet de relancer et permet de remplacer la

    réponse par le résultat de cette nouvelle requête

    Permet de remettre en position neutre en
    l’occurence l’itérateur qui
    fi
    ltre nos chunks =>
    pour recevoir 404

    View full-size slide

  34. [\n
    {\n
    "id": 1,\n
    "name": "Leanne Graham",\n
    "username": "Bret",\n
    "email": "[email protected]",\n
    "address": {\n
    "street": "Kulas Light",\n
    "suite": "Apt. 556",\n
    "city": "Gwenborough",\n
    "zipcode": "92998-3874",\n
    "geo": {\n
    "lat": "-37.3159",\n
    "lng": "81.1496"\n
    }\n
    },\n
    "phone": "1-770-736-8031 x56442",\n
    },\n
    {\n ……
    }]
    Extrait du JSON de la réponse principale si pas d’erreur

    View full-size slide

  35. Transclusion de plusieurs points d’API
    Deuxième enjeu:
    - Questionnement sur la meilleure façon d’aborder le problème.

    - Bonne performance et
    fl
    uidité => manipuler du json n’est pas toujours le plus léger

    - Nécessité d’avoir la possibilité de manipuler les bouts de réponses pour y effectuer
    des insertions de propriétés

    View full-size slide

  36. Transclusion de plusieurs points d’API
    Car le but est de leur inclure une information
    additionnelle par des sous requêtes lancées
    de manière concurrente
    Pourquoi?
    Et pouvoir facilement identi
    fi
    premier bloc à traiter

    View full-size slide

  37. array:10 [▼
    0 => '{'id':1,'name':'Leanne
    Graham','username':'Bret','email':'Sincere@april .
    biz','address':{'street':'Kulas Light','suite':'Apt .
    556','city':'Gwenborough','zipcode' ▶’
    1 => '{'id':2,'name':'Ervin
    Howell','username':'Antonette','email':'Shanna@melissa .
    tv','address':{'street':'Victor Plains','suite':'Suite
    879','city':'Wisokyburgh','z ▶'
    2 => ‘{…}

    ]
    JSON MANIPULABLE: parsedMainResponse

    View full-size slide

  38. public static function updateData(array $parsedMainResponse, AsyncContext $context, array $options): array
    {

    foreach ($parsedMainResponse as $mainKey => $subChunk) {
    ...
    $tempAdditionalResponses = [];

    foreach ($toAdd as $key => $url) {
    preg_match('/"id":(\d+)/',$subChunk, $match);

    $tempAdditionalResponses[] = sprintf(', "%s":', $key);
    $additionalResponse = $context->replaceRequest(
    'GET',
    $url,
    $options
    );
    $tempAdditionalResponses[] = $additionalResponse;
    }
    $tempAdditionalResponses[] = substr($subChunk, -1);

    $blocks[] = $tempAdditionalResponses;
    }
    $blocks[] = $original;
    return $blocks;
    }
    1.
    1. identi
    fi
    e la requête principale grace au
    contexte
    2.
    2.
    Manipuler les sous parties pour envisager
    une place pour les insertions
    3.
    3.
    replace la dernière parenthèse après

    les insertions
    tant que c'est pas le dernier sous bloc on
    ajoute délimiteur etc
    4.
    4.
    Dans le but de terminer sur la réponse
    pour laquelle la requête a été faite
    initialement

    View full-size slide

  39. array:7 [▼
    0 => '{'id':1,'name':'Leanne
    Graham','username':'Bret','email':'Sincere@april . biz','address':
    {'street':'Kulas Light','suite':'Apt . 556','city':'Gwenborough','zipcode' ▶'
    1 => ', 'availabilities':'
    2 => Symfony\Component\HttpClient\Response\TraceableResponse {#514 ▶}
    3 => ', 'posts':'
    4 => Symfony\Component\HttpClient\Response\TraceableResponse {#557 ▶}
    5 => '}'
    6 => ','
    ]
    Un bloc parmi les blocs
    Transclusion de plusieurs points d’API

    View full-size slide

  40. Transclusion de plusieurs points d’API
    //$parsedMainResponse = halaxa/json-machine ease json
    manipulation w/ PassThruDecoder
    yield $context->createChunk('[');
    //on va récupérer $part qui est un bloc (à ce
    niveau le 1er)
    while (null !== $chunk = self::passthru($context,
    $part)->current()) {
    yield $chunk;
    }
    $context->passthru(static function (ChunkInterface
    $chunk, AsyncContext $context)
    use (&$parsedMainResponse, &$part, &$blocks, $options) {

    }
    Sous requêtes de manière

    concurrente + distinction du

    premier bloc
    Itérateur parcouru jusqu’à ce qu’il

    atteigne une ResponseInterface:

    AsyncContext - replaceReponse

    fi
    nit un nouveau passthru

    callable par lequel passeront

    les chunks => en l’occurence

    reprendre sur la réponse de la

    première propriété ajoutée
    On est ici !

    View full-size slide

  41. Transclusion de plusieurs points d’API
    Rappel de la situation:
    Bout de réponse de

    requête principale
    Traitement
    du
    « premier»
    bloc
    Jusquà’ce qu’il tombe sur

    ResponseInterface
    is LastChunk ?
    Action: remplacer la réponse via

    AsyncContext
    On est ici !
    Découpage de la réponse

    de requête principale +

    Suivi des blocs re-manipulés

    View full-size slide

  42. Transclusion de plusieurs points d’API
    Troisième enjeu:
    - A partir de ce nouveau passthru, nécessité d’assurer une continuité pour reconstruire
    notre réponse principale tant que notre traitement n’est pas terminé.

    - Trois points important à gérer:

    - Quand un premier chunk est émis

    - Quand un dernier chunk est émis et qu’il y a toujours des blocs à traiter

    - Ce qui ne rentre pas dans un des cas précédents

    View full-size slide

  43. Transclusion de plusieurs points d’API
    - A déjà été « émis » et ne peut pas
    émettre plusieurs
    fi
    rstChunk
    - Important d’émettre ce qui est émis
    => dans notre cas il va s’agir des
    réponses aux sous requêtes
    if ($chunk->isFirst()) {
    return;
    }
    yield $chunk;

    View full-size slide

  44. Transclusion de plusieurs points d’API
    Quid du lastChunk émis relatif à la sous requête ?
    L’émettre ? Le manipuler ?
    Requête mal formée Oui mais comment ?

    View full-size slide

  45. Transclusion de plusieurs points d’API
    On reboucle avec l’AsyncContext !

    View full-size slide

  46. Transclusion de plusieurs points d’API
    Quand on arrive au lastChunk on va se trouver dans cette situation:
    array:4 [▼
    3 => ', 'posts':'
    4 =>
    Symfony\Component\HttpClient\
    Response\TraceableResponse
    {#550 ▶}
    5 => '}'
    6 => ','
    ]
    Bloc / Part
    array:7 [▼
    0 => '{'id':1,'name':'Leanne
    Graham','username':'Bret','email':'Sinc
    ere@april . biz','address':
    {'street':'Kulas Light','suite':'Apt .
    556','city':'Gwenborough','zipcode' ▶'
    1 => ', 'availabilities':'
    2 =>
    Symfony\Component\HttpClient\Response\T
    raceableResponse {#514 ▶}
    3 => ', 'posts':'
    4 =>
    Symfony\Component\HttpClient\Response\T
    raceableResponse {#557 ▶}
    5 => '}'
    6 => ','
    ]
    Bloc / Part

    View full-size slide

  47. Bloc à ce niveau
    créer le chunk à ce niveau pour l’émettre
    Remplacer la réponse pour pouvoir la
    traiter et sera émise au prochain passage
    Transclusion de plusieurs points d’API
    Que devons nous faire ?
    Créer un chunk à ce niveau et sera
    émis par la suite
    Avec l’aide de l’AsyncContext
    array:4 [▼
    3 => ', 'posts':'
    4 =>
    Symfony\Component\HttpClient\
    Response\TraceableResponse
    {#550 ▶}
    5 => '}'
    6 => ','
    ]

    View full-size slide

  48. Transclusion de plusieurs points d’API
    Bout de réponse
    de

    requête principale
    Traitement du
    bloc
    Jusqu'à ce qu’il tombe sur

    ResponseInterface
    is LastChunk?
    Action: remplacer la réponse via

    AsyncContext
    Ou
    Bloc
    fi
    ni
    Traitement bloc à n+1
    LastChunk
    Ignore les
    fi
    rstChunk
    Découpage de la réponse

    de requête principale +

    Suivi des blocs re-manipulés

    View full-size slide

  49. Transclusion de plusieurs points d’API
    Important d’identi
    fi
    er quand ça se termine
    - nécessité d’émettre un chunk
    - revenir sur la réponse pour
    laquelle on a fait l’appel
    initialement.
    Faire attention à bien reconstituer le json
    ! Syntax Error !

    View full-size slide

  50. Transclusion de plusieurs points d’API:

    Qu’est ce qu’on y a gagné
    - Flux de réponse facilement manipulable dans un
    cadre asynchrone préservé : un asyncContext bien
    utile

    - Des requêtes additionnelles effectuées de
    manière concurrente peu importe le sous objet
    qu’elles référencent dans la première réponse

    - Une très bonne performance

    QUID de la limite du nombre de requêtes

    concurrentes

    émises ?
    Simulation sur fake api

    View full-size slide

  51. Transclusion de plusieurs points d’API
    Et la limite sur le nombre de requêtes concurrentes
    dans tout ça ?
    public static function updateData(array $parsedMainResponse, AsyncContext $context, array
    $options): array
    {

    $concurrencyLimit = (int) $options['user_data']['concurrency'];

    foreach ($parsedMainResponse as $mainKey => $subChunk) {
    if ($concurrencyLimit && $concurrencyLimit < $mainKey) {
    break;
    }

    }

    }

    View full-size slide

  52. Transclusion de plusieurs points d’API
    Bout de réponse
    de

    requête
    principale
    Découpage de la réponse

    de requête principale

    +

    Suivi des blocs re-manipulés
    Traitement
    du bloc
    Jusqu'à ce qu’il tombe sur

    ResponseInterface
    is LastChunk ?
    Action: remplacer la réponse via

    AsyncContext
    Ou
    Bloc
    fi
    ni
    Traitement bloc à n+1
    Un nombre limité de blocs re

    manipulés
    LastChunk
    Ignore les
    fi
    rstChunk
    Quatrième enjeu : contrôle des
    requêtes concurrentes

    View full-size slide

  53. Transclusion de plusieurs points d’API
    - Un
    fl
    ux de requêtes
    concurrentes facilement
    identi
    fi
    able et manipulable

    - AsyncContext : fonctionnalité de
    « pause » si nécessité également

    Simulation sur fake api

    View full-size slide

  54. Jongler en asynchrone avec Symfony HttpClient

    Et donc au
    fi
    nal ?
    - Un AsyncDecoratorTrait indispensable ?

    - Un combo « générateur » + AsyncContext + AsyncResponse gagnant

    - Un combo gagnant mais pas pour toutes les situations non plus

    - Une
    fl
    exibilité dans la manipulation des chunks non handicapante pour la performance

    View full-size slide

  55. Jongler en asynchrone avec Symfony HttpClient

    Et donc au
    fi
    nal ?
    Mais, vous savez, moi je ne crois pas qu’il y ait de bon ou de mauvais processus en soi. Moi,
    si je devais résumer la vie de Symfony HttpClient aujourd’hui avec vous, je dirais que c’est
    d’abord des recherches, une documentation qui m’a tendu la main, peut-être à un moment où
    je ne pouvais pas, où j’étais seule dans mes pensées. Et c’est assez curieux de se dire que
    les hasards, les frustrations, les documentations forgent une destinée… Parce que quand on
    a le goût de la chose, quand on a le goût de la chose bien faite, le beau geste, parfois on ne
    trouve pas le processus en face, je dirais, le processus qui vous aide à avancer. Alors ce n’est
    pas mon cas, comme je le disais là, puisque moi au contraire, j’ai pu ; et je dis Merci à
    l’AsyncDecoratorTrait, je dis merci à la communauté Symfony…
    Monologue de Otis dans Asterix et Cléopatre revisité

    View full-size slide