Introducción a Spring Batch. Toma 2

Fuente: spring.io

 

Esta es la segunda parte de Introducción a Spring Batch. Toma 1 donde se explicaron algunos conceptos básicos sobre jobs de Spring Batch y se describió lo mínimo necesario para usar Spring Batch en una aplicación. ¡Continuamos!

 

5. Transaccionalidad

Un step se puede declarar como tasklet-oriented o chunk-oriented:

  • Step tasklet-oriented: Podemos definir una tarea simple con el método tasklet() que se caracteriza porque se crea una transacción por cada elemento. En caso de fallo de escritura, la transacción se revierte.
  • Step chunk-oriented: Un chunk es un conjunto de elementos que se leen y se procesan por separado pero se persisten juntos. El número de elementos por chunk se indica en la definición del step con el método chunk(). Con este tipo de steps, se crea una transacción por cada chunk.

Nos podría interesar elegir procesamiento chunk-oriented para maximizar el uso de la RAM o beneficiarnos de la transaccionalidad (por ejemplo, haciendo rollback de la escritura de 10 elementos que deberían ir juntos). Por otra parte, también nos podría interesar escribir un conjunto de elementos a la vez (p. ej, enviar a un endpoint que recibe un array de elementos).

Como siempre, con Spring Boot no es necesario declarar el transactionManager ya que es inyectado automáticamente por el framework.

En el siguiente commit, se ha declarado un ItemReader que lee un archivo CSV y un ItemWriter que muestra cada objeto Persona. También se ha cambiado la definición del step para establecer un chunk de 2 elementos.

Las implementaciones de FlatFileItemReader, DelimitedLineTokenizer, DefaultLineMapper y FieldSetMapper ofrecidas por Spring nos ayudan a leer un CSV en unas pocas líneas.

 

6. Tolerancia a fallos

Los componentes ItemReader, ItemProcessor e ItemWriter lanzarán excepciones en caso de error. Podemos evitar que ante una excepción rompa la aplicación con el método skipLimit() indicando el número de excepciones a saltar. Además los tipos de excepciones tienen que definirse usando el método skip() (tantas veces como sea necesario).

Para trabajar con los elementos saltados (que han provocado una excepción) se puede implementar SkipListener. Un ejemplo sería imprimir la entrada errónea o guardarla en otro CSV.

En el tercer commit, se ha introducido un error de lectura “-“ en el CSV y un error de escritura (lanzando la excepción IllegalArgumentException). Se ha incluido el método faultTolerant() en el step con un límite de 2 excepciones FlatFileParseException, FlatFileFormatException e IllegalArgumentException.

Como el límite es 2, el job se ejecutará correctamente saltando los 2 errores introducidos. Como ejercicio, podríamos añadir otro error al archivo CSV y ver cómo falla el job.

 

7. Reintentos

Es posible reintentar una acción que ha fallado previamente (tanto la lectura como el procesamiento o la escritura). En el cuarto commit se ha añadido una llamada al método retryLimit(1) para reintentar las excepciones del tipo IllegalArgumentException.

Es conveniente utilizar un límite para los reintentos ya que es posible que la misma excepción continúe sucediendo cada vez que se ejecute el mismo proceso.

 

8. Logs y estadísticas

El método StepBuilderFactory#listener() recibe cualquier tipo de listener como por ejemplo ChunkListener, StepListener, RetryListener… Todos estos pueden utilizarse para registrar el progreso (escribir logs) de un step. Como apoyo para escribir estos logs, podemos utilizar el contexto del step o del job para guardar timestamps o cualquier otro tipo de información.

Centrándonos en registrar el progreso de un job, definiendo JobBuilderFactory podemos usar el método .listener() pasando como argumento una instancia de JobExecutionListener.

La clase StepExecution tiene un método getSummary() que devuelve cierta información tras la ejecución del step, como por ejemplo:

El campo status devuelve un valor del enumerado BatchStatus y puede ser mapeado al código de finalización de la aplicación.

El campo exitStatus devuelve un valor del enumerado ExitStatus y representa el estado de finalización del step. Este valor no siempre es equivalente al valor del campo status. Además es posible personalizarlo para proporcionar más información y con ello, por ejemplo, definir un flujo condicional en el que el inicio de un step dependa del valor del campo exitStatus del step anterior.

En cualquier caso, Spring Batch permite definir flujos de steps donde un step se ejecuta  de manera condicional teniendo en cuenta el estado de finalización del step anterior.

En este commit se han definido unos cuantos listeners para registrar cada acción ocurrida durante el procesamiento batch. Además se ha añadido un atributo “timing” al contexto del chunk para medir el tiempo transcurrido durante el procesamiento de cada chunk.

 

