Présentation des taches asynchrones

Gestion des opérations dans le 2e plan

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com

La différence importante entre les applications écrites en Java et celles en PHP est la facilité de mise en place de certaines fonctionnalités. En Java, et plus particulièrement dans Spring, l'ajout des tâches à exécuter au deuxième plan est extrêmement facile. Et en plus, l'exécution de ces tâches peut s'étaler tout au long du cycle de vie de notre application (jusqu'à son arrêt).

Pour effectuer la même chose avec PHP il faudrait jouer avec des configurations, CRONs (...) tandis qu'en Spring les tâches planifiées font partie intégrale du framework. Il suffit d'activer la gestion par annotations associée au namespace task de notre fichier de configuration. On verra cela plus loin. Au début on présentera rapidement la gestion des tâches planifiées dans Spring.

Exécution des tâches planifiées sous Spring

Grâce à ses deux interfaces, TaskExecutor et TaskScheduler, Spring permet de gérer facilement les tâches planifiées (scheduled tasks) et celles doivent s'exécuter d'une manière asynchrone. L'asynchrone, qu'est-ce que cela veut dire ? Dans le mondes des applications web, cette nomination signifie l'exécution des tâches sans aucune intervention de l'utilisateur. Dans le modèle synchronisé, l'utilisateur fait une requête pour obtenir la réponse du serveur. Dans le modèle désynchronisé, l'utilisateur fait une requête, le serveur lui répond. Mais la réponse ne doit pas être la dernière action du serveur. Ce dernier peut, sans aucune demande de l'utilisateur, envoyer de nouvelles informations.

En ce qui concerne les tâches planifiées, il s'agit des actions qui doivent être effectuées à un moment donné, par exemple à 22h20 de chaque samedi ou toutes les 20 minutes après le redémarrage de l'application.

Comment TaskExecutor et TaskScheduler peuvent aider à gérer ces tâches ? Concernant l'abstraction TaskExecutor, elle est identique à l'interface java.util.concurrent.Executor. Elle posède une seule méthode qui permet de lancer une tâche. Cette dernière doit implémenter l'interface Runnable. Plusieurs implémentations du TaskExecutor sont proposées dans Spring :
- SimpleAsyncTaskExecutor : chaque tâche est exécutée dans un nouveau thread.
- SyncTaskExecutor : les tâches ne sont pas invoquées de manière asynchrone. Elles sont toutes exécutées dans le thread qui les appelle.
- ConcurrentTaskExecutor
- SimpleThreadPoolTaskExecutor
- ThreadPoolTaskExecutor
- TimerTaskExecutor :
- WorkManagerTaskExecutor :

TaskScheduler, quant à lui, a été introduit dans la 3e version du Spring. Il permet, à travers de ses multiples méthodes, d'exécuter des tâches dans un point précis dans le futur. Les tâches peuvent dont être exécutées à une date précise, à une intervalle déterminée ou à une intervalle précise à partir d'une date.

Trigger, c'est-à-dire CRON dans Spring

Un autre élément nécessite d'être expliqué. Il s'agit de l'interface Trigger qui rend possible l'exécution des tâches en fonction des exécutions précédentes. Ses implémentations déterminent donc le temps de la prochaine exécution d'une tâche en se basant sur le contexte (paramètre TriggerContext passé dans la méthode nextExecutionTime du Trigger).

L'une des implémentations les plus importantes de cette interface est CronTrigger. Comme son nom indique, elle permet de gérer les tâches avec la configuration inspirée du CRON. Une autre implémentation est PeriodicTrigger qui supporte l'appel des tâches d'une manière périodique (une intervalle).

Configurer scheduled tasks dans Spring

Pour mettre en place les tâches planifiées dans Spring, il faut commencer par créer un fichier de contexte qui définira le scheduler, l'executor et qui activera la gestion des actions par annotations :

    <task:scheduler id="taskScheduler"/>
    <task:executor id="taskExecutor" pool-size="1"/>
    <task:annotation-driven executor="taskExecutor" scheduler="taskScheduler"/>
    <context:component-scan base-package="library.async"/>

