style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">

Spring Batch, más allá del tutorial

Parte de la operación de un sistema en el que llevo ya años trabajando, consiste en generar varios reportes diarios para conciliar con proveedores. El proceso es automatizado y ya hay componentes reutilizables para realizar esto, pero la cantidad de productos últimamente ha crecido bastante y esto trae un problema de desempeño, pues cada reporte lee la misma tabla, pero con criterios diferentes, para obtener prácticamente el mismo tipo de datos: Las ventas de X producto de un día.

La tabla en cuestión contiene las ventas de todos los productos, de modo que lo que ocurre diariamente es que se realiza la misma consulta, una vez por producto; algo así como SELECT * FROM venta WHERE producto=? y solamente cambia el producto. Si tenemos 20 productos, pues son 20 consultas a la tabla.

El diseño actual de los generadores de reportes es que son simples Runnables que extienden una clase base con lo necesario para realizar la consulta, basada en ciertos parámetros, y simplemente deben implementar un método que recibe un registro, para generar una línea de un archivo de texto. Para cada reporte nuevo, se crea una clase nueva con poco código y se agrega en una configuración XML de Spring para que se ejecute de manera automática todos los días.

Se me ocurrió que este diseño puede cambiar, de una manera no muy radical para los componentes, aunque sí para el proceso general: Hacer una sola lectura a la tabla, obteniendo los movimientos del día, y recorrer cada registro, pasándolo como argumento a una lista de componentes y que cada uno lo ignore o lo procese, según su criterio interno (básicamente, si el producto indicado en ese registro le "interesa" o no, pero puede ser cualquier otro criterio).

Estaba a punto de ponerme a escribir todo el mecanismo para esto, cuando el buen rugi me recomendó Spring Batch; me puse a revisarlo y me pareció que resuelve este tipo de problemas, por lo que decidí probarlo primero.

Primer contacto

Como sucede con cualquier cosa de Spring, el contacto inicial es complicado. Yo ya conozco Spring y lo he usado desde la versión 1.0, de hecho tengo aplicaciones con arquitectura de contenedor ligero desde hace ya varios años, basadas en Spring para toda la configuración y conexión de componentes. Aún así, usar Spring Batch por primera vez fue algo complicado.

Obviamente uno puede seguir paso a paso el tutorial simple para ver cómo funciona, y eso no es realmente complicado. Pero este tipo de cosas no siempre explican muy bien cómo extender la aplicación o cómo echarla a andar en un esquema distinto. Por ejemplo, el tutorial habla de cómo hacer solamente un proceso donde se leen datos de una fuente, se transforman, y se escriben a otro lugar (fuente y destino pueden ser por ejemplo base de datos y archivo de texto, o al revés). En mi caso, lo que necesito es leer de una fuente y escribir a varios destinos.

La manera en que funciona Spring Batch es que tienes jobs, que son las tareas completas para ejecutar (un proceso por lotes, pues). Cada job tiene varios pasos, implementados como Steps. Y cada paso tiene una entrada, procesamiento y salida, en forma de un ItemReader, ItemProcessor y finalmente un ItemWriter. Se pueden manejar varios ItemProcessors para realizar transformaciones graduales de los datos.

Para encontrar cómo escribir a varias salidas, cuando esta cosa acepta un solo ItemWriter, tuve que clavarme más en la documentación, tanto referencia como API. Finalmente encontré un CompositeItemWriter, que lo único que hace es agrupar varios ItemWriters para delegarles la escritura de los datos que recibe, de manera secuencial. Esto es justo lo que necesitaba para la salida, de modo que esa parte ya está resuelta.

Adecuaciones

Al inicio mencioné que sólo necesito leer de una tabla. En realidad, las ventas del día están repartidas en dos tablas, porque tenemos dos tipos de entidades completamente distintas que realizan ventas, y cada una registra sus ventas aparte. Pero de cada a los proveedores, no se necesita hacer una distinción entre ambos tipos; solamente hay que enviar las ventas realizadas, en el mismo reporte.

Por más que busqué, no encontré algo como CompositeItemReader. No sé si no ha habido suficiente demanda de algo así o cuál sea la razón para que no exista, pero pues tuve que implementar mi propio ItemReader, este sí lo hice muy específico para que contenga precisamente dos ItemReaders, que obtenga datos de uno y cuando se agote ese lector, comience a obtener datos del segundo lector. Este es el código de ese componente:

public class DualItemReader implements ItemReader<Map<String,Object>>, ItemStream {

  ItemReader<Map<String,Object>> reader1
  ItemReader<Map<String,Object>> reader2
  private ItemReader<Map<String,Object>> current
  private ExecutionContext ctxt

  Map<String,Object> read() {
    if (current == null) {
      current = reader1
      if (reader1 instanceof ItemStream) { //que sí debería ser
        ((ItemStream)reader1).open(ctxt)
      }
    }
    Map<String,Object> item = current.read()
    if (r == null && current == reader1) {
      if (reader instanceof ItemStream) {
        ((ItemStream)reader1).close()
      }
      current = reader2
      if (reader2 instanceof ItemStream) {
        ((ItemStream)reader2).open(ctxt)
      }
      r = current.read()
    }
    return r
  }