9. Parando y re-arrancando un proceso batch

Spring Batch nos proporciona el bean JobOperator con los siguientes métodos:

  • getRunningExecutions(“jobName”) que devuelve una lista de ids de los jobs que se están ejecutando.
  • stop() que para la ejecución del step en cuanto se termina de ejecutar el código escrito en el mismo.
  • restart() que continúa la ejecución a partir del siguiente step.

 

10. Administración web

El proyecto Spring Cloud Data Flow proporciona una interfaz web y una CLI para administrar los jobs y streams que proceden de los proyectos Spring Cloud Task y Spring Cloud Stream. El objetivo del proyecto Spring Cloud Task es el de integrar los jobs de Spring Batch como microservicios en la nube.

Para convertir un proyecto en Spring Batch a uno de Spring Cloud Task únicamente es necesario añadir la dependencia y la anotación @EnableTask.

 

Aquí podemos encontrar los pasos necesarios para ejecutar un job dentro del Spring Cloud Data Flow:

Lo más común es registrar la aplicación con la URI de Maven (maven://) para que Spring Cloud Data Flow se descargue el artefacto del repositorio. Otros tipos de aplicaciones que se pueden registrar son sinks y sources.

Podemos consultar las estadísticas de los jobs accediendo a: http://localhost:9393/dashboard/index.html#/jobs/executions/1

Y las estadísticas de los steps en: http://localhost:9393/dashboard/index.html#/jobs/executions/1/1

Sin duda merece la pena darle una oportunidad a Spring Cloud Data Flow, tiene cosas muy chulas como por ejemplo, una herramienta visual para pintar el flujo de datos entre los distintos microservicios.

Es posible definir el directorio en el que se almacenarán los logs de la aplicación. No obstante, aunque no especifiquemos ninguna ruta, en el log de Spring Cloud Data Flow podremos distinguir una primera línea que comienza con “Logs will be in ” indicando la carpeta temporal elegida por Spring para este propósito.

En este commit podemos ver lo fácil que es convertir un proyecto Spring Cloud Data Flow:

 

11. Particionamiento

Existen diferentes estrategias para escalar un proceso batch ya sea de manera multihilo o multiproceso.

El particionamiento de un step es una de ellas y permite ejecutar steps en máquinas remotas o en hilos locales. Consiste en definir un step maestro (master step) que delegará el trabajo particionado en steps esclavos (slave steps). Para usar el particionamiento de steps es necesario definir tanto la estrategia de particionado como los esclavos.

El step maestro utilizará una implementación de Partitioner para escribir en el contexto de ejecución toda la información que necesitará cada esclavo para procesar su partición de datos.

En este último commit se ha añadido un segundo archivo CSV y se ha definido un bean CustomMultiResourcePartitioner que utilizará un step maestro para escribir el nombre del archivo CSV en cada contexto. Cada esclavo recibe un contexto diferente por parte del maestro.

Los dos esclavos se ejecutan en hilos diferentes porque se ha declarado un taskExecutor con el DSL del StepBuilderFactory.

Además, se ha utilizado la anotación @StepScope para recuperar el nombre del archivo guardado en el contexto usando el Spring Expression Language.

 

12. Extra:

Spring Batch proporciona múltiples implementaciones para facilitar el desarrollo de procesos batch. Aquí puedes encontrar una lista de ellas:

View at Medium.com

Esta segunda toma de la introducción a Spring Batch es una traducción del artículo publicado en el blog de Fintonic Product Team.

Anuncios

Introducción a Spring Batch. Toma 1

1. ¿Qué es un proceso batch?

Un proceso batch es una tarea automática para procesar un gran de volumen de datos. Este proceso se puede ejecutar sin interacción humana y repetirse periódicamente.

Qué no es un proceso batch: no es una tarea programada (un cron). Es bastante común programar un proceso batch, pero no es necesario hacerlo.

2. ¿Qué debemos tener en cuenta con un proceso batch?

  • Transaccionalidad porque queremos hacer roll back cuando los datos han sido invalidados.
  • Tolerancia a fallos porque no queremos que la aplicación termine cuando ocurra una excepción.
  • Reintentos porque… sometimes shit happens.
  • Logs y estadísticas porque de vez en cundo necesitamos saber qué ocurre dentro del proceso batch.
  • Parada y arranque de los procesos batch.
  • Administración web porque mola.
  • Particionamiento porque el trabajo puede ser compartido entre diferentes máquinas.

Cada uno de estos aspectos será comentado más tarde. He preparado unos cuantos commits en un repo para repasar todos estos puntos mostrando las diferencias en el código.

3. Conceptos de Spring Batch

Spring Batch es un proyecto que nos proporciona un framework para desarrollar aplicaciones batch. Este proyecto tiene un largo recorrido y es el resultado de la contribución de varias empresas con mucha experiencia en procesamiento batch.

Jobs, Steps, ItemReader, ItemProcessor and ItemWriter:

Fuente: Spring Batch Reference Documentation https://docs.spring.io/spring-batch/trunk/reference/html/domain.html

En Spring Batch se ejecutan jobs que se dividen en steps. Cada step tiene un componente para obtener los objetos (ItemReader), otro para procesarlos (ItemProcessor) y uno más para persistirlos (ItemWriter). El componente de procesamiento de datos es opcional.

Normalmente necesitaremos una instancia de JobBuilderFactory para declarar el Job y un StepBuilderFactory  para declarar el Step. No hay problema, Spring nos proporciona ambas.

JobExecution y JobLauncher

Un job puede ejecutarse por una instancia de JobLauncher. Durante su ejecución la información es almacenada y compartida en el JobExecutionContext y el StepExecutionContext.

El JobLauncher devuelve un JobExecution que nos da información sobre la ejecución, como por ejemplo el resultado de la misma: COMPLETED, FAILED…

JobInstance, JobParameters y RunIdIncrementer

JobInstance es la combinación de un Job y sus JobParameters. Una de las reglas de Spring Batch es que no se puede volver a ejecutar un Job si su JobExecution tiene estado COMPLETED. Sin embargo, se puede utilizar un RunIdIncrementer para ejecutar el mismo job varias veces ya que éste modifica internamente sus parámetros.

JobRepository

Spring Batch gestiona por sí sólo una base de datos con información sobre la ejecución de los jobs instanciando un bean de JobRepository. Para ello sólo necesitamos declarar la dependencia de H2 en el entorno de desarrollo.

4. Requisitos mínimos para comprobar que todo esto funciona

  • Dependencia de Spring Boot Starter Batch (aquí es donde reside toda la magia).
  • Dependencia del driver de la base de datos.
  • Anotación @EnableBatchProcessing en la clase de configuración de Spring.
  • Bean que define del job.
  • Bean que define el step con una tarea que escriba un mensaje “Reading…”. Cuando esa tarea devuelva un null indicará el fin de la fuente de datos y por tanto que el job ha terminado.
@Bean
public Job job(Step step1) throws Exception {
    return jobBuilderFactory.get("job1")
        .incrementer(new RunIdIncrementer())
        .start(step1)
        .build();
}

@Bean
public Step step1() {
    return stepBuilderFactory.get("step1")
        .tasklet(new Tasklet() {
            public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
                System.out.println("Reading...");
                return null;
            }
        })
        .build();
}

