jueves, 3 de marzo de 2011

Quartz

En Java podemos ejecutar acciones automáticamente cada cierto tiempo con la clase java.util.Timer. Pero a veces  necesitamos ejecutar tareas de forma automática en un determinado momento, como por ejemplo borrar ficheros de directorios temporales a las 5:00 A.M, comprobar si algún sistema externo está funcionando correctamente desde las 08:00 A.M.  o hacer copias de determinadas tablas de bases de datos los domingos de madrugada. Para realizar estas tareas de forma robusta y eficaz tenemos Quartzun framework open source con licencia Apache 2.0 para la planificación y gestión de tareas. Actualmente se utiliza con éxito en muchos proyectos como Spring y conocidas organizaciones como JBoss, Cisco o Adobe. Entre sus principales características están:


  • Se puede utilizar tanto en aplicaciones J2EE y J2SE.
  • Planificación flexible de tareas mediante expresiones CRON.
  • Mantenimiento del estado de tareas en caso de fallos del sistema (información de estado almacenada en BD).
  • Posibilidad de participar en transacciones JTA.
  • Posibilidad de configuración en modo cluster.

Elementos básicos


Se basa en tres componentes principalmente:


  • La tarea que queremos ejecutar (por ejemplo, borrar directorios temporales)
  • El planificador o scheduler (que administra y ejecuta las tareas)
  • El trigger, el evento que avisa al planificador para que ejecute la tarea
Primero debemos crear una clase Java que implemente la interfaz Job e introducir en ella la lógica de negocio de la tarea a realizar:


public class MiTrabajo implements Job {
  public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
// Lógica de negocio
}
}

Posteriormente obtenemos el planificador y le decimos que trigger dispara la ejecución de nuestro trabajo:


SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory();
sched = schedFact.getScheduler();
sched.start();

JobDetail jobDetail = new JobDetail("miTrabajo", null, jpascu.quartz.job.MiTrabajo.class);

//el trigger se disparará cada 10 segundos
Trigger trigger = TriggerUtils.makeSecondlyTrigger(10);

//Otro Ejemplo: ejecutar a las 11:26 de cada viernes (ver manual de uso)
//Trigger trigger = new CronTrigger("myTrigger", null, "0 26 11 ? * FRI");

trigger.setStartTime(new java.util.Date());
trigger.setName("myTrigger");
sched.scheduleJob(jobDetail, trigger);


Integración con una aplicación Java SE

Para probar esta aplicación en una aplicación Java SE sólo tendremos que colocar el código anterior donde creamos apropiado en nuestra aplicación. Para realizar una prueba rápida podríamos introducirlo en la función main().

Integración con una aplicación Web

Otras veces una aplicación web es la que necesita ejecutar las tareas. Hay varias formas de configurar nuestro planificador en un entorno web:

  • Sobrescribiendo el método init() de nuestro servlet o utilizando un ServletContextListener.
  • Utilizando el framework Spring.
En este apartado nos centraremos en la configuración de Quartz en una aplicación Web utilizando Spring. Como características adicionales, mostraremos como se configura Quartz en un entorno de cluster y utilizando persistencia en base de datos para almacenar la información de estado de las tareas.

La configuración la vamos a realizar en el fichero de contexto de Spring (applicationContext.xml).

Deberemos declarar primero las tareas que queremos ejecutar:

<bean name="borrarFicherosTemporales" class="org.springframework.scheduling.quartz.JobDetailBean">
  <property name="jobClass" value="es.jpascu.mantenimiento.jobs.BorrarFicherosTemporalesJob"/>
</bean>

Cuando active la tarea (Job) se ejecutará la lógica de negocio que contenga el método execute.

Tenemos la posibilidad de definir dos tipos de triggers que asociaremos a nuestras tareas:

  • CronTriggerBean. Se usa para ejecutar un Job en un instante determinado mediante expresiones de tipo Cron. En este caso ejecutamos la tarea a las 15:30 todos los días menos los Jueves.

        <bean id="triggerBorrarFicherosTemporales" class="org.springframework.scheduling.quartz.CronTriggerBean">
   <property name="jobDetail" ref="borrarFicherosTemporales"/>
<property name="cronExpression" value="0 30 15 ? * SUN,MON,TUE,WED,FRI,SAT"/>
</bean>

  • SimpleTriggerBean. Se utiliza para ejecutar un Job cada cierta cantidad de milisegundos. En este caso ejecutaremos la tarea cada 6 segundos esperando 2 segundos en el instante inicial cuando se planifique.
        <bean id="triggerBorrarFicherosTemporales" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
   <property name="jobDetail" ref="borrarFicherosTemporales"/>
<property name="startDelay" value="2000"/>
   <property name="repeatInterval" value="60000"/>
</bean> 

A continuación configuramos el planificador mediante la clase SchedulerFactoryBean . La propiedad dataSource indica el nombre del data source donde están las tablas que utiliza Quartz para mantener un estado de las tareas. En la propiedad triggers se definen los triggers que utiliza el planificador y que activan nuestras tareas. En la propiedad de tipo lista quartzProperties configuramos que la aplicación sera desplegada en cluster (org.quartz.jobStore.isClustered=true) y que se utilizará una base de datos para almacenar el estado de las tareas (org.quartz.jobStore.class)

