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. Jongler en asynchrone avec Symfony HttpClient Symfony HttpClient : quelques

    rappels L’AsyncDecoratorTrait et son « contexte » Et si on passait à la pratique ?
  2. HttpClient: quelques rappels - un composant standalone - pour consommer

    des APIs - par des opérations synchrones ou asynchrones - « Native PHP Streams » et cURL.
  3. 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..’, […] );
  4. 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 { … } }
  5. - un réel besoin - décorateurs / middlewares / event

    ? - préserver la dimension asynchrone Manipulation des chunks
  6. AsyncDecoratorTrait - Pour des décorateurs de HttpClient - Qui ne

    consomment pas les réponses 
 pour préserver le cadre asynchrone
  7. - 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
  8. 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
  9. 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); } }
  10. 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
  11. 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; };
  12. 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
  13. 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 !
  14. 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
  15. 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
  16. 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
  17. 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 ], ] ); … }
  18. 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); } … }
  19. 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:
  20. $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
  21. [\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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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 Dé 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 !
  28. 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
  29. 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
  30. 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;
  31. 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 ?
  32. 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
  33. 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 => ',' ]
  34. 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
  35. 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 !
  36. 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
  37. 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; } … } … }
  38. 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
  39. 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
  40. 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
  41. 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é