SERVEI DE PLANIFICACIÓ DE TASQUES




Introducció

Propósit

El Servei de Planificació de Tasques de canigo permet configurar l'execució de tasques de forma diferida en els moments en els que es determini:

  • en qualsevol moment del dia (precisió de milisegons)
  • en alguns dies de la setmana
  • en alguns dies del mes
  • en alguns dies de l'any
  • un número específic de repeticions
  • repetidament fins a una data/instant determinat
  • repetida e indefinidament
  • repetidament en un determinat interval de temps

Context i Escenaris d'Ús

El Servei de Planificació de Tasques es troba dins la Capa de Dades/Integració en el context dels serveis proporcionats per canigo.

Versions i Dependències

Les dependències descrites a la següent url son requerides per tal de compilar i fer funcionar el projecte:
Dependències Servei de Planificació de Tasques

A qui va dirigit

Aquest document va dirigit als següents perfils:

  1. Programador. Per conéixer l'ús del servei
  2. Arquitecte. Per conéixer quins són els components i la configuració del servei
  3. Administrador. Per conéixer cóm configurar el servei en cadascun dels entorns en cas de necessitat

Documents i Fonts de Referència

[1] Quartz http://www.opensymphony.com/quartz



 Atenció: s'ha afegit un apartat descrivint la solució a un problema que surgeix al configurar les versions 2.3.x el servei de planificació per funcionar en un Cluster.


Descripció Detallada

Arquitectura i Components

El Servei de Planificació de Tasques de canigo ofereix interfícies d'ús que s'abstreuen de la implementació escollida per millor mantenibilitat en futures versions i futures implementacions alternatives.

En l'actualitat, canigo proporciona una implementació basada en Quartz (projecte open source desenvolupat per PartNET).

Els components podem classificar-los en:

  1. Interfícies i Components Genérics. Interfícies del servei i components d'ús general amb independència de la implementació escollida.
  2. Implementació de les interfícies basada en Quartz
    Es pot trobar tota la documentació JavaDoc y el codi font referent aquests components a les següents urls:

JavaDoc: http://canigo.ctti.gencat.net/confluence/canigodocs/site/canigo2_0/canigo-services-scheduler/apidocs/index.html
Codi Font:  http://canigo.ctti.gencat.net/confluence/canigodocs/site/canigo2_0/canigo-services-scheduler/xref/index.html

Instal.lació i Configuració

Instal.lació

La instal.lació del servei requereix de la utilització de la llibreria 'canigo-services-scheduler' i les dependències indicades a l'apartat 'Introducció-Versions i Dependències'.

Configuració

Fitxer de configuració: canigo-services-scheduler.xml

Ubicació proposada: <PROJECT_ROOT>/src/main/resources/spring

La configuració del Servei de Planificació de tasques implica 3 pasos:

  1. Definir les tasques que volem executar de forma diferida
  2. Definir els triggers que defineixen en quin moment s'executaran les tasques
  3. Definir la factoria per executar les tasques amb els triggers

Definició de les tasques

Per a definir una tasca cal definir 2 parts:

1 Tasca

La tasca és simplement la referència a la classe que conté el mètode que volem executar de forma diferida.

Exemple:

<!- TASKS DEFINITIONS ->
<bean id="taskWriteLog" class="net.gencat.ctti.canigo.samples.jpetstore.scheduler.TaskWriteLog"/>



La classe tasca és un POJO que no ha d'implementar cap interfície en concret ni extendre cap classe.

2 Detall de la tasca
Per definir els detalls de la tasca per tal que pugui ser executada de forma diferida podem fer ús de 2 classes:

1 'SpringQuartzMethodInvokingJobDetailFactoryBean'. Amb aquesta podrem fer referència al mètode concret a executar. Es permet definir les següents propietats:

Propietat Requerit Descripció
targetObject Referència a la classe que conté el mètode a executar de forma diferida
targetMethod Referència al mètode de la classe que s'executarà
arguments No Llista d'arguments a passar al mètode. Podem fer ús dins la llista de valors directes (amb <value>) o referències (amb <ref bean>). Aquesta llista ha de ser creada en el mateix ordre que la llista d'arguments definida al mètode.

