SyntaxHighlighter

Showing posts with label Spring Batch. Show all posts
Showing posts with label Spring Batch. Show all posts

Saturday, August 17, 2013

How to Emulate a remote JVM in JUnit using Spring, Ant, H2 and ActiveMQ

How to Emulate a remote JVM in JUnit using Spring, Ant, H2 and ActiveMQ


recently i needed to test a scenario where one JVM was communicating to another using both a shared database and JMS.  i also needed to have each JVM be "invisible" to each other and make sure that the couldn't 'accidentally' share things across and application context.

so, to do this, i needed to kick start a separate JVM from inside a JUnit test, and communicate via JMS and a database... fun.

here's what i needed;

- shared database (standalone server)
- shared JMS (standalone server)
- JVM A - application context with unique configuration
- JVM B - application context with unique configuration

so, here's the solution i put together with some google/stackoverflow help and the following;
- Spring
- JUnit 4
- Ant
- H2
- ActiveMQ

here's the infrastructure utilities class.  it's responsible for setting up 'shared' infrastructure such as H2 and ActiveMQ.  it also provides hooks for binding to JNDI in a standalone context to help with sharing the resources in an 'agnostic' kind of way.

package de.incompleteco.spring.batch.ha;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

import javax.sql.DataSource;

import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.command.ActiveMQQueue;
import org.apache.activemq.spring.ActiveMQConnectionFactory;
import org.h2.jdbcx.JdbcConnectionPool;
import org.h2.jdbcx.JdbcDataSource;
import org.h2.tools.Server;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.mock.jndi.SimpleNamingContextBuilder;

public class InfrastructureUtils {

 private static final Logger logger = LoggerFactory.getLogger(InfrastructureUtils.class);
 
 private static Server server;
 
 private static BrokerService broker;
 
 public static void startH2() throws Exception {
  if (server == null) {
   server = Server.createTcpServer("-tcpAllowOthers");
   server.start();
   logger.info(server.getStatus());
  }//end if
 } 
 
 public static void stopH2() throws Exception {
  if (server != null) {
   server.stop();
  }//end if
 } 
 
 public static void startAMQ() throws Exception {
  if (broker == null) {
   broker = new BrokerService();
   broker.addConnector("tcp://localhost:61616");
   broker.setUseJmx(false);
   broker.setUseShutdownHook(true);
   broker.start();
   broker.deleteAllMessages();//clean up
  }//end if
 }
 
 public static void stopAMQ() throws Exception {
  if (broker != null) {
   broker.stop();
  }//end if
 } 
 
 public static void bindLocalAMQ(String connectionFactoryName,String... queueNames) {
  //get jndi
  SimpleNamingContextBuilder builder = SimpleNamingContextBuilder.getCurrentContextBuilder();
  //get the connection
  ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory();
  connectionFactory.setBrokerURL("tcp://localhost:61616");
  //bind
  builder.bind("jms/" + connectionFactoryName,connectionFactory);
  //setup the queues
  for (String name : queueNames) {
   builder.bind("jms/" + name,new ActiveMQQueue(name));   
  }//end for
 } 
 
 public static DataSource bindLocalH2(String dataSourceName) throws Exception {
  JdbcDataSource dataSource = new JdbcDataSource();
  dataSource.setURL("jdbc:h2:tcp://localhost/~/test");
  //build a pool and bind
  JdbcConnectionPool pool = JdbcConnectionPool.create(dataSource);
  SimpleNamingContextBuilder builder = SimpleNamingContextBuilder.getCurrentContextBuilder();
  builder.bind("jdbc/" + dataSourceName, pool);
  //return it in case there's other uses
  return dataSource;
 } 
 
 public static String[] convertSqlFile(String location) throws Exception {
  List sqlStatements = new ArrayList();
  //load up the file
  BufferedReader reader = new BufferedReader(new InputStreamReader(TestSimpleBatchHAService.class.getResourceAsStream(location)));
  String line;
  StringBuilder statement = new StringBuilder();
  while ((line = reader.readLine()) != null) {
   if (line.contains(";") && !line.contains("--")) {
    statement.append(line);
    sqlStatements.add(statement.toString().replace(';',' '));
    statement = new StringBuilder();//reset the string
   } else if (line.contains("--")) {
    //ignore
   } else {
    statement.append(line);
   }//end if
  }//end while
  reader.close();
  //return
  return sqlStatements.toArray(new String[sqlStatements.size()]);
 } 
}


here's the RemoteJVMEmulator class.  it's a 'standalone' class (static void main(..)) that bootstraps and application context.  it also uses the infrastructure utilities to bind the resources to the JNDI.

package de.incompleteco.spring.batch.ha;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.mock.jndi.SimpleNamingContextBuilder;

public class RemoteJVMEmulator {

 private static final Logger logger = LoggerFactory.getLogger(RemoteJVMEmulator.class);
 
 //bind shared resources to JNDI
 public static void setupJNDI() throws Exception {
  SimpleNamingContextBuilder.emptyActivatedContextBuilder();
  //bind AMQ
  InfrastructureUtils.bindLocalAMQ("ConnectionFactory","request.queue","reply.queue");
  //bind h2
  InfrastructureUtils.bindLocalH2("DataSource");
 }
 
 public static void main(String[] args) throws Exception {
  //setup jndi
  setupJNDI();
  //start the app context
  ApplicationContext context = new ClassPathXmlApplicationContext(args);
  //print a statement saying "it's up"
  logger.info(RemoteJVMEmulator.class.getSimpleName() + " is up " + context.getStartupDate());
 }

}


and here's the test runner.  this uses the infrastructure utilities to startup the shared services and bind them locally.  it then uses and inner class to build and run and Ant task that will kick off the RemoteJVMEmulator class.  a couple of things to note about it though;
- the Ant task is started in a separate thread - mainly because it blocks
- the Ant task is started first to act as the 'remote client'/'listener'
- the 'remote client'/'listener' needs to be setup such that you don't need to be in the same JVM to access any information from it.  (treat it as a 'remote' server deployed and running without any interaction services - you could connect to it via JMX or expose other services, that's up to you)

package de.incompleteco.spring.batch.ha;

import static org.junit.Assert.assertFalse;

import java.io.PrintStream;

import javax.sql.DataSource;

import org.apache.tools.ant.DefaultLogger;
import org.apache.tools.ant.DemuxOutputStream;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Java;
import org.apache.tools.ant.types.Path;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.jndi.SimpleNamingContextBuilder;

//don't run this test in CI
@Ignore
public class TestSimpleBatchHAService {

 
 @BeforeClass
 public static void beforeClass() throws Exception {
  //start a builder
  SimpleNamingContextBuilder.emptyActivatedContextBuilder();
  //setup the services (h2 and amq)
  InfrastructureUtils.startH2();
  //setup the database
  DataSource dataSource = InfrastructureUtils.bindLocalH2("DataSource");
  setupH2Data(dataSource);
  //start amq
  InfrastructureUtils.startAMQ();
  //bind
  InfrastructureUtils.bindLocalAMQ("ConnectionFactory","request.queue","reply.queue");
 }
 
