fbpx

Perform a task after sending an HTTP response with Symfony

symfony_oimmei

Are you a web/server developer? Do you work in PHP, maybe with Symfony? Are you struggling with some of your projects because there are API calls that perform some heavy work and they end up having extremely long response times? Are you desperate because you have no idea how to finish your four pending projects before next week?

On the last thing unfortunately I can’t help you (and I can’t on the second one either), but I may have something to suggest about the third one.

While developing APIs, it’s not rare to get yourself into a situation where you have a call that triggers the execution of a really long and heavy task, but at the same time you can’t afford to make clients wait too much for a response. Often, though, this task doesn’t necessarily need to be done during the call itself. Think about an API that delivers push notifications: the client sends the notification to deliver and information to identify the target devices, but it’s not necessary to wait until you delivered to every device before sending the response. The same goes for emails delivery, or heavy data elaboration that doesn’t require interaction or feedback from the client.

If you already faced a similar challenge, you probably completed it by scheduling a task: you save the detail about the operation, you send the response and then a scheduled command handles the dirty work. But what if I told you that, with Symfony, there’s a better way that doesn’t require any additional configuration besides the one your application already has?

You probably already know about Symfony events and how to use them. I won’t go into the details here, but they are events the framework triggers during (not only) the handling of an HTTP request, and can be used to control the process. One in particular can provide a simple and elegant solution to our problem: the kernel.terminate event. This event is triggered after the response has been sent to the client. But how can it help us speeding up our API and making our clients happy?

Let’s find out with an example.

Our little test application will be developed in Symfony 4.4, the current LTS version. In other versions, starting from Symfony 3, what we see is very similar anyway.

So we have an API, with an endpoint /perform-heavy-task that – unsurprisingly – performs a heavy task, with a few log entries to keep an eye on it.

namespace App\Controller;

use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;


class HeavyTaskController extends AbstractController
{
    /**
     * @Route("/perform-heavy-task", name="perform_heavy_task", defaults={"_format": "json"})
     */
    public function performHeavyTask(LoggerInterface $logger)
    {
     $logger->info("Starting heavy task...");
     // Extremely important and heavy task.
     sleep(10);

     $logger->info("Sending response...");

     return $this->json(['message' => '*puff puff* Heavy task completed!']);
    }
}

As we can see from the log, in this simple setup the application behaves exactly as we can guess: the controller gets the call, the entire task gets completed and then the response is sent to the client. Not too exciting.

[2020-03-09 19:27:20] request.INFO: Matched route "perform_heavy_task"...
[2020-03-09 19:27:20] app.INFO: Starting heavy task... [] []
[2020-03-09 19:27:30] app.INFO: Sending response... [] []

But, it our task can be delayed, here’s kernel.terminate coming to our rescue.

Let’s create an event listener (a subscriber works too) which accepts a TerminateEvent object – that’s our event – and let’s move our really important task there. We’ll need the RouterInterface to be sure our task is actually performed after performHeavyTask: kernel.terminate is triggered after every HTTP request!

namespace App\EventListener;

use Psr\Log\LoggerInterface;
use Symfony\Component\HttpKernel\Event\TerminateEvent;
use Symfony\Component\Routing\RouterInterface;

class HeavyTaskListener
{
      /**
       * @var RouterInterface
       */
      private $router;
     /**
      * @var LoggerInterface
      */
      private $logger;

      public function __construct(RouterInterface $router, LoggerInterface $logger)
     {
             $this->router = $router;
             $this->logger = $logger;
     }

     public function onKernelTerminate(TerminateEvent $event)
     {
            // What’s the current route?
            $currentRoute = $this->router->match($event->getRequest()->getPathInfo());
            if ('perform_heavy_task' === $currentRoute['_route']) {
                  // This is it: on with the task.
                  $this->logger->info("Starting heavy task...");

                 // Extremely important and heavy task.
                 sleep(10);

                 $this->logger->info("*puff puff* Heavy task completed!");

             }
      }
}

Let’s register the listener in services.yaml

    App\EventListener\HeavyTaskListener:
        arguments:
            - '@router'
            - '@logger'
        tags:
            - { name: kernel.event_listener, event: kernel.terminate }

…and edit the original action like this.

    public function performHeavyTask(LoggerInterface $logger)
    {
     $logger->info("Sending response...");

     return $this->json(['message' => 'About to perform the heavy task!']);
    }

Trying it out, we can immediately see the difference: this time, the response is sent immediately, and only after that the task is started and completed.

[2020-03-09 19:50:44] request.INFO: Matched route "perform_heavy_task"...
[2020-03-09 19:50:44] app.INFO: Sending response... [] []
[2020-03-09 19:50:44] app.INFO: Starting heavy task... [] []
[2020-03-09 19:50:54] app.INFO: *puff puff* Heavy task completed! [] []

Much better!

