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

Jongler en asynchrone avec Symfony HttpClient

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 Slide

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

    View Slide

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

    View Slide

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

    View 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 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 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 Slide

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

    View Slide

  9. Con
    fi
    rmation d’un besoin ?

    View Slide

  10. - un réel besoin

    - décorateurs / middlewares / event ?

    - préserver la dimension asynchrone

    Manipulation des chunks

    View Slide

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

    View Slide

  12. AsyncDecoratorTrait
    - Pour des décorateurs de HttpClient

    - Qui ne consomment pas les réponses

    pour préserver le cadre asynchrone

    View 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 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 Slide

  15. Qui est cette AsyncResponse ?

    View 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 Slide

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

    View 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 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 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 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 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 Slide

  23. AsyncResponse: comment je m’y prends?

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

    View 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 Slide

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

    View 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 Slide

  27. A l’attaque ?

    View Slide

  28. Ne prenez pas peur !

    View 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 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 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 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 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 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 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 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 Slide

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

    ]
    JSON MANIPULABLE: parsedMainResponse

    View 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 Slide

  39. array:7 [▼
    0 => '{'id':1,'name':'Leanne
    Graham','username':'Bret','email':'[email protected] . 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 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 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 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 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 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 Slide

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

    View 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
    [email protected] . 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 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 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 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 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 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 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 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 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 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 Slide