0
votes

I have a Spring Batch (3.0.6) / Spring Batch Admin (2.0 snapshot) application that I'm attempting to schedule jobs with using Quartz (2.2.2). Using batch admin config for hsqldb (we don't care about this data at all) we're getting a multiple transaction change error when running the Quartz jobs (though the Spring Batch jobs are kicked off and completing fine).

I receive the following error:

    [2016-05-02 16:28:16.005] boot - 48230  INFO [scheduler_Worker-2] --- MyJobBean: Launching myBatchJob
[2016-05-02 16:28:16.014] boot - 48230  WARN [scheduler_Worker-1] --- MyBatchJob: Job failed with Exception PreparedStatementCallback; SQL [INSERT into BATCH_JOB_INSTANCE(JOB_INSTANCE_ID, JOB_NAME, JOB_KEY, VERSION) values (?, ?, ?, ?)]; transaction rollback: serialization failure; nested exception is java.sql.SQLTransactionRollbackException: transaction rollback: serialization failure
java.sql.SQLTransactionRollbackException: transaction rollback: serialization failure
at org.hsqldb.jdbc.JDBCUtil.sqlException(Unknown Source)
    at org.hsqldb.jdbc.JDBCUtil.sqlException(Unknown Source)
    at org.hsqldb.jdbc.JDBCPreparedStatement.fetchResult(Unknown Source)
    at org.hsqldb.jdbc.JDBCPreparedStatement.executeUpdate(Unknown Source)
    at org.apache.commons.dbcp.DelegatingPreparedStatement.executeUpdate(DelegatingPreparedStatement.java:105)
    at org.apache.commons.dbcp.DelegatingPreparedStatement.executeUpdate(DelegatingPreparedStatement.java:105)
    at org.springframework.jdbc.core.JdbcTemplate$2.doInPreparedStatement(JdbcTemplate.java:873)
    at org.springframework.jdbc.core.JdbcTemplate$2.doInPreparedStatement(JdbcTemplate.java:866)
    at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:629)
    at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:866)
    at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:927)
    at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:932)
    at org.springframework.batch.core.repository.dao.JdbcJobInstanceDao.createJobInstance(JdbcJobInstanceDao.java:115)
    at org.springframework.batch.core.repository.support.SimpleJobRepository.createJobExecution(SimpleJobRepository.java:135)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:302)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:281)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.batch.core.repository.support.AbstractJobRepositoryFactoryBean$1.invoke(AbstractJobRepositoryFactoryBean.java:172)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:208)
    at com.sun.proxy.$Proxy61.createJobExecution(Unknown Source)
    at org.springframework.batch.core.launch.support.SimpleJobLauncher.run(SimpleJobLauncher.java:125)
    at org.springframework.batch.core.launch.JobLauncher$run.call(Unknown Source)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:45)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:110)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:130)
    at com.app.MyBatchJob.performJob(MyBatchJob.groovy:41)
    at com.app.MyBatchJob$performJob.call(Unknown Source)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:45)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:110)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:114)
    at com.app.MyJobBean.executeInternal(MyJobBean.groovy:36)
    at org.springframework.scheduling.quartz.QuartzJobBean.execute(QuartzJobBean.java:75)
    at org.quartz.core.JobRunShell.run(JobRunShell.java:202)
    at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573)
Caused by: org.hsqldb.HsqlException: transaction rollback: serialization failure
at org.hsqldb.error.Error.error(Unknown Source)
    at org.hsqldb.Session.executeCompiledStatement(Unknown Source)
    at org.hsqldb.Session.execute(Unknown Source)
    ... 41 more
Caused by: org.hsqldb.HsqlException: transaction rollback: row change by multiple transactions
at org.hsqldb.error.Error.error(Unknown Source)
    at org.hsqldb.TransactionManagerMVCC.addInsertAction(Unknown Source)
    at org.hsqldb.Session.addInsertAction(Unknown Source)
    at org.hsqldb.Table.insertSingleRow(Unknown Source)
    at org.hsqldb.StatementDML.insertSingleRow(Unknown Source)
    at org.hsqldb.StatementInsert.getResult(Unknown Source)
    at org.hsqldb.StatementDMQL.execute(Unknown Source)
    ... 43 more
Caused by: org.hsqldb.HsqlException: integrity constraint violation: unique constraint or index violation; JOB_INST_UN table: BATCH_JOB_INSTANCE
at org.hsqldb.error.Error.error(Unknown Source)
    at org.hsqldb.Constraint.getException(Unknown Source)
    at org.hsqldb.index.IndexAVLMemory.insert(Unknown Source)
    at org.hsqldb.persist.RowStoreAVL.indexRow(Unknown Source)
    ... 49 more