Exemple:

<property name="arguments">
<list>
<value>5</value>
<ref bean="logService"/>
</list>
</property>
concurrent No Indicar si es poden executar de forma concurrent vàries instàncies de la tasca

Recomanació: Usar 'false'

Per defecte: true

Exemple:

<bean id="taskWriteDetail" class="net.gencat.ctti.canigo.services.scheduler.impl.quartz.
SpringQuartzMethodInvokingJobDetailFactoryBean">
  <property name="targetObject" ref="taskWriteLog"/>
  <property name="targetMethod" value="writeLog"/>
  <property name="concurrent" value="false"/>
</bean>



2 'SpringQuartzJobDetailBean'. En aquest cas la classe haurà d'extendre la classe 'SpringQuartzJobBean'.

|| Propietat || Requerit || Descripció ||

jobClass Referència a la classe que extén la classe 'SpringQuartzJobBean'
jobDataAsMap Map de dades que passarem a la tasca. Cadascun dels keys ha de tenir un 'set' corresponent a la classe

Exemple:

<bean name="exampleJob" class="net.gencat.ctti.canigo.services.scheduler.impl.quartz.
SpringQuartzJobDetailBean">
  <property name="jobClass" value="example.ExampleJob"/>
  <property name="jobDataAsMap">
    <map>
      <entry key="timeout" value="5"/>
    </map>
  </property>
</bean>



En aquest cas, la tasca seria definida així:

package example;
// import section
...

public class ExampleJob extends SpringQuartzJobBean {
  private int timeout;

  /**
  * Setter called after the ExampleJob is instantiated
  * with the value from the JobDetailBean (5)
  */
  public void setTimeout(int timeout) {
    this.timeout = timeout;
  }

  protected void executeInternal(JobExecutionContext ctx) throws SchedulerServiceException {
    // do the actual work
  }
}



Com veiem, la classe defineix el mètode 'executeInternal' on realitzarà el procediment de la tasca.

Definició dels triggers
Mitjançant els triggers configurarem en quin moment s'ha d'executar la tasca.
canigo permet la utilització de 2 classes:

  1. 'SpringQuartzSimpleTriggerBean'
Propietat Requerit Descripció
jobDetail Referència a la tasca definida en el pas anterior
endTime No Hora de finalització del trigger
startDelay No Temps (en mil.lisegons) que ha de transcórrer abans d'executar-se el primer trigger
startTime Hora d'inicia de la tasca
repeatInterval No Temps (en mil.lisegons) que ha de transcórrer abans d'executar-se el següent trigger

Exemple:

<!-- TRIGGERS DEFINITIONS -->
<bean id="taskWriteTrigger" class="net.gencat.ctti.canigo.services.scheduler.impl.quartz.
SpringQuartzSimpleTriggerBean">
  <!-- see the example of method invoking job above -->
  <property name="jobDetail" ref="taskWriteDetail"/>
  <!-- 10 seconds -->
  <property name="startDelay" value="10000"/>
  <!-- repeat every 20 seconds -->
  <property name="repeatInterval" value="20000"/>
  <property name="concurrent" value="false"/>
</bean>



  1. 'SpringQuartzCronTriggerBean'.
Propietat Requerit Descripció
jobDetail Referència a la tasca definida en el pas anterior
cronExpression Expressió de tipus cron de Unix. Per a més informació consultar http://wiki.opensymphony.com/display/QRTZ1/CronTriggers+Tutorial

Exemple:

<bean id="taskWriteTrigger2" class="net.gencat.ctti.canigo.services.scheduler.impl.quartz.
SpringQuartzCronTriggerBean">
  <!-- see the example of method invoking job above -->
  <property name="jobDetail" ref="taskWriteDetail"/>
  <!-- run every morning at 6 AM -->
  <property name="cronExpression" value="0 0 6 * * ?"/>
  <property name="concurrent" value="false"/>