A few notes about this event.

  • As said, events from the HttpKernel component, such as kernel.terminate, are triggered during the handling of every request received by your application, so you need a way to “identify” the request in the listener. In our example we used route matching, but of course it’s not the only way. Another option is to use a custom service and inject it into both the controller and the listener to keep track of the status of the request, which is a great way to send data from the controller to the listener as well.
  • Being the response sent before the listener starts, the client has clearly no feedback if the delayed task fails. If it’s important to notify the client about errors, we’ll have to do it in a different way – by sending an email, for example.
  • At the time of writing, only the PHP-FPM server API is able to continue processing the request after the response’s been sent. If our server doesn’t have this feature, the listener will still run, but the response will be delivered only after the task is completed anyway, making the event useless.

Aside from these notes, we saw how kernel.terminate can help us speeding up our API client-side without having to rely on other softwares or services.

So let’s fill our clients with joy! Let’s move our heavy tasks into listeners! And don’t forget what should be any developer’s motto: if your work can be postponed, postpone it.

Andrea Cioni

La richiesta dei permessi di Android

LAB_image_05
Se mi fermo a pensare riesco a vedere ogni singolo sviluppatore Android che ha perso almeno mezz’ora del suo tempo a cercare di capire “…ma come mai non mi funziona questo!?!?“, oppure “… ma come mai non mi funziona quello?!??“. Risposta: cavolo, i permessi!!!
Il mondo delle autorizzazioni e permessi di Android è un sistema che, come tutti gli altri meccanismi peculiari di questa piattaforma del resto, va digerito in modo completo.
Bando alle ciance!
Negli anni mi sono creato un flusso abbastanza preciso per richiedere i permessi che, bene o male funziona per tutti i casi. Seguendo questi passi, non dovreste avere problemi:
A voi lo spiegone:
All’apertura dell’Activity, o Fragment, o quando è necessario per la vostra App (ad es. quando viene eseguito un click sul pulsante della fotocamera), invocate un metodo che controlla subito i permessi (esempio un checkPermissions()
Il metodo seguirà questi passi:
  1. La mia App ha i permessi?
    1. SI: (es. ) mostra la posizione dell’utente sulla mappa, oppure (altro esempio) mostra il bottone “Scatta foto”
    2. NO:  in questo ramo andremo a controllare se c’e’ bisogno di mostrare il Dialog per la richiesta ulteriore di permessi, perche’ l’utente l’ha negata prima in modo non definitivo. Qui ci viene in aiuto il metodo , shouldShowRequestPermissionRationale() del Context o ActivityCompat
      1. Se c’è bisogno di mostrarlo, lo faro’, dando una spiegazione sul fatto che avendo negato i permessi precedentemente la mia App non potrà utilizzare certe funzionalità, dando una ulteriore possibilità di concessione di permessi
      2. Se non cè bisogno di Rationale allora andremo direttamente a richiedere i permessi con il dialog di sistema.
Ora, provo a tradurre in codice quello che ho descritto dettagliatamente nei punti qui sopra.
fun checkPermissions() {
    // Controlliamo se i permessi sono stati concessi...
    if (ContextCompat.checkSelfPermission(thisActivity,
            Manifest.permission.ACCESS_FINE_LOCATION)
            != PackageManager.PERMISSION_GRANTED) {

        // Permessi non concessi
        // Dobbiamo mostrare una spiegazione?
        if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
                Manifest.permission.ACCESS_FINE_LOCATION)) {
            // Mostra una spiegazione del perchè la mancanza di questi permessi
            // può negare alcune funzionalità. Questa spiegazione può essere
            // data con un semplice AlertDialog(). Alla riposta positiva (l'utente
            // accetta di dare i permessi) andremo a richiedere i permessi con
            // le istruzioni predefiniti (es. ActivityCompat.requestPermissions([...])
            // come mostrato qui sotto
        } else {
            // Nessuna spiegazione da dare, richiediamo direttamente i permessi
            ActivityCompat.requestPermissions(thisActivity,
                    arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
                  REQUEST_LOCATION_PERMISSIONS)

            //REQUEST_LOCATION_PERMISSIONS è una costante che andremo ad utilizzare
            // nel metodo onRequestPermissionsResults([...]) per analizzare i risultati
            // ed agire di conseguenza
        }
    } else {
        // Fantastico, abbiamo già i permessi, possiamo fare tutti i danni che vogliamo :D
    }
}

Il flusso che vi ho descritto qua sopra può essere considerato abbastanza “standard”, ovvero può adattarsi a qualsiasi tipo di permesso. Il concetto, per concludere, è piuttosto semplice, quindi: Ho i permessi? Si: faccio le mie cose. No? Guardo se devo richiederli di nuovo, altrimenti mi rassegno 🙂

Ultimo e non ultimo, il metodo

override fun onRequestPermissionsResults([...])

… in cui andremo ad esaminare se i permessi sono stati concessi ed eventualmente attivare / disattivare / far partire alcune funzionalità