Puedes clonar el repo que he preparado para este post y ver el código añadido en el primer commit para ejecutar Spring Batch con lo mínimo necesario.

Este artículo es una traducción del que escribí hace unos meses en el blog de Fintonic Engineering.

Pensando en la documentación técnica como grafos

Lo he dicho ya varias veces, me he cansado de los words como documentación. Personalmente veo la documentación como un grafo de conocimiento vivo y alimentado por todo el equipo. Un grafo inteligente al que se le podría hacer preguntas.

documentationAsGraph

¿Qué representarían los nodos del grafo y qué información almacenarían?

En el grafo que sería la base de conocimiento del equipo cada nodo podría ser un concepto, un término, una pieza de la documentación técnica… Cualquier elemento al que se referencie siempre y de forma única porque, por ejemplo, no deberían haber dos guías de cómo usar Puppet en un proyecto de la organización.

Cada nodo almacenaría la URL donde estaría el texto de la documentación, el nombre de su autor, la fecha de la última modificación y la fecha de obligada revisión:

(howToPuppet:GUIDE {
name:"HowTo Puppet in a new project",
url:"doc://howtopuppet/",
author:"Jonás",
lastModified:"20160505",
revisionOn:"20160601"
})

En el ejemplo, aparezco como autor de una guía ficticia sobre cómo usar Puppet en un nuevo proyecto dentro de la organización. La última fecha de modificación indica cómo de actualizado está el documento y revisionOn establece una fecha en la que debería ser revisado para actualizar información obsoleta. Al grafo se le podría preguntar qué documentación tiene que ser revisada en una cierta fecha:

MATCH (docsToBeRevised)
WHERE docsToBeRevised.revisionOn <= "20160601"
RETURN docsToBeRevised.name

O quién es el gurú de cierto tema en el equipo preguntándole al oráculo quién ha modificado últimamente más documentación relacionada con un nodo concreto:

