Académique Documents
Professionnel Documents
Culture Documents
1 Introduction ...................................................................................................................... 1
2 Planification de tâches avec le JDK ................................................................................ 2
3 Planification de tâches avec Quartz................................................................................ 4
3.1 Premiers pas ....................................... ........................................................................... 4
3.1.1 Installation / Configuration ................................................................................ 4
3.1.2 Démarrage .......................................................................................................... 4
3.1.3 Définition d’un Job............................................................................................. 5
3.1.4 Définition d’un Job avec conservation d’état..................................................... 5
3.1.5 Définition d’un Trigger ...................................................................................... 5
3.1.6 Echec d’activation d’un trigger (misfire) : ......................................................... 7
3.1.7 Les listeners........................................................................................................ 7
3.2 Gestion des Jobs et Triggers....................................................................................... 8
3.3 Services techniques .................................................................................................... 9
3.3.1 Gestion des exécutions concurrentes.................................................................. 9
3.3.2 Persistance .......................................................................................................... 9
3.3.3 Séparation Client/Serveur avec RMI................................................................ 10
3.3.4 Clustering ......................................................................................................... 12
3.3.5 Plugins .............................................................................................................. 13
3.4 Quelques Jobs utiles fournis avec Quartz : .............................................................. 13
3.5 Démarrer le scheduler en même temps qu’un serveur web...................................... 15
4 Conclusion....................................................................................................................... 15
5 Références ....................................................................................................................... 16
1 Introduction
Beaucoup d' applications d'entreprise nécessitent des traitements
souvent longs et coûteux en ressources système. Lorsque ces
traitements ne nécessitent pas d'intéraction avec l'utilisateur, leur
déclenchement peut être différé sur une période de charge faible,
afin de ne pas détériorer les temps de réponse. Depuis la version
1.4, java permet d'effectuer des planifications de tâche simples.
Cependant les applications d'entreprise exigent des possibilités de
reprise après panne, haute disponibilité, etc. C'est ce que propose
Quartz, une librairie open-source.
Depuis la version 1.3, l’API standard de Java propose un système de planification de tâches
basique au travers des classes java.util.Timer et java.util.TimerTask [1]. La classe Timer
représente le scheduler et TimerTask une tâche à exécuter. On définit une tâche avec une
classe qui dérive de TimerTask et implémente la méthode run(). Une instance de la tâche est
ensuite passée au Timer en spécifiant des paramètres de planification. Le timer exécute
ensuite la méthode run() à la date programmée.
L’exemple suivant permettra de se familiariser avec l’API. On définit une tâche MyTask qui
attend aléatoirement entre 1000 et 2500 ms. Cette tâche a été planifiée pour être exécutée
toutes les deux secondes, trois secondes après le démarrage du programme.
}
}
La classe Timer possède deux constructeurs qui permettent de créer un timer en mode blocant
ou en mode démon. Pour rappel, un programme java se termine une fois que tous les threads
non démons sont terminés.
-Le constructeur par défaut crée un timer blocant. C’est le cas de notre programme d’exemple
qui attend donc l’arrêt de la JVM pour se terminer (CTRL-C).
-Le constructeur avec argument crée un timer démon. Celui-ci se termine en même temps que
le programme principal. Pour l’utiliser dans notre exemple, il faut mettre en attente le thread
principal avec ajoutant par exemple Thread.sleep(…) à la fin du bloc main, afin que le timer
puisse lancer ses tâches. Le lancement d’un autre thread blocant comme une frame swing est
une autre possibilité.
Une fois le timer lancé, on crée une planification en spécifiant une date de début et la période
de répétitions. Attention : certaines planifications comme « tous les mois » ne seront pas
possibles car la période entre deux mois n’est jamais la même…
Il existe deux types de planification :
Timer.schedule(…) respecte les période de répétitions. La date de prochain traitement est
calculée en ajoutant la période à la date du dernier traitement. Si cette date n’est pas respectée
(par ex : le garbage-collector ou l’accès à une ressource a ralenti la tâche), un retard apparaît
et s’accumule au précédent.
Timer.scheduleAtFixedRate(…) essaye de respecter les dates d’exécution. La date de prochain
traitement est calculée par rapport à la date du premier traitement. Ainsi si une tâche a pris du
retard, les tâches suivantes peuvent rattraper ce retard en s’exécutant immédiatement après
leur précédente.
Le Timer, comme tous les scheduler ne peut pas empêcher l’apparition des retards. Mais
généralement les schedulers proposent des solutions pour les minimiser ou les contourner en
amortissant le retard d’une tâche sur les suivantes, quitte à ne pas respecter un intervalle
défini.
De plus la probabilité et le temps moyen des retards augmente avec le nombre de tâche,
surtout si plusieurs tâches sont programmée en même temps. Et là le Timer n’est pas adapté.
La raison réside dans son implémentation. Le Timer dispose d’une file d’attente de tâches
ordonnée par date croissante de prochain démarrage et d’un thread de traitement.
L’algorithme implémenté est simple : le thread se met en attente de la première tâche de la
file. Lorsque la date d’exécution est atteinte, le thread appelle la méthode run(), met à jour la
prochaine date d’exécution de la tâche et se remet en attente.
La présence d’un seul thread de traitement interdit d’optimiser les exécutions concurrentes de
tâches. Malheureusement ce mode d’utilisation est fréquent sur les applications d’entreprise,
généralement distribuées. Dans une application distribuée plusieurs tâches peuvent facilement
s’exécuter en parallèle pour optimiser l’utilisation des ressources. Une tâche peut effectuer un
calcul tandis qu’une autre effectue des requêtes sur une base de données, et une autre effectue
un transfert de fichier… Le Timer n’est donc pas optimal dans ce genre d’application, mais tel
n’est pas son but. D’autant qu’il existe une spécification pour un Timer « j2ee » géré par un
conteneur de serveur d’application.
D’autres points font du Timer un composant pour des applications standards et non
« entreprise » :
-pas de planification sophistiquée.
-pas de persistance des tâches
-pas de système de gestion des tâches évolué
Cependant les cas d’utilisation du timer sont multiples et ce dernier reste intéressant pour sa
grande simplicité et son intégration dans l’API standard (. Des exemples ? la reconnexion à
intervalle régulier d’un client FTP ou l’observation de la modification d’un fichier de
paramétrage.
Comme d’habitude, tout dépend du besoin de votre application…
3 Planification de tâches avec Quartz
Remarque : Ce tutorial s’est basé sur la version 1.4.5 de Quartz.
Il existe plusieurs schedulers disponibles pour la plateforme java mais les plus intéressants
sont Quartz et Flux. Flux est un scheduler commercial très complet, livré avec des outils de
conception et de monitoring, qui permet même de gérer des processus workflow. Quartz ne va
pas jusque là mais dispose de nombreuses fonctionnalités qui conviendront à la plupart des
applications métier. Quartz fait partie du projet OpenSymphony qui propose des composants
orientés entreprise pour la plateforme J2EE La licence de Quartz dérive et est entièrement
compatible avec Apache Software License définie par Apache Software Foundation [2] . Il est
open-source, peut être redistribué avec modification des sources, et libre d’utilisation dans des
projets commerciaux. C’est pour cette raison que nous allons le découvrir.
3.1.2 Démarrage
Il existe principalement trois types d’objets : le Scheduler, les Jobs et les Triggers. Un Job
(travail) représente une tâche à exécuter, un Trigger (déclencheur) représente le mécanisme de
planification d’une tâche. Concernant les relations, un job peut être utilisé par plusieurs
trigger, mais un trigger n’est associé qu’à un seul job.
En démarrant le scheduler, celui-ci se mettra immédiatement en attente des jobs. Pour cela on
fait d’abords appel à la factory qui permet d’instancier le scheduler, puis on invoque la
méthode start() sur l’instance obtenue. Le type de scheduler est déterminé par la factory en
fonction des services techniques décrit dans un fichier de propriété, cependant ce fichier n’est
pas nécessaire si on souhaite travailler avec le scheduler par défaut. Dans notre cas, les
services comme le fail-over et le clustering seront désactivés (voir §3.3.4).
Il est possible de passer des données à un job en utilisant un JobDataMap. Cet objet est un
tableau associatif qui sera disponible dans le contexte d’exécution du Job (méthode execute)
JobDataMap map = new JobDataMap();
map.put(“userId”,"155-123587");
jobDetail.setJobDataMap(map);
CronTrigger : ce trigger permet des planifications basées sur les jours du calendrier. Il utilise
la syntaxe des expressions cron d’Unix.
Une expression cron est un ensemble de six champs obligatoires plus un facultatif, séparés par
des espaces, et pouvant êtres associés à des caractères spéciaux:
Exemples :
« Tous les jours du lundi au vendredi à 08h00 » se traduit par « 0 0 8 ? * MON-FRI »
« Tous les derniers vendredi du mois à 10h15 » se traduit par « 0 15 10 ? * 6L »
trigger.setCalendarName("cal1");
sched.addCalendar("cal1", c, true, true);
sched.scheduleJob(jobDetail, trigger) ;
Lorsqu’un trigger ne peut pas traiter un job à l’heure prévue, le trigger passe dans l’état
« misfired ». Ce cas se produit lorsque qu’aucun thread du pool n’est disponible (jobs trop
longs par exemple), ou que le scheduler, lancé en tant que serveur distant, est injoignable.
Lorsqu’un Trigger passe dans l’état « misfired », le scheduler peut tenter d’effectuer une
action tel que relancer le job immédiatement. C’est semble-t-il la politique par défaut, qui
correspond au comportement du Timer du JDK. Il existe cependant d’autres actions possibles
et ce pour chaque type de trigger. On spécifie l’action via la méthode
Trigger.setMisfireInstruction(int).
Les noms d’actions renseignent déjà d’eux-même, mais pour plus de détail, cf la javadoc !
On peut également permettre un délai de retard au-delà duquel le Trigger passe dans l’état
« misfired ». Le délai de retard par défaut est de 60 secondes et peut se configurer (voir § 3.3).
Exemple :
class RapportsVentesJobListener implements JobListener {
public String getName() {
return "rapportsVentesListener";
}
public void jobExecutionVetoed(JobExecutionContext context) {
}
public void jobToBeExecuted(JobExecutionContext context) {
System.out.println("mise à jour du SI client : le job va être
exécuté");
}
public void jobWasExecuted(JobExecutionContext context,
JobExecutionException jobException) {
System.out.println("mise à jour du SI client : le job s'est
exécuté");
if(jobException==null) {
System.out.println("ENVOI mail succès");
} else {
System.out.println("ENVOI mail échec");
System.out.println(jobException);
}
}
}
sched.addJobListener(new RapportsVentesJobListener());
jobDetail.addJobListener("rapportsVentesListener");
org.quartz.scheduler.instanceName = SchedulerDeTest
org.quartz.scheduler.instanceId = scheduler1
#==========================================================================
# Configure ThreadPool
#==========================================================================
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = -1
org.quartz.threadPool.threadPriority = 5
#==========================================================================
# Configure JobStore
#==========================================================================
org.quartz.jobStore.misfireThreshold = 60000
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
Pour informer quartz qu’il doit utiliser un fichier et non le paramétrage par défaut, il suffit de
renseigner son emplacement dans une variable système de la JVM au démarrage :
java -Dorg.quartz.properties=conf/quartz.properties …
org.quartz.threadPool.threadCount = 20
3.3.2 Persistance
La gestion du cycle de vie des jobs et des triggers est réalisée par le JobStore. Par défaut, ou si
on le paramètre ainsi, Quartz utilise un JobStore non persistant avec la classe
org.quartz.simpl.RAMJobStore. Par conséquent si l’application se plante, les jobs sont perdus.
La solution est d’utiliser un JobStore persistant dans un SGBD. Quartz supporte la plupart des
SGBD. Cela permet de garantir la reprise après panne (fail-over), en contrepartie les
performances du scheduler sont diminuées du fait des requêtes SGBD, mais cela est
généralement négligeable car ils existent souvent d’autres ralentissements dans une
application.
-driverDelegateClass définit la classe de mapping entre le JobStore et SGBD. Ici j’ai choisi le
mapping standard car j’utilise MySQL. Les autres SGBD possèdent leur propre mapping qui
sont référencés dans la documentation de Quartz.
-tablePrefix permet de distinguer les tables Quartz des autres, si vous décider d’installer les
tables dans une base existante.
-dataSource référence la description de la datasource (voir ci-dessous)
- misfireThreshold définit le délai de dépassement par rapport à la date planifiée au-delà
duquel un trigger passe dans l’état misfired.
- isClustered active ou non le mode cluster (voir plus loin)
Remarque : Les scripts de création des tables Quartz se trouvent dans répertoire docs/dbTables.
#==========================================================================
# Configure Datasources
#==========================================================================
org.quartz.dataSource.myDS.driver = org.gjt.mm.mysql.Driver
org.quartz.dataSource.myDS.URL = jdbc:mysql://localhost/quartz
org.quartz.dataSource.myDS.user = root
org.quartz.dataSource.myDS.password =
org.quartz.dataSource.myDS.maxConnections = 5
org.quartz.dataSource.myDS.validationQuery = select lock_name from
qrtz_locks where lock_name = 'TRIGGER_ACCESS';
if(System.getSecurityManager() != null) {
System.setSecurityManager(
new java.rmi.RMISecurityManager()
);
}
try {
SchedulerFactory schedFact = new StdSchedulerFactory();
Scheduler sched = schedFact.getScheduler();
sched.start();
} catch (SchedulerException se) {
se.printStackTrace();
}
}
}
Configuration quartz du serveur : Il faut ajouter ces paramètres en plus du pool de thread et du
jobStore
org.quartz.scheduler.instanceName = Sched1
org.quartz.scheduler.rmi.export = true
org.quartz.scheduler.rmi.registryHost = localhost
org.quartz.scheduler.rmi.registryPort = 1099
org.quartz.scheduler.rmi.createRegistry = true
Rmi.policy : Dans notre exemple ce script accorde toutes les permissions (tous les clients sont
acceptés et font ce qu’ils veulent…)
grant {
permission java.security.AllPermission;
};
@SET QRTZ_CP=.;%QRTZ%\lib\commons-logging.jar;%QRTZ%\lib\commons-
collections.jar;%QRTZ%\lib\commons-dbcp-1.1.jar;%QRTZ%\lib\commons-pool-
1.1.jar;%QRTZ%\lib\log4j.jar;%QRTZ%\lib\jdbc2_0-
stdext.jar;%QRTZ%\lib\quartz.jar
@SET RMI_CODEBASE=file:/d:/program/quartz-1.4.5/lib/quartz.jar
Exemple de client :
public class TestClient {
@SET QRTZ_CP=.;%QRTZ%\lib\commons-logging.jar;%QRTZ%\lib\commons-
collections.jar;%QRTZ%\lib\commons-dbcp-1.1.jar;%QRTZ%\lib\commons-pool-
1.1.jar;%QRTZ%\lib\log4j.jar;%QRTZ%\lib\jdbc2_0-
stdext.jar;%QRTZ%\lib\quartz.jar
java -cp %QRTZ_CP% -Dorg.quartz.properties=remoteClient.properties
com.developpez.scheduletest.quartz.TestClient
Remarque : En client/serveur, le job est exécuté dans la JVM du serveur, il faudra donc
veiller à ce que le scheduler dispose des classes nécessaire à l’exécution…
3.3.4 Clustering
Le clustering permet de créer un super-scheduler réparti sur des machines différentes.
Le traitement d’un job s’effectuera sur la première instance de scheduler disponible dont au
moins un thread est disponible. De même si un job s’exécute sur une instance et que celle-ci
plante, il est possible de paramétrer la récupération du job sur une instance disponible (voir
l’exemple ClusterTest de Quartz) Cela permet donc de garantir une haute disponibilité de
traitement des tâches, une reprise après panne améliorée de même qu’un équilibrage de
charge, lorsque les ressources d’une machine seule ne suffisent plus (trop de threads ou trop
de mémoire utilisée).
Prérequis :
Il faut utiliser un JobStore persistant, pointant sur une datasource partagée par chaque
instance.
Les machines du clusters doivent être synchrones (utiliser un serveur de temps)
Configuration :
Les identifiants d’instance des scheduler doivent être unique dans tout le cluster. Pour
simplifier la configuration, on peut utiliser AUTO ainsi les fichiers quartz.properties sont
identiques sur chaque instance.
En fait chaque scheduler utilisant la datasource sera considéré comme faisant partie du
cluster. Les instances communiquent par l’intermédiaire de la datasource. Ainsi aucune autre
configuration n’est nécessaire (par exemple au niveau du réseau). Le paramètre
clusterCheckinInterval permet de vérifier l’état du cluster à chaque instant et observer les
instances disponibles.
org.quartz.scheduler.instanceId = AUTO
#==========================================================================
org.quartz.jobStore.isClustered = true
org.quartz.jobStore.clusterCheckinInterval = 20000
3.3.5 Plugins
Si certaines fonctionnalités de Quartz manquent ou ne sont pas adaptés à l’application métier,
il est possible de les développer en tant que plugins
#==========================================================================
# Configure Plugins
#==========================================================================
org.quartz.plugin.jobHistory.class =
org.quartz.plugins.history.LoggingJobHistoryPlugin
map.put(EJBInvokerJob.INITIAL_CONTEXT_FACTORY,"weblogic.jndi.WLInitialConte
xtFactory");
map.put(EJBInvokerJob.PROVIDER_URL,"t3://192.168.1.2:7001");
map.put(EJBInvokerJob.EJB_JNDI_NAME_KEY,"application/Sales");
map.put(EJBInvokerJob.EJB_METHOD_KEY,"reportByClientId");
map.put(EJBInvokerJob.EJB_ARGS_KEY,new Object[]{"152-12568"});
NativeJob
Lance une commande du système d’exploitation.
NoOpJob
Ne fait rien… ou presque ! Le JobListener est quand même notifié, ce qui permet de
déporter le traitement dans ce dernier (c'est-à-dire coté client…)
4 Conclusion
Nous venons d’aborder deux façons différentes de planifier des tâches en Java. Le Timer du
JDK est certes moins évolué que Quartz mais à l’avantage d’être plus simple et de faire partie
de l’API Standard. Le Timer est réservé à des applications qui peuvent se passer de services
tels que la persistance et l’équilibrage de charge, qui ne se soucient pas d’être notifié des
tâches perdues lors d’un crash ou qui ne souhaitent pas de contrôle sophistiqué des
planifications et des tâches.
Il est à noter qu’IBM et BEA ont déposé une spécification d’API standard pour la
planification de tâche gérée par un serveur d’application (prise en charge des ressources par le
conteneur). JSR 236: Timer for Application Servers
5 Références
[1] Javadoc 1.4.2 de java.util.Timer :
http://java.sun.com/j2se/1.4.2/docs/api/java/util/Timer.html