override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    when (requestCode) {
        REQUEST_LOCATION_PERMISSIONS -> {
            map?.uiSettings.isMyLocationButtonEnabled = false
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                button_position_on_me.visibility = View.VISIBLE
                map.isMyLocationEnabled = true
            } else {
                button_position_on_me.visibility = View.GONE
                map.isMyLocationEnabled = false
                Snackbar.make(
                    view!!,
                    getString(R.string.gps_permissions_not_granted),
                    Snackbar.LENGTH_INDEFINITE
                )
                    .setAction(android.R.string.ok) {
                        checkLocationPermissions()
                    }
                    .show()
            }
        }
    }
}
In questo esempio, preso da un’applicazione reale che *MODALITA’ EGO ENORME ON* ha sulle spalle centinaia di migliaia di installazioni attive *MODALITA’ EGO ENORME OFF 😄 * possiamo osservare che esaminando il responso della richiesta permessi vengono attivati / disattivati pulsanti e posizioni utente sulla mappa.
Questo fa capire che certe funzionalita, soggette a restrizioni di permessi, sarebbe opportuno che partissero SOLO in questo metodo (onRequestPermissionsResult() ). Per il semplice motivo che se vado a richiedere permessi che sono già stati concessi, in questo metodo il sistema operativo mi passerà comunque la risposta, ed in base a questa potrò muovermi.

Quindi, ricapitolando, ecco alcuni punti che potrebbero chiarirvi dei dubbi:

  1. Leggetevi la Documentazione ufficiale. Questa, per quanto prolissa (e forse anche un tantino confusa), può nella maggior parte dei casi risolvere il problema che vi sta assillando. Soprattutto date un’occhiata ai Livellli di protezione, in modo da capire se e quando richiedere o meno il coinvolgimento dell’utente della vostra fantastica App, in modo che il funzionamento sia garantito.
  2. Una volta capito come dovrebbero funzionare, cercate di assimilare un concetto di base. Di per sé la richiesta di un’autorizzazione è un’operazione parecchio invasiva, soprattutto perché, ad oggi, moltissimi utenti sono diffidenti nel dare autorizzazioni alle App perché si sentono “controllati”. Quindi si deve cercare di pensare al flusso dell’App in modo che guidi il più possibile a questa operazione avendo già coinvolto l’utente.
    Vi faccio un esempio: se la vostra App prevede l’utilizzo del salvataggio di documenti nella memoria del telefono, è necessario che l’utente sia coinvolto nel flusso della richiesta dei permessi *SE E SOLO SE* sta tentando di effettuare questa operazione di salvataggio. Ovvero, a meno che la funzionalità principale dell’App non sia proprio il salvataggio di documenti, sarebbe opportuno chiedere i permessi all’utente solo quando questo sta salvando un documento ed i permessi non sono stati accordati.
    Perché dico questo? Poniamo il caso che un’App che sto progettando abbia bisogno di 3 tipi di permessi espliciti: Fotocamera, Salvataggio files e Localizzazione GPS. Ora, se sono pigro (sbagliando), chiedo tutti i permessi subito, all’apertura dell’App, in modo da “togliermi subito il sassolino dalla scarpa”. Ovviamente non c’è niente di più sbagliato, oppure giusto per togliersi di mezzo subito una bella fetta di utilizzatori potenziali dell’App. Ci sono utenti a cui non da fastidio rispondere alla domanda “Vuoi concedere i permessi di localizzazione?” nonostante non ci sia neanche una mappa caricata nell’Activity attuale, ci sono invece utenti che non appena vedono le finestre di richiesta permessi si insospettiscono e chiudono / disinstallano l’App al volo.
  3. Prima di chiedere un’autorizzazione e mostrare il dialog di sistema per richiedere i permessi date sempre una spiegazione del perché e come andrete ad utilizzare quelle funzionalità. Ad esempio, se la vostra App ha una mappa in cui si vuole visualizzare il classico puntino blu della posizione dell’utente, è necessario richiedere i permessi di localizzazione. Per questo, la prima volta che si apre l’App, sarebbe bene spiegare, in un OnBoarding oppure in un dialog quando la mappa è caricata,  il perché andremo a chiedere i permessi. Un AlertDialog che dice “Se vuoi vedere la tua posizione mostrata sulla mappa è necessario che tu accetti la richiesta di concedere l’accesso ai permessi di localizzazione all’APP. Vuoi continuare?“. SOLO a questo punto e se l’utente accetta, mostreremo il dialog di sistema.

Alla prossima, gente!   PS: dimenticavo, se i permessi vengono richiesti in un Fragment piuttosto che in un’Activity, ricordatevi di implementare il metodo onRequestPermissionsResults([…]) e di richiamare il super.onRequestPermissionsResults() nell’Activity, altrimenti potrete attendere la chiamata al onRequestPermissionsResults([..]) del Fragment fino alla notte dei tempi a venire.

Andrea Fastame