 @AfterClass
 public static void afterClass() throws Exception {
  //shutdown the services
  InfrastructureUtils.stopH2();
  InfrastructureUtils.stopAMQ();
 }
 
 private static void setupH2Data(DataSource dataSource) throws Exception {
  //execute the statements
  JdbcTemplate template = new JdbcTemplate(dataSource);
  //drop statements
  String[] statements = InfrastructureUtils.convertSqlFile("/org/springframework/batch/core/schema-drop-h2.sql");
  try {
   for (String statement : statements) {
    template.execute(statement);
   }//end for
  }
  catch (Exception e) { }
  statements = InfrastructureUtils.convertSqlFile("/META-INF/sql/schema-ext-drop-h2.sql");
  try {
   for (String statement : statements) {
    template.execute(statement);
   }//end for
  }
  catch (Exception e) { }  
  //create statements
  statements = InfrastructureUtils.convertSqlFile("/org/springframework/batch/core/schema-h2.sql");
  for (String statement : statements) {
   template.execute(statement);
  }//end for  
  //create statements
  statements = InfrastructureUtils.convertSqlFile("/META-INF/sql/schema-ext-h2.sql");
  for (String statement : statements) {
   template.execute(statement);
  }//end for    
 }
 
 
 @Test
 public void testExecute() throws Exception {
  //start a thread to run the remote
  new Thread(new RemoteJVMRunner()).start();
  //now start the 'server'
  ApplicationContext context = new ClassPathXmlApplicationContext("classpath:/META-INF/spring/server-context.xml");
  //now that it's started, run the job
  Job job = context.getBean(Job.class);
  JobParameters parameters = new JobParametersBuilder().addLong("runtime",System.currentTimeMillis()).toJobParameters();
  JobLauncher launcher = context.getBean("remoteJobLauncher", JobLauncher.class);
  JobExecution execution = launcher.run(job,parameters);
  JobExplorer explorer = context.getBean(JobExplorer.class);
  //monitor
  while (explorer.getJobExecution(execution.getId()).isRunning()) {
   Thread.sleep(500);
  }//end while
  //reload the execution
  execution = explorer.getJobExecution(execution.getId());
  //check
  assertFalse(execution.getStatus().isUnsuccessful());
 }

 
 class RemoteJVMRunner implements Runnable {
  
  @Override
  public void run() {
   Project project = new Project();
   project.setName("remote-jvm");
   project.init();
   //setup the logger
   DefaultLogger logger = new DefaultLogger();
   project.addBuildListener(logger);
   logger.setOutputPrintStream(System.out);
   logger.setErrorPrintStream(System.err);
   logger.setMessageOutputLevel(Project.MSG_INFO);
   System.setOut(new PrintStream(new DemuxOutputStream(project,false)));
   System.setErr(new PrintStream(new DemuxOutputStream(project,true)));
   //start the project
   project.fireBuildStarted();
   
   Java java = new Java();
   java.setProject(project);;
   java.setTaskName("run-remote-jvm");
   java.setFork(true);
   java.setFailonerror(true);
   //set the classname
   java.setClassname(RemoteJVMEmulator.class.getName());
   java.setClasspath(new Path(project,System.getProperty("java.class.path")));
   //create arguments
   java.createArg().setValue("classpath:/META-INF/spring/client-context.xml");
   //init
   java.init();
   //execute
   java.executeJava();
  }
 }
}


Spring Batch High Availability

Spring Batch High Availability


here's a quick project i recently threw together to demonstrate how to setup a high availability cluster for processing Spring Batch.  HA doesn't mean what you typically think, but in a batch environment, it allows for a configurable period of time in which should a job 'stop running' (that is, seem to be running but the underlying JVM has failed), the job will be 'failed' and restarted immediately on another cluster. the design is as follows;

Master/Heartbeat Consumer/Launcher

Overview

- JVM that starts Jobs on itself or other nodes
- Heartbeat Consumer - runs a TCP socket server that listens for 'heartbeats' from other JVM's
- Is responsible for failing over a job to another server should the job fail where it is

Key Components

- MessageChannelJobLauncher - uses Spring Integration to serialize the Job's name and it's parameters and send it out as a start request
- BatchHAService - service to be periodically invoked and check for that executions registered are still running/stable - responsible for failing and restarting jobs that become 'ghosts'
- BatchHeartbeatConsumerService - responsible for consuming heartbeats and registering job execution ids

Client/Heartbeat Producer

Overview

- JVM that actually runs a Job
- produces Heartbeats - a list of registered/in-flight job execution ids
- uses a RemoteJobRegistry to persist job names in a shared batch database

Key Components

- JobExecutionRegisterListener - wraps any jobs and registers the job execution id in the current JVM
- JobExecutionRegisterListenerPostProcessor - responsible for implementing the Listener on all Jobs
- RemoteJobRegistry - wraps the 'JVM local' JobRegistry and persists in a shared database, any Job names on the local JVM
- BatchHeartbeatClientService - responsible for accepting jobExecution registers and publishing

The infrastructure pieces are;
- Job "start" requests are sent via JMS
- there is a shared Job Database (batch database) that includes a new table, Job_Entity for persisting job names
- TCP server/client implemented in Spring Integration to publish the Job Execution Ids as part of the heart beat

the code will be up on github soon and i'll flesh it out properly, but it really is magic what Spring Integration can do.


Thursday, July 18, 2013

Spring Batch - How to stop simultaneous execution of a Job

Spring Batch - How to stop simultaneous execution of a Job


so here's the use case; Job A can only be "in-flight"/running one at a time.  That is, for business or resource reasons, no matter if the job parameters are different, only one instance of the Job can be running at a time.

here's a quick AOP solution

first our class;
package de.incompleteco.spring.batch.support;

import java.util.List;
import java.util.Set;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;

public class SimultaneousJobAspect implements MethodInterceptor, InitializingBean {

 private static final int JOB_INDEX = 0;
 
 //list of job names that SHOULD NOT run simultaneously
 private List jobNames;
 
 private JobExplorer jobExplorer;
 
 @Override
 public Object invoke(MethodInvocation invocation) throws Throwable {
  //get the job names
  Object[] arguments = invocation.getArguments();
  //get the 'job' argument (argument 0)
  Job job = (Job) arguments[JOB_INDEX];
  //get the name
  for (String jobName : jobNames) {
   if (jobName.equalsIgnoreCase(job.getName())) {
    //check if there's one running
    Set jobExecutions = jobExplorer.findRunningJobExecutions(jobName);
    if (jobExecutions != null && !jobExecutions.isEmpty()) {
     //have a match --> throw a job exception
     throw new JobExecutionAlreadyRunningException(jobName + " is already running and can't be run simultaneously");
    }//end if
   }//end if
  }//end for
  //continue
  return invocation.proceed();
 }

