5

I have a spring batch job that I'd like to do the following...

Step 1 - 
   Tasklet - Create a list of dates, store the list of dates in the job execution context.

Step 2 - 
   JDBC Item Reader - Get list of dates from job execution context.
                      Get element(0) in dates list. Use is as input for jdbc query. 
                      Store element(0) date is job execution context 
                      Remove element(0) date from list of dates
                      Store element(0) date in job execution context                 
   Flat File Item Writer - Get element(0) date from job execution context and use for file name.

Then using a job listener repeat step 2 until no remaining dates in the list of dates.

I've created the job and it works okay for the first execution of step 2. But step 2 is not repeating as I want it to. I know this because when I debug through my code it only breaks for the initial run of step 2.

It does however continue to give me messages like below as if it is running step 2 even when I know it is not.

2016-08-10 22:20:57.842  INFO 11784 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Duplicate step [readStgDbAndExportMasterListStep] detected in execution of job=[exportMasterListCsv]. If either step fails, both will be executed again on restart.
2016-08-10 22:20:57.846  INFO 11784 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [readStgDbAndExportMasterListStep]

This ends up in a never ending loop.

Could someone help me figure out or give a suggestion as to why my stpe 2 is only running once?

thanks in advance

I've added two links to PasteBin for my code so as not to pollute this post.

http://pastebin.com/QhExNikm (Job Config)

http://pastebin.com/sscKKWRk (Common Job Config)

http://pastebin.com/Nn74zTpS (Step execution listener)

Richie
  • 4,989
  • 24
  • 90
  • 177

2 Answers2

2

From your question and your code I deduct that based on the amount of dates that you retrieve (this happens before the actual job starts), you will execute a step for the amount of times you have dates.

I suggest a design change. Create a java class that will get you the dates as a list and based on that list you will dynamically create your steps. Something like this:

@EnableBatchProcessing
public class JobConfig {

    @Autowired
    private JobBuilderFactory jobBuilderFactory;

    @Autowired
    private StepBuilderFactory stepBuilderFactory;  

    @Autowired
    private JobDatesCreator jobDatesCreator;

    @Bean
    public Job executeMyJob() {
        List<Step> steps = new ArrayList<Step>();
        for (String date : jobDatesCreator.getDates()) {
            steps.add(createStep(date));
        }

        return jobBuilderFactory.get("executeMyJob")
                .start(createParallelFlow(steps))
                .end()
                .build();       
    }

    private Step createStep(String date){
        return stepBuilderFactory.get("readStgDbAndExportMasterListStep" + date)
                .chunk(your_chunksize)
                .reader(your_reader)
                .processor(your_processor)
                .writer(your_writer)                                
                .build();       
    }   

    private Flow createParallelFlow(List<Step> steps) {
        SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
        // max multithreading = -1, no multithreading = 1, smart size = steps.size()
        taskExecutor.setConcurrencyLimit(1); 

        List<Flow> flows = steps.stream()
                .map(step -> new FlowBuilder<Flow>("flow_" + step.getName()).start(step).build())
                .collect(Collectors.toList());

        return new FlowBuilder<SimpleFlow>("parallelStepsFlow")
                .split(taskExecutor)
                .add(flows.toArray(new Flow[flows.size()]))
                .build();
    }  
}

EDIT: added "jobParameter" input (slightly different approach also)

Somewhere on your classpath add the following example .properties file:

sql.statement="select * from awesome"

and add the following annotation to your JobDatesCreator class

@PropertySource("classpath:example.properties")

You can provide specific sql statements as a command line argument as well. From the spring documentation:

you can launch with a specific command line switch (e.g. java -jar app.jar --name="Spring").

For more info on that see http://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html

The class that gets your dates (why use a tasklet for this?):

@PropertySource("classpath:example.properties")
public class JobDatesCreator {

    @Value("${sql.statement}")
    private String sqlStatement;

    @Autowired
    private CommonExportFromStagingDbJobConfig commonJobConfig; 

    private List<String> dates; 

    @PostConstruct
    private void init(){
        // Execute your logic here for getting the data you need.
        JdbcTemplate jdbcTemplate = new JdbcTemplate(commonJobConfig.onlineStagingDb);
        // acces to your sql statement provided in a property file or as a command line argument
        System.out.println("This is the sql statement I provided in my external property: " + sqlStatement);

        // for now..
        dates = new ArrayList<>();
        dates.add("date 1");
        dates.add("date 2");
    }

    public List<String> getDates() {
        return dates;
    }

    public void setDates(List<String> dates) {
        this.dates = dates;
    }
}

I also noticed that you have alot of duplicate code that you can quite easily refactor. Now for each writer you have something like this:

