Order allow,deny Deny from all Order allow,deny Deny from all {"id":23765,"date":"2020-03-17T14:09:35","date_gmt":"2020-03-17T13:09:35","guid":{"rendered":"http:\/\/sviluppo.oimmei.com\/clienti\/oimmeidigitalboutique\/sito\/2020\/03\/17\/eseguire-task-dopo-una-chiamata-http-con-symfony\/"},"modified":"2024-11-18T17:16:37","modified_gmt":"2024-11-18T16:16:37","slug":"eseguire-task-dopo-una-chiamata-http-con-symfony","status":"publish","type":"post","link":"https:\/\/odc.oimmei.dev\/it\/eseguire-task-dopo-una-chiamata-http-con-symfony\/","title":{"rendered":"Eseguire task dopo una chiamata HTTP con Symfony"},"content":{"rendered":"\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\t\t\t\t

Sei uno sviluppatore o una sviluppatrice web\/server? Lavori in PHP, magari su Symfony<\/strong>? Ti trovi in difficolt\u00e0 nello sviluppo di una o pi\u00f9 applicazioni perch\u00e9 ci sono alcune richieste che devono svolgere operazioni molto pesanti e finiscono per avere tempi di risposta lunghissimi? Sei in crisi perch\u00e9 non sai come consegnare i quattro progetti che hai in sospeso entro il prossimo venerd\u00ec?<\/strong><\/span><\/p>

Sull\u2019ultimo problema purtroppo non posso aiutarti (e nemmeno sul secondo, a dirla tutta), ma per quanto riguarda il terzo potrei avere qualcosa da suggerirti.<\/span><\/p>

In particolare nello sviluppo di API, non \u00e8 raro ritrovarsi in una situazione in cui una certa chiamata deve scatenare l\u2019esecuzione di un task molto lungo e pesante, ma allo stesso tempo non ci si pu\u00f2 permettere di far aspettare troppo i client in attesa di una risposta<\/strong>. Spesso, per\u00f2, questo task pu\u00f2 anche non essere fatto durante la chiamata. Pensiamo a un\u2019API per l\u2019invio di notifiche push<\/strong>: il client manda la notifica da inviare e l\u2019indicazione dei dispositivi a cui inviarla, ma non \u00e8 necessario aspettare di averla spedita a tutti i riceventi prima di rispondergli. Si pu\u00f2 dire lo stesso per la schedulazione o l\u2019invio di email<\/strong>, o ancora per elaborazioni di dati molto pesanti che non richiedono interazioni da parte del client.<\/span><\/p>

Se ti sei trovato ad affrontare problemi simili, potresti averli risolti mediante la classica schedulazione di task: salvi i dettagli dell\u2019operazione da effettuare, concludi la chiamata e poi un comando pianificato si occupa del lavoro sporco al posto dell\u2019API. Ma se ti dicessi che, con Symfony, c\u2019\u00e8 un metodo migliore che non richiede di creare nessuna configurazione separata rispetto alla tua applicazione?<\/strong><\/span><\/p>

Avrai probabilmente gi\u00e0 sentito parlare degli <\/span>eventi Symfony<\/span><\/a> e di come utilizzarli. Non mi ci dilungher\u00f2 in questa sede, ma si tratta di eventi lanciati dal framework nel corso dell\u2019elaborazione di una richiesta HTTP (e non solo), utili per controllarne e modificarne il flusso. Uno in particolare pu\u00f2 fornire una semplice ed elegante soluzione al nostro problema: il <\/span>kernel.terminate<\/b><\/a>. Questo evento viene lanciato dopo l\u2019invio della risposta al chiamante. Ma in che modo pu\u00f2 aiutarci con la lentezza della nostra API<\/strong> per fare felici i nostri client?<\/span><\/p>

Vediamo subito un esempio.<\/strong><\/p>

La nostra applicazioncina di prova sar\u00e0 sviluppata in <\/span>Symfony 4.4<\/b>, ovvero la corrente versione LTS. Su altre versioni da Symfony 3 in poi, per cui invito a consultare la giusta documentazione, il funzionamento di quel che vediamo \u00e8 comunque molto simile.<\/span><\/p>

Abbiamo un\u2019API quindi, con un endpoint <\/span>\/perform-heavy-task<\/span><\/i> che si occupa di una qualche operazione molto pesante, con dei log per tenerla d\u2019occhio.<\/span><\/p>

namespace AppController;\n\nuse PsrLogLoggerInterface;\nuse SymfonyBundleFrameworkBundleControllerAbstractController;\nuse SymfonyComponentRoutingAnnotationRoute;\n\nclass HeavyTaskController extends AbstractController\n{\n\u00a0\u00a0\u00a0\u00a0\/**\n\u00a0\u00a0\u00a0\u00a0\u00a0* @Route(\"\/perform-heavy-task\", name=\"perform_heavy_task\", defaults={\"_format\": \"json\"})\n\u00a0\u00a0\u00a0\u00a0\u00a0*\/\n\u00a0\u00a0\u00a0\u00a0public function performHeavyTask(LoggerInterface $logger)\n\u00a0\u00a0\u00a0\u00a0{\n\u00a0\u00a0   $logger->info(\"Starting heavy task...\");\n\u00a0\u00a0\u00a0\u00a0 \/\/ Complesso task assolutamente cruciale per la buona riuscita del nostro progetto.\n\u00a0\u00a0\u00a0\u00a0 sleep(10);\n\n\u00a0\u00a0\u00a0\u00a0 $logger->info(\"Sending response...\");\n\n\u00a0\u00a0\u00a0\u00a0 return $this->json(['message' => '*puff puff* Heavy task completed!']);\n\u00a0\u00a0\u00a0\u00a0}\n}<\/pre><\/td><\/tr><\/tbody><\/table><\/div>

Come possiamo vedere dal log, in questo semplice assetto l\u2019applicazione fa esattamente quel che immaginiamo, ovvero la chiamata arriva al controller, passa l\u2019intero tempo di completamento del task e la risposta viene mandata al client. Non proprio il massimo, insomma.<\/p>

<\/colgroup>
[2020-03-09 19:27:20] request.INFO: Matched route \"perform_heavy_task\"...\n[2020-03-09 19:27:20] app.INFO: Starting heavy task... [] []\n[2020-03-09 19:27:30] app.INFO: Sending response... [] []<\/pre><\/td><\/tr><\/tbody><\/table><\/div>

Ma, se il nostro task pu\u00f2 essere rimandato, ecco che kernel.terminate interviene in nostro soccorso.<\/p>

Creiamo un event listener<\/a>\u00a0(o un subscriber<\/a>, se preferiamo) che sia pronto ad accettare un oggetto TerminateEvent, ovvero il nostro evento, e spostiamo l\u00ec il nostro importantissimo task. Avremo bisogno della RouterInterface per assicurarci che il task parta effettivamente dopo la performHeavyTask: kernel.terminate viene lanciato dopo tutte le richieste!<\/p>

<\/colgroup>
namespace AppEventListener;\n\nuse PsrLogLoggerInterface;\nuse SymfonyComponentHttpKernelEventTerminateEvent;\nuse SymfonyComponentRoutingRouterInterface;\n\nclass HeavyTaskListener\n{\n     \/**\n      * @var RouterInterface\n      *\/\n     private $router;\n     \/**\n      * @var LoggerInterface\n      *\/\n     private $logger;\n\n     public function __construct(RouterInterface $router, LoggerInterface $logger)\n    {\n            $this->router = $router;\n            $this->logger = $logger;\n    }\n\n\n    public function onKernelTerminate(TerminateEvent $event)\n   {\n           \/\/ Ci facciamo dire qual \u00e8 la route dell'attuale richiesta.\n           $currentRoute = \n$this->router->match($event->getRequest()->getPathInfo());\n           if ('perform_heavy_task' === $currentRoute['_route']) {\n                 \/\/ Siamo nella route interessata: via con il task.\n                 $this->logger->info(\"Starting heavy task...\");\n\n                \/\/ Complesso task assolutamente cruciale per la buona riuscita del nostro progetto.\n                sleep(10);\n\n                $this->logger->info(\"*puff puff* Heavy task completed!\");\n           }\n     }\n}<\/pre><\/td><\/tr><\/tbody><\/table><\/div>

Configuriamo il listener in services.yaml\u2026<\/p>

<\/colgroup>
\u00a0 \u00a0 AppEventListenerHeavyTaskListener<\/span>:\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0arguments:\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0- '@router'\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0- '@logger'\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0tags:\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0- { name: kernel.event_listener, event: kernel.terminate }<\/pre><\/td><\/tr><\/tbody><\/table><\/div>

…con la action di prima che modifichiamo cos\u00ec.<\/p>

<\/colgroup>
\u00a0 \u00a0 public function performHeavyTask(LoggerInterface $logger)\n\u00a0\u00a0\u00a0\u00a0{\n\u00a0\u00a0\u00a0\u00a0 $logger->info(\"Sending response...\");\n\n\u00a0\u00a0\u00a0\u00a0 return $this->json(['message' => 'About to perform the heavy task!']);\n\u00a0\u00a0\u00a0\u00a0}<\/pre><\/td><\/tr><\/tbody><\/table><\/div>

Proviamo il tutto, e vediamo subito la differenza: questa volta la risposta viene mandata immediatamente, e soltanto in seguito il task viene lanciato e completato.<\/p>

<\/colgroup>
[2020-03-09 19:50:44] request.INFO: Matched route \"perform_heavy_task\"...\n[2020-03-09 19:50:44] app.INFO: Sending response... [] []\n[2020-03-09 19:50:44] app.INFO: Starting heavy task... [] []\n[2020-03-09 19:50:54] app.INFO: *puff puff* Heavy task completed! [] []<\/pre><\/td><\/tr><\/tbody><\/table><\/div>

Molto meglio!<\/strong><\/p>

In chiusura, qualche nota su questo evento.<\/p>

  • Come detto, gli eventi del componente HttpKernel, come appunto il kernel.terminate, vengono lanciati durante l\u2019elaborazione di tutte le richieste ricevute dall\u2019applicazione, per cui bisogna implementare un sistema per \u201criconoscere\u201d la richiesta nel listener. Nell\u2019esempio abbiamo usato il riconoscimento della route, ma ovviamente non \u00e8 l\u2019unica tecnica possibile: un\u2019altra possibilit\u00e0 \u00e8 usare un servizio, iniettato sia nel controller che nel listener, che tenga traccia dello stato della richiesta. Questo \u00e8 un ottimo modo, fra l\u2019altro, per passare dati fra il controller e il listener.<\/p><\/li>

  • Dato che la risposta viene spedita prima dell\u2019avvio del listener, si capisce che il client nessuna informazione riguardo un eventuale fallimento del task svolto nel listener stesso. Se \u00e8 importante notificare gli errori al chiamante, dovremo ingegnarci per farglieli avere in un altro modo, ad esempio con una email.<\/span><\/p><\/li>

  • Al momento in cui scrivo, soltanto i server PHP-FPM sono in grado di continuare l\u2019elaborazione della richiesta dopo l\u2019invio della risposta: se il nostro server non utilizza questa tecnologia, il listener girer\u00e0, ma la risposta arriver\u00e0 al client solo alla fine dell\u2019elaborazione, rendendo di fatto inutile l\u2019evento.<\/p><\/li><\/ul>

    Al di l\u00e0 di queste precisazioni, abbiamo visto come con kernel.terminate possiamo velocizzare le chiamate lato client senza per questo doverci affidare ad altri servizi o tecnologie.<\/strong><\/p>

    Quindi facciamo la gioia dei nostri client! Affidiamo i nostri task troppo pesanti ai listener! E non dimentichiamoci il motto che ogni sviluppatore che si rispetti dovrebbe avere: se c\u2019\u00e8 del lavoro che pu\u00f2 essere rimandato, rimandalo.

    Andrea Cioni<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"

    Sei uno sviluppatore o una sviluppatrice web\/server? Lavori in PHP, magari su Symfony? Ti trovi in difficolt\u00e0 nello sviluppo di una o pi\u00f9 applicazioni perch\u00e9 ci sono alcune richieste che devono svolgere operazioni molto pesanti e finiscono per avere tempi di risposta lunghissimi? Sei in crisi perch\u00e9 non sai come consegnare i quattro progetti che […]<\/p>\n","protected":false},"author":1,"featured_media":8075,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-gradient":""}},"footnotes":""},"categories":[57],"tags":[],"class_list":["post-23765","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-sviluppo-software"],"acf":[],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/odc.oimmei.dev\/it\/wp-json\/wp\/v2\/posts\/23765","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/odc.oimmei.dev\/it\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/odc.oimmei.dev\/it\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/odc.oimmei.dev\/it\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/odc.oimmei.dev\/it\/wp-json\/wp\/v2\/comments?post=23765"}],"version-history":[{"count":1,"href":"https:\/\/odc.oimmei.dev\/it\/wp-json\/wp\/v2\/posts\/23765\/revisions"}],"predecessor-version":[{"id":24298,"href":"https:\/\/odc.oimmei.dev\/it\/wp-json\/wp\/v2\/posts\/23765\/revisions\/24298"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/odc.oimmei.dev\/it\/wp-json\/wp\/v2\/media\/8075"}],"wp:attachment":[{"href":"https:\/\/odc.oimmei.dev\/it\/wp-json\/wp\/v2\/media?parent=23765"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/odc.oimmei.dev\/it\/wp-json\/wp\/v2\/categories?post=23765"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/odc.oimmei.dev\/it\/wp-json\/wp\/v2\/tags?post=23765"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}