<!-- A list of Triggers to be scheduled and executed by Quartz -->
    <bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <property name="dataSource" ref="qtzTxDataSource"/>
        <property name="transactionManager" ref="transactionManager"/>
        <property name="overwriteExistingJobs" value="true"/>
        <property name="autoStartup" value="true" /> 
 <property name="triggers">
              <list>
                 <ref bean="triggerBorrarFicherosTemporales"/>                          
               </list>
          </property>
          <property name="applicationContextSchedulerContextKey">
            <value>applicationContext</value>
      </property>
       <property name="quartzProperties">
            <props>
                <prop key="org.quartz.scheduler.instanceName">TMBatchScheduler</prop>
                <prop key="org.quartz.scheduler.instanceId">AUTO</prop>
                <prop key="org.quartz.jobStore.misfireThreshold">60000</prop>
                <prop key="org.quartz.jobStore.class">org.quartz.impl.jdbcjobstore.JobStoreTX</prop>
                <prop key="org.quartz.jobStore.driverDelegateClass">org.quartz.impl.jdbcjobstore.oracle.weblogic.WebLogicOracleDelegate</prop>
                <prop key="org.quartz.jobStore.tablePrefix">qrtz_</prop>
                <prop key="org.quartz.jobStore.isClustered">true</prop>
                <prop key="org.quartz.threadPool.class">org.quartz.simpl.SimpleThreadPool</prop>
                <prop key="org.quartz.threadPool.threadCount">25</prop>
                <prop key="org.quartz.threadPool.threadPriority">5</prop>
            </props>
      </property> 
    </bean>

Por último definimos el transaction manager:

   <bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager" >
        <property name="sessionFactory">
      <ref local="sessionFactory"/>
        </property>
    </bean>

Para que se levante el contexto de Spring cada vez que se despliegue la aplicación y arrancar el planificador de tareas debemos añadir las siguientes líneas al descriptor web.xml:

<context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>
       
     <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    
     <listener>
        <listener-class>org.quartz.ee.servlet.QuartzInitializerListener</listener-class>
    </listener>
...
</context-param>


6 comentarios:

  1. Buenas,

    antes que nada, gracias por la explicación, ha sido muy clara y me ha permitido entender perfectamente muchas cosas que antes no sabía cómo se hacían.

    Tengo una duda respecto al momento de arrancar el planificador de tareas. ¿Existe alguna opción para especificar que las tareas se comiencen a ejecutar 10 minutos después del arranque? Es decir, existe la opción de darle un tiempo de espera antes que se ejecute un job (startDelay), ¿pero decirle un tiempo de espera cuando se arranca el servidor y se levanta a su vez el listener de quartz? Para darle un tiempo a que esté todo levantado antes de ejecutar jobs.

    Merci!

    ResponderEliminar
  2. Hola Marc. Si ves la definición del trigger de los ejemplos, además de indicar el job que queremos ejecutar y cada cuando lo queremos ejecutar, se puede configurar un startDelay, que es el tiempo de espera antes de comenzar a ejecutar el job por primera vez.

    No se si te refieres a esto, sino dimelo y te intento responder. Espero haberte ayudado. Un saludo.

    ResponderEliminar
  3. Hola Pascu,

    sip, es lo que te comentaba, que el startDelay indica el tiempo de espera antes de ejecutarse, pero no es exactamente lo que busco.

    Verás, la aplicación lanza un job, oks? Y ese job comienza a ejecutarse a los 20 segundos. Así que sería correcto que el startDelay fueran 20 segundos.

    El problema lo tengo al arrancar el servidor. Se comienzan a levantar los portlets, webapps, aplicaciones, etc, y también los listeners. Y se ponen en marcha los jobs. ¿Qué ocurre? Pues que uno de esos jobs necesita acceder a un webapp que aún no ha sido arrancado. Así que me interesaría poder indicar de alguna manera que cuando se arranque el servidor, estos jobs no comiencen a ejecutarse hasta al cabo de unos 15 mins, para dar tiempo a que todo esté levantado.

    Así que no sería un delay al llamarse al job por primera vez, sino al arrancarse el job por reinicio de servidor (en nuestra caso, usamos liferay).

    Merci!

    ResponderEliminar
  4. Hola de nuevo Marc.

    En el startDelay puedes indicar el tiempo que quieras. En lugar de 20 segundos, puedes poner 20 minutos si así lo deseas. Habrá que transformarlo primero a milisegundos claro.

    También puedes utilizar los CronTrigger que son mucho más precisos a la hora de definir cuando quieres que se ejecuta tu job. Con este tipo de trigger podemos ejecutar un job un Jueves a las 11:30 de la noche si así lo deseamos.

    Si puedes poner más tiempo de startDelay para que a tu app accedida desde ese job le de tiempo a arrancar, no entiendo que problema hay. ¿O el problema es que cuando se reinicia tu servidor no respeta el tiempo de startDelay?

    Un saludo.

    ResponderEliminar
  5. No exactamente. El problema es que el startDelay se hace siempre que se lance el job por primera vez, sea por una primera llamada al job, o sea por un reinicio de liferay.

    Tengo dos casos de inicio de job: se lanza el job desde la aplicación (con un delay de 20 segundos) y se lanza al reiniciarse el servidor (ahí el delay ha de ser de 15 minutos). El startDelay se usa en ambos casos por igual, y necesito algo que lo diferencie.

    ResponderEliminar
  6. Hola Marc.

    He estado liado con otros temas. La verdad que entiendo el problema pero no sé si con la versión de Quartz que utilizo (1.X) se puede resolver. Nunca he hecho nada parecido a lo que me comentas.

    ¿Lo has resuelto al final ?

    ResponderEliminar