  void open(ExecutionContext c) {
    ctxt = c
  }
  void update(ExecutionContext c) {
    ((ItemStream)current)?.update(c)
  }
  void close() {
    ((ItemStream)current)?.close()
  }
}

Sólo falta configurar el componente para que tenga dos lectores delegados y listo.

Configuración por XML

El tutorial solamente cubre la configuración via Java, la cual me parece muy poco práctica; si bien es odioso lidiar con XML, es bastante útil manejar configuraciones de Spring como XML porque se pueden agregar y quitar componentes sin necesidad de compilar nada ni hacer redeploy. Incluso se pueden tener varias configuraciones alternas, para iniciar una aplicación con alguna de ellas; esto lo hacemos actualmente con el generador de reportes y la idea es mantener ese esquema. Pero no fue fácil encontrar la manera de cambiar la config a XML. Por ahora, lo hicimos manteniendo Spring Boot, pero debido a lo poco que lo usamos, seguramente se puede eliminar posteriormente:

class Prueba {

  static void main(String[] args) {
    SpringApplication app = SpringApplicationBuilder().application()
    app.webEnvironment=false
    app.mainApplicationClass=Prueba.class
    app.headless=true
    app.sources=['/main.xml', '/jobs.xml'] as Set
    ApplicationContext ctxt = app.run(args)
    JobLauncher launcher = ctxt.getBean(JobLauncher.class)
    ctxt.getBeans(Job.class).each { k, job ->
      launcher.run(job, new JobParameters())
    }
  }
}

Hice "todo" ese código para que se lancen todos los jobs configurados en XML. De hecho, si no utilizara Spring Boot, sería menos código, pues sería una línea para crear el ApplicationContext en vez de toda esa configuración. En cuanto al XML, lo partí en dos archivos, para tener uno con los componentes de infraestructura y otro con la config de Spring Batch como tal, aunque aquí los muestro como un solo archivo:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:p="http://www.springframework.org/schema/p"
        xmlns:c="http://www.springframework.org/schema/c"
        xmlns:batch="http://www.springframework.org/schema/batch"
        xmlns:context="http://www.springframework.org/schema/context"
        xsi:schemaLocation="http://www.springframework.org/schema/beans <a href="http://www.springframework.org/schema/beans/spring-beans.xsd
" title="http://www.springframework.org/schema/beans/spring-beans.xsd
">http://www.springframework.org/schema/beans/spring-beans.xsd
</a>            <a href="
http://www.springframework.org/schema/context" title="http://www.springframework.org/schema/context">http://www.springframework.org/schema/context</a> <a href="http://www.springframework.org/schema/context/spring-context.xsd
" title="http://www.springframework.org/schema/context/spring-context.xsd
">http://www.springframework.org/schema/context/spring-context.xsd
</a>            <a href="
http://www.springframework.org/schema/batch" title="http://www.springframework.org/schema/batch">http://www.springframework.org/schema/batch</a> <a href="http://www.springframework.org/schema/batch/spring-batch.xsd">

" title="http://www.springframework.org/schema/batch/spring-batch.xsd">

">http://www.springframework.org/schema/batch/spring-batch.xsd">

</a>    <context:annotation-config />

    <bean id="dataSource" destroy-method="close"
                class="org.apache.commons.dbcp2.BasicDataSource" />
        <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
                        c:_0-ref="dataSource" />
        <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate"
              p:transactionManager-ref="transactionManager" />
        <bean id="jobRepository" class="org.springframework.batch.core.repository.support.JobRepositoryFactoryBean"
                p:dataSource-ref="dataSource" p:transactionManager-ref="transactionManager" />
        <bean id="jobLauncher" class="org.springframework.batch.core.launch.support.SimpleJobLauncher"
                p:jobRepository-ref="jobRepository" />

        <bean id="reader" class="test.DualItemReader"
          p:reader1-ref="reader1" p:reader2-ref="reader2" />

        <bean id="writer" class="org.springframework.batch.item.support.CompositeItemWriter">
                <property name="delegates"><list>
                        <bean class="com.newvision.reportes.Proc1" />
                        <bean class="com.newvision.reportes.Proc2" />
                </list></property>
        </bean>

        <bean id="reader1" class="org.springframework.batch.item.database.JdbcCursorItemReader" />
        <bean id="reader2" class="org.springframework.batch.item.database.JdbcCursorItemReader" />

        <batch:job id="conciliaciones" restartable="true">
                <batch:step id="ventas" allow-start-if-complete="true">
                        <batch:tasklet>
                                <batch:chunk reader="reader" writer="writer" commit-interval="20" />
                        </batch:tasklet>
                </batch:step>
        </batch:job>

</beans>

To Boot or not to Boot