Commençons l'analyse de cette configuration ligne par ligne. Le premier élément, taskScheduler, crée ThreadPoolTaskScheduler. Il s'agit de l'implémentation de l'interface TaskScheduler. C'est elle qui se charge de mettre en place toute la logique d'exécution des tâches (exécution à une date précise, à une intervalle précise...).

Le second composant est <task:executor />. Il va créer l'instance de la classe ThreadPoolTaskExecutor. Son premier id, va être utilisé en tant que préfixe pour les noms des threads exécutés dans le pool. Cet élément possède également un attribut, pool-size<, qui détermine le nombre maximum des threads qui peuvent être soumis d'un seul coup à ThreadPoolExecutor.

Plusieurs fois dans cet article on a déjà abordé la notion des pools. Mais sa définition doit être encore floue. Pool en anglais veut dire piscine. Chaque piscine a une superficie spécifiée en avance. Il est donc impossible qu'une personne supplémentaire y accède quand elle est pleine. Thread pool en Java ressemble à cette métaphore. D'avance on précise le nombre des threads qui peuvent "vivre" dans le pool. Une fois que une tâche est terminée dans le pool, une autre, qui n'a pas encore été traitée, est envoyée à sa place.

Concernant le troisième élément, il active la gestion des annotations en indiquant qui va jouer le rôle de l'exécuteur et du scheduler. La dernière ligne spécifie le nom du package dans lequel se trouvent les classes qui doivent être considérées en tant que tâches planifiées.

// TODO : différence entre TaskExecutor et TaskScheduler ?

Configurer les scheduled tasks avec les annotations

Dans notre application on utilisera qu'une seule annotation disponible : @Scheduled(fixedRate = X). Elle permet de spécifier une intervalle de temps pour appel des méthodes annotées. Comme cela, dans l'exemple ci-dessous, la fonction public void lookForDelayed() sera invoquée toutes les 30 minutes :

    @Scheduled(fixedRate = 1800000) // every 30 minutes
    public void lookForDelayed() {
        borrowingService.checkDelayed();
    }

Et c'est tout pour exécuter une méthode en tant qu'une tâche planifiée. Pour montrer un peu plus la puissance de cette partie du Spring, on abordera un exemple plus complexe. Il s'agira de l'envoi du newsletter.

L'envoi du newsletter sera lancé toutes les 30 minutes. Chaque lancement va pouvoir récupérer 5 newsletters différents. L'envoi peut être fait au 50 personnes maximum. L'intervalle entre chaque étape d'envoi est de 5 secondes. Notre classe se présente de la manière suivante :

afficher le code

Deux tâches asynchrones sont gérées par cet outil d'envoi des newsletters. La première qui se trouve dans la méthode getNewsletters(), va charger les 5 premières newsletters à envoyer avec une liste des 50 premiers destinataires. L'état de la newsletter sera modifiée tant qu'il n'atteindra pas le niveau "presque tout envoyé". L'état des destinataires sera mis en "à envoyer". Grâce à cela, la seconde tâche, sendNewsletters(), saura immédiatement à qui envoyer la newsletter. On privilégie la sauvegarde dans la base plutôt que dans un champ du NewsletterSenderAsync à cause de la persistance des données. Elles ne seront pas perdues suite à, par exemple, un redémarrage du serveur, et l'émetteur pourra continuer son travail comme si rien n'était.

En ce qui concerne la deuxième tâche, elle s'occupe d'envoyer les newsletters en attente. Elle est lancée toutes les 30 minutes, comme la première. L'envoi du mail se fait toutes les 5 secondes. L'incrémentation ++offset permet à l'outil de récupérer des destinataires différentes à chaque nouveau lancement.

Bartosz KONIECZNY Tâches asynchrones

Une question ? Une remarque ?

*

*

Un conseil Symfony1

Comment inclure un template commun pour plusieurs modules ?

Le fichier à inclure (par exemple _menu.php) devrait être stocké dans le répertoire templates de l'application en question.

Ensuite, dans notre fichier de layout (par exemple layout.php), il suffit d'appeler le helper include_partial : Le répertoire global signifie que le template est global et n'appartient pas à un module particulier.