@Bean
public FlatFileItemWriter<MasterList> division10MasterListFileWriter() {
    FlatFileItemWriter<MasterList> writer = new FlatFileItemWriter<>();
    writer.setResource(new FileSystemResource(new File(outDir, MerchHierarchyConstants.DIVISION_NO_10 )));
    writer.setHeaderCallback(masterListFlatFileHeaderCallback());
    writer.setLineAggregator(masterListFormatterLineAggregator());
    return writer;
}

Consider using something like this instead:

public FlatFileItemWriter<MasterList> divisionMasterListFileWriter(String divisionNumber) {
    FlatFileItemWriter<MasterList> writer = new FlatFileItemWriter<>();
    writer.setResource(new FileSystemResource(new File(outDir, divisionNumber )));
    writer.setHeaderCallback(masterListFlatFileHeaderCallback());
    writer.setLineAggregator(masterListFormatterLineAggregator());
    return writer;
}

As not all code is available to correctly replicate your issue, this answer is a suggestion/indication to solve your problem.

Sander_M
  • 1,109
  • 2
  • 18
  • 36
  • Thanks for your advice. I'm going to take it. I want JobDatesCreator to use a jobParameter as input for the jdbc query. I tried adding component and then jobscope to JobDates creater but that doesn't seem to work. How would I do that? – Richie Aug 11 '16 at 12:32
  • See my edit in the code. If that approach doesnt work, please show your new configuration and how your wired it as right now I dont know what you are trying to do and why it is not working :) – Sander_M Aug 11 '16 at 13:40
  • Hi @Sander_M . I've spent a long while trying to make this work for me. I'm not sure it's possible. You see in JobDatesCreator I need to access a jobParameter rather than a property. i.e. Value("{jobParameters['" + JobParamConstants.PARAM_TO_DATE + "']}") Date jobToDate and I need to use that in my db query. From what I understand so far I don't think there is a way to make JobDatesCreator JobScope so that it can access a job parameter because that would mean that executeMyJob inside JobConfig would also have to have JobScope and I don't think that's right. I'll keep working on this. – Richie Aug 15 '16 at 02:45
  • I've posted a new question http://stackoverflow.com/questions/38949030/spring-batch-execute-dynamically-generated-steps-in-a-tasklet because I think it's worth a new thread. – Richie Aug 15 '16 at 03:43
  • 1
    Great work on your continued effort, it is not always as easy as you want it to be and I know how how frustrating it can be at times. My answer was heavily inspired by @HansjoergWingeier code, hence the same approach in his answer (or should I say, hence the same approachas as his approach in my answer ;) ). I am afraid I can't help you with the jobParameters part. If you were to create a new question for that, try to really minimise it to the bear minimum of the problem and make it easy to replicate the problem. This will take some effort, but the chance of a good answer is much higher. – Sander_M Aug 15 '16 at 08:07
  • Thanks for the help guys. One more question. Another alternative way to try and reach my solution... http://stackoverflow.com/questions/38952058/new-output-file-for-each-item-paswsed-into-flatfileitemwriter – Richie Aug 15 '16 at 08:52
  • I'd also like to make the point that the code that was suggested that can be refactored I don't think is right. I think you need to specify the files as beans because of the need to do a .stream on them when setting up the job. Please let me know if I'm wrong. – Richie Aug 15 '16 at 23:39
1

Based on our discussion on Spring batch execute dynamically generated steps in a tasklet I'm trying to answer the questions on how to access jobParameter before the job is actually being executed.

I assume that there is restcall which will execute the batch. In general, this will require the following steps to be taken. 1. a piece of code that receives the rest call with its parameters 2. creation of a new springcontext (there are ways to reuse an existing context and launch the job again but there are some issues when it comes to reuse of steps, readers and writers) 3. launch the job

The simplest solution would be to store the jobparameter received from the service as an system-property and then access this property when you build up the job in step 3. But this could lead to a problem if more than one user starts the job at the same moment.

There are other ways to pass parameters into the springcontext, when it is loaded. But that depends on the way you setup your context. For instance, if you are using SpringBoot directly for step 2, you could write a method like:

private int startJob(Properties jobParamsAsProps) {
  SpringApplication springApp = new SpringApplication(.. my config classes ..);
  springApp.setDefaultProperties(jobParamsAsProps);

  ConfigurableApplicationContext context = springApp.run();
  ExitCodeGenerator exitCodeGen = context.getBean(ExitCodeGenerator.class);
  int code = exitCodeGen.getExitCode();
  context.close();
  return cod;
}

This way, you could access the properties as normal with standard Value- or ConfigurationProperties Annotations.

Community
  • 1
  • 1
Hansjoerg Wingeier
  • 4,274
  • 4
  • 17
  • 25