</bean>



Definició de la factoria dels triggers
Per últim, definirem un bean de la classe ' net.gencat.ctti.canigo.services.scheduler.impl.quartz.SpringQuartzSchedulerFactoryBean' on definirem les propietats:

Propietat Requerit Descripció
triggers Llista de referències als triggers anteriorment definits

Exemple:

<!-- SCHEDULER FACTORY BEAN DEFINITION -->
<bean class="net.gencat.ctti.canigo.services.scheduler.impl.quartz.SpringQuartzSchedulerFactoryBean">
  <property name="triggers">
    <list>
      <ref bean="taskWriteTrigger"/>
      <ref bean="taskWriteTrigger2"/>
    </list>
  </property>
</bean>



Configuració en Cluster

Par a la utilització del Servei de Planificació de tasques en una aplicació desplegada en un Cluster de servidors d'aplicacions cal tenir en compte els requeriments de configuració de Quartz en aquest tipus d'entorns.

http://www.opensymphony.com/quartz/wikidocs/ConfigJDBCJobStoreClustering.html

Concretament, hi ha algunes condicions d'us que son importants al configurar l'entorn:

  • Cal utilitzar un JDBC-Jobstore, es a dir, l'estat de les tasques s'ha de mantenir en taules de la base de dades
  • Les propietats de configuració de Quartz han de ser les mateixes en tots els nodes del cluster
  • L'identificador de cada instància de Quartz que participi del cluster ha de ser diferent. Això es pot aconseguir amb el valor AUTO en la propietat org.quartz.scheduler.instanceId=AUTO
  • En les propietats de configuració de Quartz cal afegir org.quartz.jobStore.isClustered=true
  • Els relotjes de les màquines que composen el cluster han d'estar sincronitzats amb un error menor d'1 segon

Utilització del Servei

La instanciació, la preparació i la petició del servei es fa de manera transparent, de tal manera que el servei s'activa en el moment en que el "Scheduler Factory Bean" conté algun Trigger amb alguna tasca associada (tal i com he definit a la configuració).

En el servei, es defineixen a la configuració les tasques (Jobs) i els disparadors (Triggers) que llençaran les tasques. Les tasques, en general no han d'extendre o implementar cap interfície, però sí serà necessari en cas de definir 'SpringQuartzJobDetailBean'.

Per tant, la utilització del servei es realitzar pràcticament en la seva totalitat mitjançant la seva configuració.

Exemples

Com exemple d'utilització del servei de Planificació de Tasques s'inclou un exemple en el que 2 Triggers invoquen un mètode d'una classe Java que genera un log (mitjançant el servei de traces):

En el codi mostrat a baix, la tasca definida no implementa cap classe. Defineix únicament el mètode 'writeLog' on realitza la traça

package net.gencat.ctti.tutorial.web.temp;

import org.apache.log4j.Logger;

public class TaskWriteLog {

  public TaskWriteLog() {
  }

  public void writeLog(LoggingService logService) {
    if (logService!=null) {
      logService.getLog(this.getClass()).debug("Doing log at:" + System.
      currentTimeMillis());
    }

  }
}



Un exemple de la configuració de la seva execució diferida és la següent:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/
spring-beans.dtd?">
<beans>
  <!-- SCHEDULER FACTORY BEAN DEFINITION -->
  <bean class="net.gencat.ctti.canigo.services.scheduler.impl.quartz.
  SpringQuartzSchedulerFactoryBean">
    <property name="triggers">
      <list>
        <ref bean="taskWriteTrigger"/>
      </list>
    </property>
  </bean>

  <!-- TASKS DEFINITIONS -->
  <bean id="taskWriteLog" class="net.gencat.ctti.tutorial.web.temp.TaskWriteLog"/>
  <bean id="taskWriteDetail" class="net.gencat.ctti.canigo.services.scheduler.impl.quartz.
  SpringQuartzMethodInvokingJobDetailFactoryBean">
    <property name="targetObject" ref="taskWriteLog"/>
    <property name="targetMethod" value="writeLog"/>
    <property name="arguments">
      <list>
        <ref bean="logService"/>
      </list>
    </property>
      <property name="concurrent" value="false"/>
  </bean>

  <!-- TRIGGERS DEFINITIONS -->
  <bean id="taskWriteTrigger" class="net.gencat.ctti.canigo.services.scheduler.impl.quartz.
  SpringQuartzSimpleTriggerBean">
    <!-- see the example of method invoking job above -->
    <property name="jobDetail" ref="taskWriteDetail"/>
    <!-- 10 seconds -->
    <property name="startDelay" value="10000"/>
    <!-- repeat every 20 seconds -->
    <property name="repeatInterval" value="20000"/>
  </bean>
