2

So I've created a sample batch run to learn how to make a job repeat itself until a condition is met. For the most part, it's working as intended, but I'm getting the following log entry when I execute the program

org.springframework.batch.core.job.SimpleStepHandler: Duplicate step [getStartingStep] detected in execution of job=[infinateLoopJob]. If either step fails, both will be executed again on restart.

While I've been told by other developers it's not that big of a deal, Id rather do it right and not have anything weird happen down the road, when I want to rely on this knowledge for production code. I tried to append a timestamp generation to the name of the step, but no luck (it only appended it once upon initial creation of the Step).

So in a nutshell, how can I have it so that each step isn't using the same name over and over, and still perform the action of looping through the entire job?

The code currently being used:

AppStart.java

@SpringBootApplication(scanBasePackages="com.local.testJobLoop")
public class AppStart {

    private static final Logger logger = LoggerFactory.getLogger(com.local.testJobLoop.AppStart.class);

    AppStart() {
        super();
    }

    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(AppStart.class);
        application.setWebEnvironment(false);
        ApplicationContext context = application.run(args);

        JobLauncher jobLauncher = context.getBean(JobLauncher.class);
        Job infinateLoopJob = context.getBean("infinateLoopJob", Job.class);

        try {
            JobParameters jobParameters = new JobParametersBuilder()
                    .addLong("timestamp", System.currentTimeMillis())
                    .toJobParameters();

            JobExecution execution = jobLauncher.run(infinateLoopJob, jobParameters);
            BatchStatus status = execution.getStatus();

            logger.debug("Exit Status : {}", status);
            if (!BatchStatus.COMPLETED.equals(status)) {
                List<Throwable> exceptions = execution.getAllFailureExceptions();
                for (Throwable throwable : exceptions) {
                    logger.error("Batch Failure Exceptions:", throwable);
                }
            }
        } catch (JobExecutionAlreadyRunningException e) {
            logger.error("Job execution already running:", e);
        } catch (JobRestartException e) {
            logger.error("Illegal attempt to restart a job:", e);
        } catch (JobInstanceAlreadyCompleteException e) {
            logger.error("Illegal attempt to restart a job that was already completed successfully", e);
        } catch (JobParametersInvalidException e) {
            logger.error("Invalid job parameter:", e);
        }
        logger.debug("Done");
    }
}

AppJobConfig.java

@Configuration
@EnableBatchProcessing
public class AppJobConfig {

    @Autowired
    private JobBuilderFactory jobBuilderFactory;

    @Bean
    public Job infinateLoopJob(@Qualifier("getStartingStep") final Step startingStep,
                               @Qualifier("getSecondStep") final Step secondStep) {
        return jobBuilderFactory.get("infinateLoopJob")
                .start(startingStep).on(Constants.STEP_EXIT_STATUS_CONTINUE).to(secondStep)
                .from(startingStep).on(Constants.STEP_EXIT_STATUS_COMPLETED).end()
                .from(secondStep).next(startingStep).build()
                .build();

    }
}

AppStepConfig.java

@Configuration
@EnableBatchProcessing
public class AppStepConfig {

    @Autowired
    private StepBuilderFactory stepBuilderFactory;

    private static final Logger logger = LoggerFactory.getLogger(AppStepConfig.class);

    @Bean
    public Step getStartingStep(@Qualifier("startingActionTasklet") final StartingActionTasklet tasklet,
                                @Qualifier("startingActionListener") final StartingActionListener listener) {
        return stepBuilderFactory.get("getStartingStep")
                                 .tasklet(tasklet)
                                 .listener(listener)
                                 .build();
    }

    @Bean
    public Step getSecondStep(@Qualifier("secondActionTasklet") final SecondActionTasklet tasklet,
                              @Qualifier("secondActionListener") final SecondActionListener listener) {
        return stepBuilderFactory.get("getSecondStep")
                                 .tasklet(tasklet)
                                 .listener(listener)
                                 .build();
    }

StartingActionTasklet.java

@Component
public class StartingActionTasklet implements Tasklet, InitializingBean {

    private static final Logger LOGGER = LoggerFactory.getLogger(StartingActionTasklet.class);

    public StartingActionTasklet() { super(); }

    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {

        int number = (int) chunkContext.getStepContext()
                                       .getStepExecution()
                                       .getJobExecution()
                                       .getExecutionContext()
                                       .get("incrementNumber");

        LOGGER.info("STARTING ACTION: Number is {number}");

        return RepeatStatus.FINISHED;
    }

    public void afterPropertiesSet() throws Exception { /* do nothing */ }
}

SecondActionTasklet.java

@Component
public class SecondActionTasklet implements Tasklet, InitializingBean {

    private static final Logger LOGGER = LoggerFactory.getLogger(SecondActionTasklet.class);