 public void setJobNames(List jobNames) {
  this.jobNames = jobNames;
 }

 public void setJobExplorer(JobExplorer jobExplorer) {
  this.jobExplorer = jobExplorer;
 }

 @Override
 public void afterPropertiesSet() throws Exception {
  Assert.notNull(jobExplorer);
 }
 
}


then the aspect configuration (using the namespace)
 
  
 


 
  
  
   
    longJob
   
  
  

now, for testing, the test class
package de.incompleteco.spring.batch;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.fail;

import javax.annotation.Resource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:/META-INF/spring/*-context.xml"})
public class SimultaneousJobAspectIntegrationTest {

 @Resource
 private JobLauncher jobLauncher;
 
 @Resource
 private Job simpleJob;
 
 @Resource
 private Job longJob;
 
 @Resource
 private JobExplorer jobExplorer;
 
 @Test
 public void test() throws Exception {
  //start  a simple job
  JobParameters jobParameters = new JobParametersBuilder().addLong("runtime",System.currentTimeMillis()).toJobParameters();
  JobExecution execution = jobLauncher.run(simpleJob,jobParameters);
  //monitor
  while (jobExplorer.getJobExecution(execution.getId()).isRunning()) {
   Thread.sleep(100);
  }//end while
  //load
  execution = jobExplorer.getJobExecution(execution.getId());
  //check
  assertFalse(execution.getStatus().isUnsuccessful());
  //now start a long job
  execution = jobLauncher.run(longJob,jobParameters);
  //start another one with different parameters
  jobParameters = new JobParametersBuilder().addLong("runtime",System.currentTimeMillis()).toJobParameters();
  try {
   execution = jobLauncher.run(longJob,jobParameters);
   fail("should've failed");
  }
  catch (JobExecutionAlreadyRunningException e) { }
  
 }
 
}


and our job and batch configs
 
  
   
    
     
      
       
      
     
     
    
   
  
 

 
  
   
    
   
  
 

 
 
 
  
 
 
 

 
  
  
 
 
 

 
 

oh, and the class that does the delay;
package de.incompleteco.spring.batch.tasklet;

import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;

public class LongTasklet implements Tasklet {

 @Override
 public RepeatStatus execute(StepContribution contribution,ChunkContext chunkContext) throws Exception {
  
  Thread.sleep(10 * 1000);// wait for 10 seconds
  
  return RepeatStatus.FINISHED;
 }

}

Monday, July 15, 2013

Spring Batch In-Memory Configuration

Spring Batch In-Memory Configuration


here's a snippet to illustrate configuration of Spring Batch Infrastructure (jobRepository, jobExplorer, jobLauncher) purely in-memory for typical JUnit Integration testing.



 
  
 

 
  
 
 
 
  
  
 

 
 
 




it gives you the basics to support a simple unit test with the following setup;

package de.incompleteco.spring.batch.job;

import static org.junit.Assert.assertFalse;

import javax.annotation.Resource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=SimpleJobConfig.class)
public class SimpleJobConfigTest {

 @Resource
 private Job job;
 
 @Resource
 private JobLauncher jobLauncher;
 
 @Resource
 private JobExplorer jobExplorer;
 
 @Test
 public void test() throws Exception  {
  //run the job
  JobExecution execution = jobLauncher.run(job,new JobParametersBuilder().toJobParameters());
  //monitor
  while (jobExplorer.getJobExecution(execution.getId()).isRunning()) {
   Thread.sleep(100);
  }//end while
  //load
  execution = jobExplorer.getJobExecution(execution.getId());
  //check
  assertFalse(execution.getStatus().isUnsuccessful());
 }


Sunday, July 14, 2013

Spring Batch Job as Neo4J Graph

Spring Batch Job as Neo4J Graph


here's a quick snippet on how to turn a Spring Batch Job object into a Neo4J graph representation.

first the node structure which looks something like this;



a Job;

package de.incompleteco.spring.batch.graph.domain;

import java.io.Serializable;

import org.springframework.data.neo4j.annotation.GraphId;
import org.springframework.data.neo4j.annotation.Indexed;
import org.springframework.data.neo4j.annotation.NodeEntity;

@NodeEntity
public class JobNode implements Serializable {

 private static final long serialVersionUID = 1L;

 @GraphId
 private Long id;
 
 @Indexed
 private String name;
 
 private StepNode firstNode;
 
 public JobNode() { }
 
 public JobNode(String name) {
  this.name = name;
 }

 public Long getId() {
  return id;
 }

 public void setId(Long id) {
  this.id = id;
 }

 public String getName() {
  return name;
 }

 public void setName(String name) {
  this.name = name;
 }

 public StepNode getFirstNode() {
  return firstNode;
 }

 public void setFirstNode(StepNode firstNode) {
  this.firstNode = firstNode;
 }
 
 
}

a Step;
package de.incompleteco.spring.batch.graph.domain;

import java.io.Serializable;
import java.util.Set;

import org.neo4j.graphdb.Direction;
import org.springframework.data.neo4j.annotation.Fetch;
import org.springframework.data.neo4j.annotation.GraphId;
import org.springframework.data.neo4j.annotation.Indexed;
import org.springframework.data.neo4j.annotation.NodeEntity;
import org.springframework.data.neo4j.annotation.RelatedTo;

import com.fasterxml.jackson.annotation.JsonIgnore;

@NodeEntity
public class StepNode implements Serializable {

 private static final long serialVersionUID = 1L;
 
 @GraphId
 private Long id;
 
 @Indexed
 private String name;
 
 private StepType type;
 
 @JsonIgnore
 @Fetch @RelatedTo(type="NEXT_NODE",direction=Direction.INCOMING)
 private Set < StepNode > parent;
 
 @Fetch @RelatedTo(type="NEXT_NODE")
 private Set < StepNode > next; 
 
 public StepNode() { }
 
 public StepNode(String name) {
  this();
  this.name = name;
 }

 public Long getId() {
  return id;
 }

 public void setId(Long id) {
  this.id = id;
 }

 public String getName() {
  return name;
 }

 public void setName(String name) {
  this.name = name;
 }

 public StepType getType() {
  return type;
 }

 public void setType(StepType type) {
  this.type = type;
 }

 public Set < StepNode > getParent() {
  return parent;
 }

 public void setParent(Set < StepNode > parent) {
  this.parent = parent;
 }

 public Set < StepNode > getNext() {
  return next;
 }

 public void setNext(Set < StepNode > next) {
  this.next = next;
 }

 @Override
 public String toString() {
  return "StepNode [id=" + id + ", name=" + name + ", type=" + type + "]";
 }
 
 

}


the Relationship between Steps (the black lines between nodes);
package de.incompleteco.spring.batch.graph.domain;

import java.io.Serializable;

import org.springframework.data.neo4j.annotation.EndNode;
import org.springframework.data.neo4j.annotation.GraphId;
import org.springframework.data.neo4j.annotation.RelationshipEntity;
import org.springframework.data.neo4j.annotation.StartNode;

@RelationshipEntity(type="NEXT_NODE")
public class NextNode implements Serializable {
 
 private static final long serialVersionUID = 1L;

 @GraphId
 private Long id;
 
 @StartNode
 private StepNode startNode;
 
 @EndNode
 private StepNode endNode;

 public Long getId() {
  return id;
 }

 public void setId(Long id) {
  this.id = id;
 }

 public StepNode getStartNode() {
  return startNode;
 }

 public void setStartNode(StepNode startNode) {
  this.startNode = startNode;
 }

 public StepNode getEndNode() {
  return endNode;
 }

 public void setEndNode(StepNode endNode) {
  this.endNode = endNode;
 }
 
 
}


a supporting "type";
package de.incompleteco.spring.batch.graph.domain;

import org.springframework.batch.core.job.flow.State;
import org.springframework.batch.core.job.flow.support.state.DecisionState;
import org.springframework.batch.core.job.flow.support.state.SplitState;

public enum StepType {

 STEP,DECISION,SPLIT;

 public static StepType getType(State state) {
  if (state instanceof DecisionState) {
   return DECISION;
  } else if (state instanceof SplitState) {
   return SPLIT;
  } else {
   return STEP;
  }
 }
 
}


now, a couple of supporting players in this pattern, namely to help with the transactional aspect of Spring-Data-Neo4J (this is pretty rough and could be cleaned up a lot)

the GraphService;
package de.incompleteco.spring.batch.graph;

import de.incompleteco.spring.batch.graph.domain.JobNode;
import de.incompleteco.spring.batch.graph.domain.StepNode;

public interface GraphService {

 public void saveStepNode(StepNode stepNode);

 public void addNext(StepNode startNode,StepNode nextNode);
 
 public void saveJobNode(JobNode jobNode);
}


the Neo4JGraphService;
package de.incompleteco.spring.batch.graph;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.neo4j.support.Neo4jTemplate;
import org.springframework.transaction.annotation.Transactional;

import de.incompleteco.spring.batch.graph.domain.JobNode;
import de.incompleteco.spring.batch.graph.domain.NextNode;
import de.incompleteco.spring.batch.graph.domain.StepNode;

public class Neo4JGraphService implements GraphService {

 @Autowired
 private Neo4jTemplate template;
 
 @Override
 @Transactional
 public void saveStepNode(StepNode stepNode) {
  System.out.println("saving..." + stepNode);
  template.save(stepNode);
 }

 @Override
 @Transactional
 public void addNext(StepNode startNode, StepNode nextNode) {
  System.out.println("creating from " + startNode + " to " + nextNode);
  template.createRelationshipBetween(startNode, nextNode, NextNode.class, "NEXT_NODE", true);
 }

 @Override
 @Transactional 
 public void saveJobNode(JobNode jobNode) {
  template.save(jobNode);
 }

 public void setTemplate(Neo4jTemplate template) {
  this.template = template;
 }

}


and finally, the guts of it all, the GraphBuilder;
package de.incompleteco.spring.batch.graph;

import java.lang.reflect.Field;
import java.util.List;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.job.flow.Flow;
import org.springframework.batch.core.job.flow.FlowJob;
import org.springframework.batch.core.job.flow.State;
import org.springframework.batch.core.job.flow.support.SimpleFlow;
import org.springframework.batch.core.job.flow.support.StateTransition;
import org.springframework.batch.core.job.flow.support.state.DecisionState;
import org.springframework.batch.core.job.flow.support.state.SplitState;
import org.springframework.beans.factory.annotation.Autowired;

import de.incompleteco.spring.batch.graph.domain.JobNode;
import de.incompleteco.spring.batch.graph.domain.StepNode;
import de.incompleteco.spring.batch.graph.domain.StepNodeRepository;
import de.incompleteco.spring.batch.graph.domain.StepType;

public class GraphBuilder {

 private static final String PATTERN_FIELD = "pattern";
 private static final String END_NEXT_PATTERN = "\\w.*.end\\d.*";
 private static final String FAIL_NEXT_PATTERN = "\\w.*.fail\\d.*";
 private static final String UNKNOWN_PATTERN = "UNKNOWN";
 private static final String DELEGATE_STATE = "DelegateState";
 private static final String FLOW_FIELD = "flow";
 private static final String STATE_TRANSITIONS_FIELD = "stateTransitions";
 private static final String START_STATE_FIELD = "startState";
 
 @Autowired
 private StepNodeRepository stepNodeRepository;
 
 @Autowired
 private GraphService graphService;
 
 public JobNode buildJobNode(Job job) throws Exception {
  //extract the flow
  FlowJob flowJob = (FlowJob) job;
  Field field = flowJob.getClass().getDeclaredField(FLOW_FIELD);
  field.setAccessible(true);
  SimpleFlow flow = (SimpleFlow) field.get(flowJob);
  build(flow);
  //build the first one
  field = flow.getClass().getDeclaredField(START_STATE_FIELD);
  field.setAccessible(true);
  State state = (State) field.get(flow);
  StepNode startNode = stepNodeRepository.findByName(state.getName());
  //set into the job
  JobNode jobNode = new JobNode(job.getName());
  jobNode.setFirstNode(startNode);
  //save
  graphService.saveJobNode(jobNode);
  //return
  return jobNode;
 }
 
 
 @SuppressWarnings("unchecked")
 public void build(SimpleFlow simpleFlow) throws Exception {
  Field field = simpleFlow.getClass().getDeclaredField(STATE_TRANSITIONS_FIELD);
  field.setAccessible(true);
  List < StateTransition > transitions = (List < StateTransition > )field.get(simpleFlow);
  //loop
  for (StateTransition stateTransition : transitions) {
   //init
   StepNode node = null;
   StepNode nextNode = null;
   String pattern = null;
   //examine the stateTransition
   State state = stateTransition.getState();
   field = stateTransition.getClass().getDeclaredField(PATTERN_FIELD);
   field.setAccessible(true);
   pattern = field.get(stateTransition).toString();
   //check
   if (stateTransition.getNext() != null 
     && stateTransition.getNext().matches(FAIL_NEXT_PATTERN)) {
    continue;//skip this one
   } else if (pattern.equals(UNKNOWN_PATTERN) 
     && stateTransition.getNext() == null) {
    continue;//skip this one
   } else if (state.getClass().getSimpleName().equals(DELEGATE_STATE)) {
    continue;//skip this one
   } else if (state instanceof DecisionState 
     && (stateTransition.getNext() != null 
     && stateTransition.getNext().matches(END_NEXT_PATTERN))) {
    continue;//skip this one
   }//end if
   //we now have a record to look at
   node = stepNodeRepository.findByName(state.getName());
   //check if it exists
   if (node == null) {
    //build it
    node = new StepNode(state.getName());
   }//end if
   //manage the 'type'
   node.setType(StepType.getType(state));   
   //persist
   graphService.saveStepNode(node);
   //process the 'next' node
   if (!stateTransition.getNext().matches(END_NEXT_PATTERN) 
     && !stateTransition.getNext().matches(FAIL_NEXT_PATTERN)) {
    //look it up
    nextNode = stepNodeRepository.findByName(stateTransition.getNext());
    if (nextNode == null) {
     nextNode = new StepNode(stateTransition.getNext());
     //persist
     graphService.saveStepNode(nextNode);
    }//end if
    //create the relationship
    graphService.addNext(node, nextNode);
   }//end if
   //now - extra handling for a SPLIT
   if (node.getType() == StepType.SPLIT) {
    //process the 'inner' flows
    for (Flow innerFlow : ((SplitState)state).getFlows()) {
     //build the steps
     build((SimpleFlow)innerFlow);
     //now add to the split
     field = innerFlow.getClass().getDeclaredField(START_STATE_FIELD);
     field.setAccessible(true);
     String stepName = ((State)field.get(innerFlow)).getName();
     //retrieve
     nextNode = stepNodeRepository.findByName(stepName);
     if (nextNode == null) {
      nextNode = new StepNode(stepName);
      //persist
      graphService.saveStepNode(nextNode);
     }//end if
     //add the relationship
     graphService.addNext(node, nextNode);
    }//end for
   }//end if
   }//end for
 }


 public void setStepNodeRepository(StepNodeRepository stepNodeRepository) {
  this.stepNodeRepository = stepNodeRepository;
 }


 public void setGraphService(GraphService graphService) {
  this.graphService = graphService;
 }
 
}


there's a few key things to note in this;
1. it assumes a FlowJob - this whole pattern doesn't really work without one and a FlowJob is what the XML config will build when parsed
2. it uses reflection to retrieve key pieces - StateTransition list is a private field but we need access to it to interrogate the structure
3. the pattern is after loading, to support different ways of defining jobs (i.e. namespace, bean, JavaConfig)

and now the Spring-Data-Neo4J config,



 
 
 
 
 
 
 
 
 

 



and our test batch xml.



 
  
 

 
  
 
 
 
  
  
 

 
 
 







 

 
  
   
  
  
   
  
  
   
  
  
   
  
  
   
          
 
 
 
 
  
   
  
  
   
  
  
   
    
     
    
    
     
        
   
   
    
     
    
    
     
       
   
   
    
     
       
      
  
  
   
  
  

 
  
   
    
   
  
  
 



so, now here's the unit test.
package de.incompleteco.spring.batch.graph;

import javax.annotation.Resource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.batch.core.Job;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.fasterxml.jackson.databind.ObjectMapper;

import de.incompleteco.spring.batch.graph.domain.JobNode;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath*:/META-INF/spring/*-context.xml"})
public class GraphBuilderIntegrationTest {

 @Resource
 private GraphBuilder builder;
 
 @Resource(name="simpleLinearJob")
 private Job job;
 
 @Resource(name="decisionAndFlowJob")
 private Job complextJob; 
 
 @Test
 public void testBuildComplexJobNode() throws Exception {
  JobNode jobNode = builder.buildJobNode(complextJob);
  //show it
  new ObjectMapper().writerWithDefaultPrettyPrinter().writeValue(System.out, jobNode);
 } 

}


finally, to help out, here's the maven pom.xml too

  4.0.0
  de.incompleteco.spring.batch.graph
  spring-batch-graph
  0.0.1-SNAPSHOT
  spring-batch-graph
    
  
    
      org.apache.maven.plugins
      maven-compiler-plugin
      
        1.7
        1.7
      
    
    
  
  
   
    org.springframework.batch
    spring-batch-core
    2.2.0.RELEASE
   
   
    org.springframework.data
    spring-data-neo4j
    2.0.2.RELEASE
   
   
    org.neo4j
    neo4j-cypher
    1.6
   
   
    org.springframework
    spring-test
    3.2.2.RELEASE
    test
   
   
    junit
    junit
    4.10
    test
   
   
    com.fasterxml.jackson.core
    jackson-databind
    2.2.0
    test
   
  



Friday, July 5, 2013

Spring Batch - looping over multiple files

Spring Batch - looping over multiple files


here's a quick solution to a scenario coming from this question.  essentially, an array of file names will be passed in as a job parameter and the job needs to process each of these files.  one option is to have the 'looping' part managed by the job orchestrator (it loops, gets a single file, then kicks off the job for that file) the other is to pass the array in, and then loop internally in the job.

which option to choose would be dependent on other aspects of the use case, but here's an example of looping internally in the job

here's the job definition;



 
  
   
   
  
  
   
    
   
  
  
   
    
      
  
 
 

 
  
  
 
 
 


 
 
 
 
 
 
 



now here's the decider code;

package de.incompleteco.spring.batch.decider;

import java.util.Arrays;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;

import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.job.flow.FlowExecutionStatus;
import org.springframework.batch.core.job.flow.JobExecutionDecider;

public class FileDecision implements JobExecutionDecider {

 public static final String INPUT_FILE = "input.file";
 public static final String INPUT_FILES = "input.files";
 public static final String DELIMITER = ",";
 
 private Queue < String > inputFiles;
 
 @Override
 public FlowExecutionStatus decide(JobExecution jobExecution,StepExecution stepExecution) {
  //check if the jobExecution has the input.file in it's context
  if (!jobExecution.getExecutionContext().containsKey(INPUT_FILE)) {
   //build the queue
   inputFiles = new LinkedBlockingQueue < String >(Arrays.asList(jobExecution.getJobParameters().getString(INPUT_FILES).split(DELIMITER)));
  }//end if
  //pop and add
  String file = inputFiles.poll();
  if (file != null) {
   jobExecution.getExecutionContext().put(INPUT_FILE, file);
   return FlowExecutionStatus.UNKNOWN;
  }//end if
  //return 'done'
  return FlowExecutionStatus.COMPLETED;
 }

}


our helper writer;

package de.incompleteco.spring.batch.item.writer;

import java.util.List;

import org.springframework.batch.item.ItemWriter;

public class SystemOutItemWriter implements ItemWriter < String > {

 @Override
 public void write(List items) throws Exception {
  for (String item : items) {
   System.out.println("this is what was received:" + item);
  }//end for
 }

}


and our resources.



 
 
 
  
 
 
 

 
  
  
 
 
 



finally, our unit test to see if it all works;

package de.incompleteco.spring.batch;

import static org.junit.Assert.assertFalse;

import java.io.File;
import java.io.FileWriter;

import javax.annotation.Resource;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import de.incompleteco.spring.batch.decider.FileDecision;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:/META-INF/spring/*-context.xml")
public class MultipleFileProcessIntegrationTest {

 @Rule
 public TemporaryFolder folder = new TemporaryFolder();
 
 @Resource
 private Job job;
 
 @Resource
 private JobLauncher jobLauncher;
 
 @Resource
 private JobExplorer jobExplorer;
 
 @Test
 public void test() throws Exception {
  //somewhere to hold the filenames
  StringBuilder builder = new StringBuilder();
  //create 3 files
  for (int i=0;i<3 add="" content:="" content="" file="" filename="" filewriter="" i="" if="" some="" test="" testfile="" the="" txt="" write="" writer.close="" writer.flush="" writer.write="" writer="new"> 0) {
    builder.append(FileDecision.DELIMITER);
   }//end if
   builder.append(file.getAbsolutePath());
   //show it
   System.out.println(file.getAbsolutePath());
  }//end loop
  
  
  //now build the job parameters
  JobParameters parameters = new JobParametersBuilder().addString(FileDecision.INPUT_FILES,builder.toString()).toJobParameters();
  //execution
  JobExecution execution = jobLauncher.run(job,parameters);
  //check
  while (jobExplorer.getJobExecution(execution.getId()).isRunning()) {
   Thread.sleep(100);
  }//end while
  //load
  execution = jobExplorer.getJobExecution(execution.getId());
  //check
  assertFalse(execution.getStatus().isUnsuccessful());
 }
 
}

.

Tuesday, June 25, 2013

Dynamically Switch SQL statements in Spring Batch

Dynamically Switch SQL statements in Spring Batch


here's a quick example of how to switch out sql statements in Spring Batch using step scope.

the job



 
  
   
    
   
  
 

 
  
  
  
   
  
 
 
 
  
  
 

 
  
  
 
 
 



the batch resource




 

 
  
 

 
  
 
 
 
 
 
 
 



and the database resource




 
  
  
 

 
  
 



the supporting itemwriter

package de.incompleteco.spring.batch.item;

public class SystemOutItemWriter {

 public void write(Object object) {
  System.out.println(object);
 }
 
}


the supporting rowmapper

package de.incompleteco.spring.batch.data;

import java.sql.ResultSet;
import java.sql.SQLException;

import org.springframework.jdbc.core.RowMapper;

public class ColumnRowMapper implements RowMapper {

 public String mapRow(ResultSet rs, int rowNum) throws SQLException {
  return rs.getString(1);
 }

}


the sql statements (junk data)

create table table_one (
 column_a varchar(50)
);

create table table_two (
 column_a varchar(50)
);

--table one
insert into table_one (column_a) values ('hello');
insert into table_one (column_a) values ('world');

--table two
insert into table_two (column_a) values ('hey');



and the unit test

package de.incompleteco.spring;

import static org.junit.Assert.assertFalse;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:/META-INF/spring/*-context.xml"})
public class SwitchSQLIntegrationTest {

 @Autowired
 private Job job;
 
 @Autowired
 private JobLauncher jobLauncher;
 
 @Autowired
 private JobExplorer jobExplorer;
 
 @Test
 public void test() throws Exception {
  //setup the parameters
  JobParameters parameters = new JobParametersBuilder().addLong("runtime",System.currentTimeMillis())
    .addString("sqlKey", "sql1").toJobParameters();
  //run
  JobExecution execution = jobLauncher.run(job,parameters);
  //test
  while (jobExplorer.getJobExecution(execution.getId()).isRunning()) {
   Thread.sleep(100);
  }//end while
  //load
  execution = jobExplorer.getJobExecution(execution.getId());
  //test
  assertFalse(execution.getStatus().isUnsuccessful());
  //run it again
  parameters = new JobParametersBuilder().addLong("runtime",System.currentTimeMillis())
    .addString("sqlKey", "sql2").toJobParameters();
  //run
  execution = jobLauncher.run(job,parameters);
  //test
  while (jobExplorer.getJobExecution(execution.getId()).isRunning()) {
   Thread.sleep(100);
  }//end while
  //load
  execution = jobExplorer.getJobExecution(execution.getId());
  //test
  assertFalse(execution.getStatus().isUnsuccessful());  
 }
 
}

Wednesday, June 5, 2013

A Simple Spring Batch Buffer Example

A Simple Spring Batch Buffer Example

here's a simple example of a Spring Batch app that has a built in buffer.  this was in response to a stackoverflow question about retrieving a recordset once from a stored procedure, then reading each line item out into a writer.  the original question stated that they couldn't really use the out of the box stored procedure one, so here's an example of the whole load-once-read-line-write of batch.

here's the reader

package de.incompleteco.spring.batch.item;

import java.util.PriorityQueue;
import java.util.Queue;
import java.util.UUID;

import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.NonTransientResourceException;
import org.springframework.batch.item.ParseException;
import org.springframework.batch.item.UnexpectedInputException;

import de.incompleteco.spring.batch.domain.Record;

public class SingleItemReader implements ItemReader {

 private Queue values = null;
 
 public Record read() throws Exception, UnexpectedInputException,ParseException, NonTransientResourceException {
  //check the queue
  if (values == null) {
   //load
   loadQueue();
  }//end if
  //return
  return getRecord(values.poll());
 }
 
 private Record getRecord(String value) {
  //init
  Record record = null;
  //check for null
  if (value != null) {
   record = new Record();
   record.setId(UUID.randomUUID());
   record.setValue(value);
  }//end if
  //return
  return record;  
 }
 
 private synchronized void loadQueue() {
  //generate a bunch of data and add to the queue
  if (values == null) {
   System.out.println("calling load of the queue");
   values = new PriorityQueue();
   for (int i=0;i<100 end="" for="" hello="" i="" if="" pre="" values.add="">
here's the writer
package de.incompleteco.spring.batch.item;

import java.util.List;

import org.springframework.batch.item.ItemWriter;

import de.incompleteco.spring.batch.domain.Record;

public class SystemItemWriter implements ItemWriter {

 public void write(List items) throws Exception {
  System.out.println("starting write...");
  System.out.println(items);
  System.out.println("...finished write");
 }

}
here's the batch config


 
  
   
    
   
  
 

 
 
 

 
  
 
  
  
 

 
  
 
 
 
 
 


and finally, the unit test
package de.incompleteco.spring.batch;

import static org.junit.Assert.assertFalse;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:/META-INF/spring/batch-context.xml"})
public class SingleReadJobIntegrationTest {

 @Autowired
 private Job job;
 
 @Autowired
 private JobLauncher jobLauncher;
 
 @Autowired
 private JobExplorer jobExplorer;
 
 @Test
 public void test() throws Exception {
  //setup the parameters
  JobParameters jobParameters = new JobParametersBuilder().addLong("runtime",System.currentTimeMillis()).toJobParameters();
  //start
  JobExecution execution = jobLauncher.run(job,jobParameters);
  //monitor
  while (jobExplorer.getJobExecution(execution.getId()).isRunning()) {
   Thread.sleep(100);
  }//end while
  //check
  execution = jobExplorer.getJobExecution(execution.getId());
  //validate
  assertFalse(execution.getStatus().isUnsuccessful());
 }
 
}

Wednesday, May 29, 2013

Spring Batch file processing

Spring Batch file processing

here's a simple little config to process a file;

first the job setup;



 
  
   
    
   
  
  
   
    
   
  
 

 
  
  
  
 
 
 
  
  
 
 
 
 
  
  
 
 
 
  
 
 
 
  
  
   
  
  
 



then the batch resources;



 

 
  
 
 
  
  
 
 
 
  
   
   
  
  
  
  
  
   
  
 



now, the entity we're going to load to;

package de.incompleteco.spring.batch.domain;

import java.io.Serializable;

public class SimpleEntity implements Serializable {

 private static final long serialVersionUID = 1L;

 private String col1;
 
 private String col2;
 
 private String col3;

 public String getCol1() {
  return col1;
 }

 public void setCol1(String col1) {
  this.col1 = col1;
 }

 public String getCol2() {
  return col2;
 }

 public void setCol2(String col2) {
  this.col2 = col2;
 }

 public String getCol3() {
  return col3;
 }

 public void setCol3(String col3) {
  this.col3 = col3;
 }
 
 
 
}


and here's an integration test to make it all work;

package de.incompleteco.spring.batch;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.FileOutputStream;

import javax.sql.DataSource;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:/META-INF/spring/*-context.xml"})
@ActiveProfiles("junit")
public class FileJobIntegrationTest {

 @Autowired
 private Job job;
 
 @Autowired
 private JobLauncher jobLauncher;
 
 @Autowired
 private JobExplorer jobExplorer;
 
 @Autowired
 private DataSource dataSource;
 
 private int recordCount = 1000000;
 
 private String fileName = System.getProperty("java.io.tmpdir") + File.separator + "test.csv";
 
 @Before
 public void before() throws Exception {
  if (new File(fileName).exists()) {
   new File(fileName).delete();
  }//end if
 }
 
 @Test
 public void test() throws Exception {
  //create a file
  FileOutputStream fos = new FileOutputStream(fileName);
  fos.write("col1,col2,col3".getBytes());
  fos.flush();
  for (int i=0;i<=recordCount;i++) {
   fos.write(new String(i + "," + (i+1) + "," + (i+2) + "\n").getBytes());
   fos.flush();//flush it
  }//end for
  fos.close();
  //lets get the size of the file
  long length = new File(fileName).length();
  System.out.println("file size: " + ((length / 1024) / 1024));
  //execute the job
  JobParameters jobParameters = new JobParametersBuilder().addString("fileName",fileName).toJobParameters();
  JobExecution execution = jobLauncher.run(job,jobParameters);
  //monitor
  while (jobExplorer.getJobExecution(execution.getId()).isRunning()) {
   Thread.sleep(1000);
  }//end while
  //load again
  execution = jobExplorer.getJobExecution(execution.getId());
  //test
  assertEquals(ExitStatus.COMPLETED.getExitCode(),execution.getExitStatus().getExitCode());
  //lets see what's in the database
  int count = new JdbcTemplate(dataSource).queryForObject("select count(*) from simple_entity", Integer.class);
  //test
  assertTrue(count == recordCount);
 }
 
}


now, testing locally, the file generated by the test is ~ 19MB and the heap goes up, but does not exhaust at 1,000,000 records.  it's a neat little example of processing a big file, quickly and cleanly into a database.

Friday, May 10, 2013

Spring Batch StringToJobLaunchRequestAdapter

Spring Batch StringToJobLaunchRequestAdapter


so i don't forget this, the proper string format for Spring Batch StringToJobLaunchRequestAdapter is

foo[bar=spam,count(long)=123]

(am always getting it wrong...)

Wednesday, April 24, 2013

Spring Batch - Parallel Item Writer

Spring Batch - Parallel Item Writer

here's a quick solution i put together for stackoverflow and writing to many itemwriters in parallel

here's the class

package de.incompleteco.spring.batch.item.support;

import java.util.List;

import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.support.CompositeItemWriter;
import org.springframework.core.task.TaskExecutor;
import org.springframework.util.Assert;

import de.incompleteco.spring.domain.SimpleEntity;

public class ParallelCompositeItemWriter extends CompositeItemWriter< SimpleEntity > {

 private List< ItemWriter< ? super SimpleEntity > > delegates;
 
 private TaskExecutor taskExecutor;
 
 @Override
 public void write(final List< ? extends SimpleEntity > item) throws Exception {
  for (final ItemWriter< ? super SimpleEntity > writer : delegates) {
   taskExecutor.execute(new Runnable()  {
    @Override
    public void run() {
     try {
      writer.write(item);
     } catch (Throwable t) {
      rethrow(t);
     } 
    }
    
    private void rethrow(Throwable t) {
     if (t instanceof RuntimeException) {
      throw (RuntimeException) t;
     }
     else if (t instanceof Error) {
      throw (Error) t;
     }
     throw new IllegalStateException(t);
    }  
   });
  }//end for
 }


 public void setTaskExecutor(TaskExecutor taskExecutor) {
  this.taskExecutor = taskExecutor;
 }
 
 @Override
 public void setDelegates(List < ItemWriter < ? super SimpleEntity > > delegates) {
  this.delegates = delegates;
  super.setDelegates(delegates);
 }

 @Override
 public void afterPropertiesSet() throws Exception {
  super.afterPropertiesSet();
  Assert.notNull(taskExecutor,"Task executor needs to be set");
 }
 
}


and here's a sample configuration to run it

 
  
   
    
   
  
 

 
  
 
 
 
  
  
 
 
 
  
   
   
   
    
   
  
  
   
   
   
    
   
    
 
 
 
  
   
  
  
   
    
  
   
    
 
 
 
 

 
     
     
     
     
     
         
             jdbc:h2:mem:a;DB_CLOSE_DELAY=-1
         
     
  
 
 
     
     
     
     
     
         
             jdbc:h2:mem:b;DB_CLOSE_DELAY=-1
         
     
   
 
 
  
 
 
 
  
 
 

 
 
 

 
     
     
 

there's a couple of things to note;
- Bitronix JTA is used to support XA transactions across multiple databases
- the jdbc stuff is pretty rubbish and needs to be done better

Spring Integration - Losing the ReplyChannel when sending over JMS

Spring Integration - Losing the ReplyChannel when sending over JMS

a scenario you may have encountered is the need to "asynchronously" get a request/response in Spring Integration via JMS.  that is, you want to send multiple, non-blocking JMS messages out, then pair them up on the way back in.

there are a couple of options to do this;
1. use a dispatcher (task executor) on your outbound channel to a JMS outbound-gateway - this will use a thread from the pool to process and wait for the reply.
2. (send) in conjunction with a message-drive-channel-adapter (receive) - this will use a thread pool on the receive.

to support the 2nd option, you can set the replyChannel to the message allowing you to 'route responses'.  an example of this is in org.springframework.batch.integration.partition.MessageChannelPartitionHandler from the Spring Batch Integration project.  the rub is, the replyChannel gets 'lost' when sending over JMS; it doesn't get translated to the wire.  

now nor should it.  the consumer of the message on the other end may be Spring Integration driven too and have it's own replyChannel settings.  so, one solution may be to use an extended HeaderMapper.  

the intent is to map any dynamic channels into the applicationContext (beanFactory as a singleton).  then, set that value into a new JMS-compatible format (String).  this header field will be 'left alone' by the consumer and returned.  on it's return, the replyChannel object is looked up and set to the replyChannel header value, allowing Spring Integration to appropriately route.

here's an example of the header

package de.incompleteco.spring.integration.jms.support;

import java.util.Map;

import javax.jms.JMSException;
import javax.jms.Message;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.integration.MessageHeaders;
import org.springframework.integration.jms.DefaultJmsHeaderMapper;

public class JMSReplyChannelJmsHeaderMapper extends DefaultJmsHeaderMapper implements ApplicationContextAware {

 private final Log logger = LogFactory.getLog(this.getClass());
 
 public static final String SENDER_REPLY_CHANNEL_NAME = "senderReplyChannelName";
 
 private ApplicationContext applicationContext;
 
 @Override
 public void fromHeaders(MessageHeaders headers, Message jmsMessage) {
  //process the parent normally
  super.fromHeaders(headers, jmsMessage);
  //now check if the headers contains a replyChannel
  if (headers.getReplyChannel() != null) {
   //now process it
   Object replyChannel = headers.getReplyChannel();
   String replyChannelName = null;
   //check
   if (replyChannel instanceof String) {
    //it's the name
    replyChannelName = replyChannel.toString();
   } else {
    //test if the object exists in the app context already
    if (!applicationContext.containsBean(replyChannel.toString())) {
     //generate a name --> use the id
     replyChannelName = headers.getId().toString();
     //set into the app context
     ((ConfigurableApplicationContext)applicationContext).getBeanFactory().registerSingleton(replyChannelName, replyChannel);     
    } else {
     replyChannelName = replyChannel.toString();
    }//end if
   }//end if
   //now add a message header
   try {
    jmsMessage.setStringProperty(SENDER_REPLY_CHANNEL_NAME, replyChannelName);
   } catch (JMSException e) {
    logger.info("failed to set senderReplyChannelName, skipping", e);
   }
  }//end if
 }

 @Override
 public Map toHeaders(Message jmsMessage) {
  Map headers = super.toHeaders(jmsMessage);
  //check if there's a SENDER_REPLY_CHANNEL_NAME
  try {
   if (jmsMessage.getStringProperty(SENDER_REPLY_CHANNEL_NAME) != null) {
    //check if the name maps to the app context
    String replyChannelName = jmsMessage.getStringProperty(SENDER_REPLY_CHANNEL_NAME);
    if (applicationContext.containsBean(replyChannelName)) {
     //got it
     logger.info("setting: " + replyChannelName + "as reply channel");
     headers.put(MessageHeaders.REPLY_CHANNEL,applicationContext.getBean(replyChannelName));
    }//end if
   }
  } catch (JMSException e) {
   logger.info("failed to set process the senderReplyChannelName, skipping", e);
  }
  //return
  return headers;
 }

 @Override
 public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
  this.applicationContext = applicationContext;
 }

}


and here's a rather long configuration using the MessageChannelPartitionHandler



 
 
 
 
 
 
 

 
  
  
  
   
 
 
 
  
  
  
  
 
 
 
  
 

 
  
   
  
 

 
  
   
    
     
      
     
    
    
   
  
 

 

 
  
  
  
  

 
  
 
 
 

 
 
 
 
  
 
 
 
 
 

 

 
 
 

 

 

 

 
  
  
 
 
 

 
 
 
 
 
  
 
 
 
  
 
 
 
  
  
 
 
 



Friday, April 5, 2013

Spring Batch Tip - Convert JobParameters to Map

Spring Batch Tip - Convert JobParameters to Map



if you want to 'flatten' the JobParameters into a simple Map (say, for logging purposes) there's always apache commons collections.

here's the code

    Map convertedParameters = MapUtils.transformedMap(jobParameters.getParameters(),TransformerUtils.nopTransformer(),TransformerUtils.stringValueTransformer());

and here's the pom reference

   
    commons-collections
    commons-collections
    3.2.1
   

Friday, March 29, 2013

Spring Batch - Running only one Job Instance at a time

Spring Batch - Running only one Job Instance at a time

an occasional use case in Spring Batch is the need to have only one instance of a Job running at any given time.  this might be due to restrictions on source data or some other competing aspect.  an extension of this use case is that you don't want to lose the incoming 'job start', you want to 'queue it up' for next time.

here's a simple solution composed in Spring Integration that supports the above ideal and uses the new Retry components from Spring Integration 2.2.  It's not the only way of putting a solution together for it, but it allows the separation of Job (Spring Batch) from Job Orchestration (not Spring Batch)

first - the class that's going to verify if a job is running or not (and throw and exception if it is)

package de.incompleteco.spring.batch.service;

public interface JobStatus {

 /**
  * verifies if the job name is running anywhere in the job explorer
  * @param jobName
  * @throws Exception
  */
 public void verifyNotRunning(String jobName) throws Exception;
 
}