</beans>




Nota
En el cas que 2 Triggers utilitzin la mateixa tasca, es podria donar el cas de que abans de que el primer Trigger finalitzi, ja es comenci a executar el segon. Per evitar aquesta situació s'afegirà en la definició de la tasca la propietat _concurrent_ amb el valor "false"


Utilització de les versions 2.3.x en un Cluster

La configuració utilitzada en Canigó fins la versio 2.2 és

<!--  Factory -->
<bean class="net.gencat.ctti.canigo.proves.tasques.SpringQuartzSchedulerFactoryBeanSerializable">
    <property name="configLocation" value="classpath:/quartz.properties"/>
    <property name="triggers">
        <list>
            <ref bean="tasca1Trigger"/>
        </list>
    </property>
</bean>
<!-- Triggers referenciats al Factory -->
<bean id="tasca1Trigger" class="net.gencat.ctti.canigo.services.scheduler.impl.quartz.SpringQuartzSimpleTriggerBean">
    <property name="jobDetail" ref="job1Detail"/>
    <property name="startDelay" value="10000"/>
    <property name="repeatInterval" value="10000"/>
</bean>
<!-- Jobs referenciats als Triggers -->
<bean id="job1Detail"
   class="net.gencat.ctti.canigo.services.scheduler.impl.quartz.SpringQuartzMethodInvokingJobDetailFactoryBean">
    <property name="targetObject" ref="tasca1"/>
    <property name="targetMethod" value="metode1"/>
    <property name="concurrent" value="false"/>
</bean>
<!-- Implementació de la tasca en l'aplicació -->
<bean id="tasca1" class="net.gencat.ctti.canigo.proves.tasques.Tasca1"/>

on la classe SpringQuartzSchedulerFactoryBeanSerializable és una extensió de la SpringQuartzSchedulerFactoryBean de canigó que implementi Serializable.

Amb aquesta configuració, per desplegar l'aplicació en un cluster cal que quartz guardi la seva configuració en una base de dades compartida entre tots els nodes, utilitzant un JDBCJobStore. La configuració corresponent a quartz.properties és

org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.dataSource=FormacioDS
org.quartz.dataSource.FormacioDS.jndiURL=java:comp/env/formacioDS
org.quartz.jobStore.tablePrefix=QRTZ\_
org.quartz.jobStore.useProperties=false
org.quartz.jobStore.misfireThreshold=60000
org.quartz.jobStore.isClustered=true
org.quartz.jobStore.clusterCheckinInterval=15000
org.quartz.jobStore.maxMisfiresToHandleAtATime=20
org.quartz.jobStore.dontSetAutoCommitFalse=false
org.quartz.jobStore.selectWithLockSQL=SELECT * FROM {0}LOCKS WHERE LOCK_NAME = ? FOR UPDATE
org.quartz.jobStore.txIsolationLevelSerializable=false
org.quartz.scheduler.instanceId=AUTO

Aquesta configuració no funciona amb les versions 2.3.x de Canigó, en les que s'ha pujat la versió de Quartz a la 1.6.0, produint-se l'error