    public SecondActionTasklet() { super(); }

    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {

        int number = (int) chunkContext.getStepContext()
                .getStepExecution()
                .getJobExecution()
                .getExecutionContext()
                .get("incrementNumber");

        LOGGER.info("SECOND ACTION: Number is " + number);

        number++;

        LOGGER.info("SECOND ACTION: Number is now " + number);

        chunkContext.getStepContext()
                .getStepExecution()
                .getJobExecution()
                .getExecutionContext()
                .put("incrementNumber", number);

        return RepeatStatus.FINISHED;

    }

    public void afterPropertiesSet() throws Exception {
        //do nothing
    }
}

StartingActionListener.java

@Component
public class StartingActionListener implements StepExecutionListener {

    private static final Logger LOGGER = LoggerFactory.getLogger(StartingActionListener.class);

    public StartingActionListener() {
        super();
    }

    public void beforeStep(StepExecution stepExecution) {
        LOGGER.debug("StartingActionListener - beforeStep");

        // Get incrementNumber from job execution context
        JobExecution jobExecution = stepExecution.getJobExecution();
        ExecutionContext jobContext = jobExecution.getExecutionContext();
        Integer incrementNumber = (Integer) jobContext.get("incrementNumber");

        if (incrementNumber == null) {
            jobContext.put("incrementNumber", 0);
        }
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        LOGGER.debug("StartingActionListener - afterStep");

        // Get incrementNumber from job execution context
        JobExecution jobExecution = stepExecution.getJobExecution();
        ExecutionContext jobContext = jobExecution.getExecutionContext();
        Integer incrementNumber = (Integer) jobContext.get("incrementNumber");

        // Continue job execution if have more feed, stop otherwise
        if (incrementNumber == null || incrementNumber > 10) {
            return new ExitStatus(Constants.STEP_EXIT_STATUS_COMPLETED);
        } else {
            return new ExitStatus(Constants.STEP_EXIT_STATUS_CONTINUE);
        }
    }
}

SecondActionListener.java

@Component
public class SecondActionListener implements StepExecutionListener {

    private static final Logger LOGGER = LoggerFactory.getLogger(SecondActionListener.class);

    public void beforeStep(StepExecution stepExecution) {

    }

    public ExitStatus afterStep(StepExecution stepExecution) {
        LOGGER.debug("SecondActionListener - afterStep");

        return null;
    }
}
Kohei TAMURA
  • 4,970
  • 7
  • 25
  • 49
canadiancreed
  • 1,966
  • 6
  • 41
  • 58

1 Answers1

1

Do the business rules dictate that starting step and second step need to be separate and distinct? If not, the easiest thing would be to combine them into a single step and leverage the Repeat functionality.

Effectively, your combined Tasklet would just return RepeatStatus.CONTINUABLE until all the work has been finished, at which point it would return RepeatStatus.FINISHED.

UPDATE: So what you could do, though it would be a pretty gross, is to add a "decisionStepThree" to your job which adds new steps to the job.

public class DecisionStepThreeTasklet implements Tasklet {

    @Autowired
    private SimpleJob job;

    @Autowired
    private Step startingStep;

    @Autowired
    private Step secondStep;

    public RepeatStatus execute(final StepContribution contribution, final ChunkContext chunkContext) {
        Collection<StepExecution> stepExecutions = chunkContext.getStepContext().
                getStepExecution().getJobExecution().getStepExecutions();

        int stepCount = stepExecutions.size();
        StepExecution lastStepExecution = getLastStep(stepExecutions);

        if (Constants.STEP_EXIT_STATUS_CONTINUE.equals(lastStepExecution.getExitStatus().getExitCode())) {
            job.addStep(copyStep(startingStep, "startingStep" + ++stepCount));
            job.addStep(copyStep(secondStep, "secondStep" + ++stepCount));
        }
        return RepeatStatus.FINISHED;
    }

    public StepExecution getLastStep(final Collection<StepExecution> c) {
        Iterator<StepExecution> itr = c.iterator();
        StepExecution lastElement = itr.next();
        while(itr.hasNext()) {
            lastElement=itr.next();
        }
        return lastElement;
    }

    private Step copyStep(final Step parent, final String name) {
        return new Step() {
            public String getName() {
                return name;
            }
            public boolean isAllowStartIfComplete() {
                return parent.isAllowStartIfComplete();
            }
            public int getStartLimit() {
                return parent.getStartLimit();
            }
            public void execute(final StepExecution stepExecution) throws JobInterruptedException {
                parent.execute(stepExecution);
            }
        };
    }    
}
Dean Clark
  • 3,770
  • 1
  • 11
  • 26
  • @Dean_Clark in this instance yes the business rules state that these would need to be separate/distinct. I understand in this simple example Repeat would be a better choice, but this is more a proof of concept for once I've learned how it's done I'd use for more complex configurations. – canadiancreed May 10 '17 at 17:33
  • @canadiancreed added more detail in the answer above. not super pretty, but it might work for you – Dean Clark May 12 '17 at 14:50