MATCH (n)-[*..2]-(f) WHERE n.name = "Puppet"
RETURN f.author AS Author, count(f.author) AS Docs
ORDER BY count(f.author) DESC LIMIT 1

Podemos jugar con grafos en la consola de neo4j, escribiéndolos en Cypher que es el lenguaje de consulta estilo SQL con el que también he creado un GraphGist muy sencillito para verlo en acción.

Las primitivas

El autor de Puppet, Luke Kanies, habla en el capítulo sobre esta herramienta en The Architecture of Open Source Applications que basaron la arquitectura de este framework en piezas primitivas. Al igual que se diseñó la arquitectura de Amazon como se comenta en este fantástico artículo sobre 10 lecciones que aprender (o que aprendieron) de los 10 años que recientemente ha cumplido AWS.

La idea es muy clara y reveladora: Diseñan piezas muy pequeñas y simples que se puedan conectar perfectamente entre ellas y que se pueden componer para construir sistemas más grandes y complejos. La ventaja principal de las piezas primitivas es que son tan pequeñas y básicas que se pueden utilizar en cualquier entorno y cualquiera puede entender su funcionamiento y trabajar con estas primitivas en muy poco tiempo.

Son los ladrillos de una casa, tan básicos que para la gran mayoría no hay ningún problema en levantar un muro. Pero a la vez, estas piezas tan pequeñas, como las letras, permiten construir sistemas muy complejos con significado propio como las oraciones. Estos sistemas se pueden conectar entre si porque sus primitivas son interconectables y se pueden ampliar o reducir. Se pueden descomponer en sistemas más pequeños (palabras) e investigar qué primitivas han tomado parte en el proceso para analizar la trazabilidad.

Obviamente es complicado llevar a la práctica el concepto de primitivas. Que cada organización tenga que diseñar y desarrollar piezas genéricas que encajen en cualquier entorno no es fácil ni de imaginar. Pero merece la pena tener la idea en la cabeza. Al fin y al cabo, la corriente de los microservicios que ha surgido a raíz de construir monstruos enormes va en la dirección de replantear el software para desarrollar sistemas que sean más fáciles de componer y descomponer. Como piezas de Lego.

Por una mejor documentación

Echo de menos que a los documentos técnicos no se le aplique el mismo rigor que al código que se desarrolla. Esto ocurre tanto en proyectos donde el negocio es el propio software como en proyectos donde el negocio es otro.

Igual que se usa Excel para todo, se usa siempre Word para mantener la documentación, sea guía de usuario, especificación técnica o normas empresariales. Al trabajar con esta documentación encuentro un montón de problemas:

Que haga falta Microsoft Word para poder ver el documento. Una documentación escrita en HTML se puede leer con cualquier navegador desde cualquier dispositivo.

Que se vean mil estilos diferentes dentro del documento. Con un buen conjunto de estilos predefinidos en CSS se evitaría que un usuario sin querer añada estilos extraños al documento. Además, utilizar características de los preprocesadores como las variables ayudan a que los estilos se diseñen con la precisión milimétrica que un ingeniero debe aplicar.

Que dos documentos de la misma empresa escritos en diferentes épocas tengan diferentes estilos. Un archivo de estilos CSS centralizado para todos los documentos evitaría la necesidad de actualizar los estilos de los documentos porque cambien los colores corporativos de la empresa.

Una tabla poco fiable de historial de versiones dentro del documento. Utilizar un sistema de control de versiones de verdad ayuda a recuperar versiones anteriores del documento y a entender cómo y por qué el documento está evolucionando.

Que los documentos se diseñen para ser imprimidos en lugar de para ser consultados. Una interfaz HTML aporta interactividad para navegar por el documento sin perder la capacidad de que se pueda imprimir teniendo un conjunto de estilos específicos para dicho formato.

Que haya información redundante ya sea porque haya varias definiciones de un mismo concepto o porque el mismo concepto tenga diferentes nombres a lo largo de todo el documento. Ya sabéis que en desarrollo de software, dos métodos que hagan lo mismo es pecado.

Con HTML, cada concepto puede ser un único archivo y cada vez que se nombre ese concepto en la documentación se enlazará a ese archivo (como Wikipedia = MediaWiki con la sintaxis [[]]).

Está claro que no todos los empleados que escriben documentación son técnicos y pueden encontrar dificultad en utilizar un sistema así. Creo que sería labor del departamento técnico educar para que se use un sistema de conocimiento correctamente porque uno de los mayores tesoros de una empresa es el know-how y mantener una documentación pobre y poco fiable no ayuda a nadie.