canigo Message: 11 jun 2009 11:33:28,552 WARN [QuartzScheduler_Worker-1] org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean$MethodInvokingJob - Invocation of method 'null' on target class [null] failed
net.gencat.ctti.canigo.services.scheduler.exception.SchedulerServiceException: java.lang.IllegalStateException: prepare() must be called prior to invoke() on MethodInvoker
at net.gencat.ctti.canigo.services.scheduler.impl.quartz.SpringQuartzMethodInvokingJobDetailFactoryBean.invoke(SpringQuartzMethodInvokingJobDetailFactoryBean.java:103)
at org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean$MethodInvokingJob.executeInternal(MethodInvokingJobDetailFactoryBean.java:272)
at org.springframework.scheduling.quartz.QuartzJobBean.execute(QuartzJobBean.java:86)
at org.quartz.core.JobRunShell.run(JobRunShell.java:202)
at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:529)
Caused by: java.lang.IllegalStateException: prepare() must be called prior to invoke() on MethodInvoker
at org.springframework.util.MethodInvoker.getPreparedMethod(MethodInvoker.java:254)
at org.springframework.util.MethodInvoker.invoke(MethodInvoker.java:279)
at net.gencat.ctti.canigo.services.scheduler.impl.quartz.SpringQuartzMethodInvokingJobDetailFactoryBean.invoke(SpringQuartzMethodInvokingJobDetailFactoryBean.java:97)
... 4 more

El workaround proposat per aquest problema consisteix en canviar la definició del Job, per tal d'utilitzar un JobDetailBean en comptes d'un SpringQuartzMethodInvokingJobDetailFactoryBean

<\!-\-  Factory \-->
<bean class="net.gencat.ctti.canigo.proves.tasques.SpringQuartzSchedulerFactoryBeanSerializable">
<property name="configLocation" value="classpath:/quartz.properties"/>
<property name="triggers">
<list>
<ref bean="tasca1Trigger"/>
</list>
</property>
</bean>

<\!-\- Triggers referenciats al Factory \-->

<bean id="tasca1Trigger" class="net.gencat.ctti.canigo.services.scheduler.impl.quartz.SpringQuartzSimpleTriggerBean">
<property name="jobDetail" ref="job2Detail"/>
<property name="startDelay" value="10000"/>
<property name="repeatInterval" value="10000"/>
</bean>

<\!-\- Jobs referenciats als Triggers \-->

<bean name="job2Detail" class="org.springframework.scheduling.quartz.JobDetailBean">
<property name="jobClass" value="net.gencat.ctti.canigo.proves.tasques.Tasca2" />
</bean>

La configuració del JobStore a quartz.properties és la mateixa i la implementació de la tasca ha d'estendre QuartzJobBean i implementar Serializable.

Un problema amb la utilització de Jobs basats en JobDetailBean és que no tenen accés fàcil al contexte de Spring. En principi es podria utilitzar la propietat applicationContextJobDataKey del JobDetailBean per indicar a Spring que injecti l'ApplicationContext a la tasca, però si es defineix un atribut ApplicationContext en la tasca es provoca un error de Quartz quan intenta serialitzar-la a la base de dades.

Una solució genèrica per tenir accés al contexte de Spring des d'una classe arbitraria consisteix en crear un bean que implementi ApplicationContextAware i que guardi l'applicationContext

public class AppCtxHelper implements ApplicationContextAware {
private static ApplicationContext applicationContext;

public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
{         this.applicationContext = applicationContext;     }
public static ApplicationContext getApplicationContext()
{         return applicationContext;     }
}
<bean name="appCtxHelper" class="net.gencat.ctti.canigo.proves.tasques.AppCtxHelper" singleton="true"/>

llavors, aquesta classe es pot utilitzar per accedir als diferents beans de l'aplicació, per exemple al servei de traces:

public class Tasca2 extends QuartzJobBean implements Serializable {
    private static final long serialVersionUID = -8171187967310135369L;
    
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        getLogger().info("Hola");
    }

    private Log getLogger() {
        LoggingService logService = (LoggingService) AppCtxHelper.getApplicationContext().getBean("loggingService");
        Log logger = logService.getLog(this.getClass());
        return logger;
    }
}