I've boiled it down to this simple config (read a list of items, convert to upper case and print them) and I still get this error. The job configuration in groovy...

@Bean
public ItemProcessor<Object, Object> processor() {
    return new ItemProcessor<Object, Object>() {
        @Override
        Object process(Object item) throws Exception {
            return item.toString().toUpperCase()
        }
    }
}

@Bean
public ItemReader<Object> reader() {
    return new ListItemReader<Object>(['John', 'Jill', 'James', 'Jenny'])
}

@Bean
public ItemWriter<Object> writers() {
    return new ItemWriter<Object>() {
        @Override
        void write(List<?> items) throws Exception {
            items.each {
                println("List item : $it")
            }
        }
    }
}

@Bean
public Job myJob() {
    return jobBuilderFactory.get("myJob")
            .incrementer(new RunIdIncrementer())
            .flow(myStep1())
            .end()
            .build()
}

@Bean
public Step myStep1() {
    return stepBuilderFactory.get("myStep1")
            .<Object, Object> chunk(10)
            .reader(reader())
            .processor(processor())
            .writer(writers())
            .build()
}

Here's the quartz bean config (cron schedule for every 15 minutes):

@Bean
public SchedulerFactoryBean scheduler() {
    SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean()
    schedulerFactoryBean.setTriggers(myTrigger().getObject())
    schedulerFactoryBean.start()
    return schedulerFactoryBean
}

@Bean
public CronTriggerFactoryBean myTrigger() {

    CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean()
    cronTriggerFactoryBean.setCronExpression("0 0/15 * * * ?")
    cronTriggerFactoryBean.setJobDetail(myJobFactory().getObject())
    return cronTriggerFactoryBean
}

@Bean
public JobDetailFactoryBean myJobFactory() {
    JobDetailFactoryBean factory = new JobDetailFactoryBean(jobClass: MyJobBean,
            name: MyJobBean.JOB_NAME)
    return factory
}

The QuartzJobBean :

@Log4j
@DisallowConcurrentExecution
class MyJobBean extends QuartzJobBean {

    public static final JOB_NAME = "myBatchJob"

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        log.info("Launching $JOB_NAME")

        ApplicationContext applicationContext = ApplicationContextUtil.getAppContext()
        MyBatchJob myBatchJob = applicationContext.getBean(JOB_NAME, MyBatchJob.class)

        try {
            myBatchJob.performJob()
        } catch (Exception e) {
            log.warn("Quartz job failed with cause $e.message", e)
        }
    }
}

The class that launches a batch job via the JobLauncher and JobLocator of spring batch:

@Log4j
@Component
class MyBatchJob {

    @Autowired
    private JobLocator jobLocator

    @Autowired
    private JobLauncher jobLauncher

    public void performJob() {

        try {
            JobParameter timestampParam = new JobParameter(System.currentTimeMillis())

            def paramMap = [:]
            paramMap.put("timestamp", timestampParam)

            JobExecution jobResult = jobLauncher.run(jobLocator.getJob("myJob"), new JobParameters(paramMap))
            log.info("Job launched with result $jobResult")

        } catch (Exception e) {
            log.warn("Job failed with Exception ${e.getMessage()}", e.getCause())
        }


    }

}    

Relevant properties for spring batch admin:

batch.job.configuration.package=com.app.jobs
batch.business.schema.script=classpath:business-schema-hsqldb.sql

ENVIRONMENT=hsqldb

The quartz job fails with an Exception but the Spring Batch job runs and completes just fine. I'm at about my wits end as to fixing the Quartz job. Anyone see anything I'm doing wrong with my configuration ?

UPDATE: When I remove Spring Batch Admin, this error goes away. If I switch from @EnableBatchAdmin back to @EnableBatchProcessing the exception does not get thrown. Now, I'm trying to figure out what between the default config of batch vs batch admin is causing this problem.

2

2 Answers

0
votes

Seems to me that the line of new RunIdIncrementer() creates new Incrementer each time, which results in 1 id each time, hence the exception that you get of unique constraint at BATCH_JOB_INSTANCE table. You need to store it somewhere and access it each time. See also this answer.

0
votes

What's the isolation level on your JobRepository? You may want to knock it down to ISOLATION_READ_COMMITTED if it is currently ISOLATION_SERIALIZABLE (ISOLATION_SERIALIZABLE is the default).

<bean id="jobRepository" class="org.springframework.batch.core.repository.support.JobRepositoryFactoryBean" 
    p:dataSource-ref="dataSource"
    p:transactionManager-ref="transactionManager">
    <property name="tablePrefix" value="${batch.schema}.BATCH_" />
    <property name="isolationLevelForCreate" value="ISOLATION_READ_COMMITTED" />
</bean>