and the implementation

package de.incompleteco.spring.batch.service;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.explore.JobExplorer;

public class SimpleJobStatus implements JobStatus {

 private List jobNames;
 
 private JobExplorer jobExplorer;

 public SimpleJobStatus() {
  jobNames = new ArrayList();
 }
 
 @Override
 public void verifyNotRunning(String jobName) throws Exception {
  //check if it's a known job
  if (jobNames.contains(jobName)) {
   //check if it's running
   Set executions = jobExplorer.findRunningJobExecutions(jobName);
   if (executions != null && !executions.isEmpty()) {
    //it's running somewhere
    throw new Exception("the job " + jobName + " is curretly running");
   }//end if
  }//end if
 }

 public void setJobNames(List jobNames) {
  this.jobNames = jobNames;
 }

 public void setJobExplorer(JobExplorer jobExplorer) {
  this.jobExplorer = jobExplorer;
 }

}


here's the Spring Integration using the retry to handle the exception and try again



 
 
 
  
   
    
     
      
       
        
         
        
       
       
        
       
      
     
    
   
  
  
 
 
 

 
  
 




so, now there are two things to note;
- the 'queue' (retry loop) is JVM bound - if the JVM dies, the 'queue' does too
- the 'verify' is based on the Job Explorer - it the Job Explorer is database, then it will go across JVM, if not, it's only for this one.

Thursday, March 28, 2013

Spring Integration - Intercepting to Raise Events

Spring Integration - Intercepting to Raise Events

in Spring Integration, it is possible to use it to raise events to external systems and leverage all the existing components.  to do this, we can leverage the publishing-interceptor, a component in Spring Integration that uses AOP to capture a method and send off information about it.

the neat thing about this is that you can add a Message Driven Architecture layer to your existing application, allowing secondary or tertiary events to be raised from existing Spring Applications (such as audit).  this can be done either with annotations (@Publisher) or through xml

here's the link to the documentation for it

http://static.springsource.org/spring-integration/reference/html/message-publishing.html