Spring Boot es muy conveniente porque en conjunto con Gradle, me crea un mega-jar que contiene internamente TODAS las dependencias necesarias. Todavía no me decido si seguir usándolo o no; las desventajas al parecer únicamente son que ese jar obviamente es muy grande (14MB), y que contiene también ya los archivos de configuración. Pero pues es cosa de sacarlos del jar y modificar un poco el método main para que procese los argumentos de CLI para leer los XML que se le indiquen, en vez de tenerlos en código duro.

Por otra parte, me falta investigar la mejor manera de integrar los generadores de reportes; una manera puede ser tenerlos en un sub-proyecto o un proyecto aparte para meterlos en un jar que a su vez se integre al mega-jar para deployment. Esto es algo que hacemos hoy sin Spring Boot; falta ver si con Boot es más fácil o más complicado, especialmente a largo plazo.

Bitácoras

Una nota final, que me di cuenta después de varias pruebas: Spring Batch lleva una bitácora de sus ejecuciones, y lo hace en cualquier base de datos que tenga a la mano por medio de un bean llamado dataSource. Al entrar a la base de datos de prueba, me di cuenta que había varias tablas nuevas, todas con predijo spring_batch_, que llevan un registro de las ejecuciones de jobs, sus pasos y resultados intermedios. Esto puede ser bastante útil para rastrear lo que pasa en caso que un archivo no se haya generado correctamente, por ejemplo.

Comentarios

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.

Akka

Para procesos Batch vengo usando Akka, el cual esta escrito en Scala, esta basado en el modelo de programación orientado a actores que tomo de Erlang, sus características mas importantes son: concurrente, distribuido, extensible, tolerante a fallos, ligero y se puede integrar con Spring Framework, en cuanto tenga la oportunidad publicare algun ejemplo de su uso.

De Spring Framework solo conozco y uso el CORE para la inyección de dependencias, pero Spring Batch me parece buena opción, ya que fue concebido y pensado desde su diseño para eso, automatizando algunos procesos en modo de archivos de configuración, tales como lecturas de archivos (txt, xml, etc;), lectura y escritura en Base de Datos, etc;

Quiero hacer una prueba ETL comparando Akka vs Spring Batch, para conocer rendimiento, uso de memoria y CPU.

Referencias: http://akka.io/

Imagen de ezamudio

akka?

Akka para batch? Supongo que se puede usar para eso, aunque según yo era más para procesamiento en línea.

Play Framework

De hecho Play Framework internamente usa Akka para procesar peticiones del Front-End en tiempo real o en linea como bien comentas. En la empresa donde laboro me pidieron migrar un proceso Batch hecho enteramente en Java (el cual era muy lento, pesado y fallaba mucho) a algo que fuera mas rápido, estable y ligero. Investigando fue como di con Akka, tal ves no fue creado con la intención de usarse en procesos Batch, pero sus características cumplían con lo que me pedían y realmente fue muy simple la implementación (tome el código heredado, definí y separe tareas, asigne las tareas a los actores, un actor lee el archivo, otros validan los registros, otros lo procesan, otros envian correo, otros persisten en BD, etc; ).

De 280 registros procesados/minuto pasamos hacer 8,000/min, el proceso se comporta muy bien a excepcion de algunos soportes por temas de infraestructura fallas en la RED.

Saludos!

Imagen de ezamudio

post

Pues no estaría mal que te echaras un post describiendo ese proceso, suena interesante.

Akka Post

Claro que si Enrique, tengo la intención de hacerlo e incluso como lo comente, comparar rendimiento frente a Spring Batch.

Imagen de AlexSnake

Spring Batch vs Sring Quartz

Se parece en algunas cosas a Quartz, yo ocupo este para correr procesos sincronizados para consultas a Base de datos y escrituras de archivos hacia FS en cuatro servidores que están en cluster y que tienen 3 clones cada uno, resulta muy interesante ver la armonía de ejecuciones que se hacen sin que se repitan las tareas entre estos. Solamente que no veo si tiene configuración de triggers en los jobs que tienes. Y as bitácoras se registran en tablas con prefijo QRTZ_
Me da la intención de poner a trabajar un mismo proceso con ambas soluciones, y ver pros y contras de cada unos, sin embargo no dudo que por alguna buena razón elegiste Spring Batch.

Saludos.

Imagen de ezamudio

funciona

La razón es que funciona. Tal vez quartz solito sea mejor, y sin duda quartz con spring debe ser una maravilla; no sé si sea mejor que spring batch, pero la verdad es que tampoco tengo tiempo de probar cada uno de los frameworks que existen para este tipo de cosas. Pregunté a varias personas, leí un poco de spring batch y lo probé y funcionó como necesitaba. No sé si tenga triggers, pero pues por ahora me basta con arrancarlo manualmente o desde un crontab.

La referencia

Para aquellos que estén interesados en Spring Batch, aquí pueden encontrar la referencia oficial. También en PDF.

style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">