17
votes

I am quite new to Spring and Spring-Batch in particular. Still I somehow managed to install the Spring Batch-Admin. I added custom jobs and Hibernate/JPA for persistence.

Everything is working as expected, up to the point where the first chunk should be persisted. Then I receive the following error-message:

org.springframework.transaction.CannotCreateTransactionException: 
      Could not open JPA  EntityManager for transaction;

nested exception is java.lang.IllegalStateException: Already value
      [org.springframework.jdbc.datasource.ConnectionHolder@60d31437] 
      for key [org.springframework.jdbc.datasource.DriverManagerDataSource@12da4b19] 
      bound to thread [jobLauncherTaskExecutor-1]

This is the full stacktrace:

org.springframework.transaction.CannotCreateTransactionException: Could not open JPA  EntityManager for transaction; nested exception is java.lang.IllegalStateException: Already value [org.springframework.jdbc.datasource.ConnectionHolder@43f9e588] for key [org.springframework.jdbc.datasource.DriverManagerDataSource@84f171a] bound to thread [jobLauncherTaskExecutor-1]
     at org.springframework.orm.jpa.JpaTransactionManager.doBegin(JpaTransactionManager.java:427)
     at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:371)
     at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:335)
     at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:105)
     at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
     at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202)
     at com.sun.proxy.$Proxy41.saveIfUnique(Unknown Source)
     at com.qompa.batch.ArticleItemWriter.write(ArticleItemWriter.java:28)
     at org.springframework.batch.core.step.item.SimpleChunkProcessor.writeItems(SimpleChunkProcessor.java:171)
     at org.springframework.batch.core.step.item.SimpleChunkProcessor.doWrite(SimpleChunkProcessor.java:150)
     at org.springframework.batch.core.step.item.FaultTolerantChunkProcessor$3.doWithRetry(FaultTolerantChunkProcessor.java:313)
     at org.springframework.batch.retry.support.RetryTemplate.doExecute(RetryTemplate.java:240)
     at org.springframework.batch.retry.support.RetryTemplate.execute(RetryTemplate.java:187)
     at org.springframework.batch.core.step.item.BatchRetryTemplate.execute(BatchRetryTemplate.java:213)
     at org.springframework.batch.core.step.item.FaultTolerantChunkProcessor.write(FaultTolerantChunkProcessor.java:402)
     at org.springframework.batch.core.step.item.SimpleChunkProcessor.process(SimpleChunkProcessor.java:194)
     at org.springframework.batch.core.step.item.ChunkOrientedTasklet.execute(ChunkOrientedTasklet.java:74)
     at org.springframework.batch.core.step.tasklet.TaskletStep$ChunkTransactionCallback.doInTransaction(TaskletStep.java:386)
     at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:130)
     at org.springframework.batch.core.step.tasklet.TaskletStep$2.doInChunkContext(TaskletStep.java:264)
     at org.springframework.batch.core.scope.context.StepContextRepeatCallback.doInIteration(StepContextRepeatCallback.java:76)
     at org.springframework.batch.repeat.support.RepeatTemplate.getNextResult(RepeatTemplate.java:367)
     at org.springframework.batch.repeat.support.RepeatTemplate.executeInternal(RepeatTemplate.java:214)
     at org.springframework.batch.repeat.support.RepeatTemplate.iterate(RepeatTemplate.java:143)
     at org.springframework.batch.core.step.tasklet.TaskletStep.doExecute(TaskletStep.java:250)
     at org.springframework.batch.core.step.AbstractStep.execute(AbstractStep.java:195)
     at org.springframework.batch.core.job.SimpleStepHandler.handleStep(SimpleStepHandler.java:135)
     at org.springframework.batch.core.job.flow.JobFlowExecutor.executeStep(JobFlowExecutor.java:61)
     at org.springframework.batch.core.job.flow.support.state.StepState.handle(StepState.java:60)
     at org.springframework.batch.core.job.flow.support.SimpleFlow.resume(SimpleFlow.java:144)
     at org.springframework.batch.core.job.flow.support.SimpleFlow.start(SimpleFlow.java:124)
     at org.springframework.batch.core.job.flow.FlowJob.doExecute(FlowJob.java:135)
     at org.springframework.batch.core.job.AbstractJob.execute(AbstractJob.java:281)
     at org.springframework.batch.core.launch.support.SimpleJobLauncher$1.run(SimpleJobLauncher.java:120)
     at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
     at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
     at java.lang.Thread.run(Thread.java:724)
Caused by: java.lang.IllegalStateException: Already value [org.springframework.jdbc.datasource.ConnectionHolder@43f9e588] for key [org.springframework.jdbc.datasource.DriverManagerDataSource@84f171a] bound to thread [jobLauncherTaskExecutor-1]
     at org.springframework.transaction.support.TransactionSynchronizationManager.bindResource(TransactionSynchronizationManager.java:189)
     at org.springframework.orm.jpa.JpaTransactionManager.doBegin(JpaTransactionManager.java:402)
... 36 more

The same Job executes fine in a standalone application. The problem occurs only in the Spring-Batch-Admin environment. Below you can see the project structure and dependencies:

enter image description here

This is the app-context.xml that overrides/extends the Batch-Admin configuration:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:batch="http://www.springframework.org/schema/batch"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:task="http://www.springframework.org/schema/task" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
        http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch-2.1.xsd
        http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.2.xsd">

    <context:component-scan base-package="com.company.batch" />

    <context:property-placeholder location="classpath:batch.properties" />

    <import resource="classpath:/META-INF/spring/batch/jobs/article-job.xml" />

    <bean id="dataSource"
        class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="${batch.jdbc.driver}" />
        <property name="url" value="${batch.jdbc.url}" />
        <property name="username" value="${batch.jdbc.user}" />
        <property name="password" value="${batch.jdbc.password}" />
    </bean>

    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <bean id="entityManagerFactory"
        class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="packagesToScan" value="com.qompa.batch" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <property name="database" value="POSTGRESQL"></property>
                <property name="showSql" value="true" />
                <property name="generateDdl" value="false" />
                <property name="databasePlatform" value="com.company.utils.persistence.CustomPGDialect" />
            </bean>
        </property>
        <property name="jpaProperties">
            <props>
                <prop key="hibernate.hbm2ddl.auto"></prop>
            </props>
        </property>
    </bean>

    <bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory" />
    </bean>
    <tx:annotation-driven transaction-manager="txManager" />

    <!-- schedule tasks -->
    <task:scheduled-tasks>
        <task:scheduled ref="articleRetrieval" method="run"
            cron="0 0 */4 * * *" />
        <task:scheduled ref="articleConversion" method="run"
            cron="0 15 */4 * * *" />
    </task:scheduled-tasks>
</beans>

What I understand so far is that it has to do with the ThreadPoolTaskExecutor to which the jobLauncherTaskExecutor bean refers. It seems to handle connection pooling for concurrently running jobs ... but to be honest I have no clue how to change my configurations to make these things work.

[Edit]: I am not even sure wether it is the afromentioned ThreadPoolTaskExecutor. But it seem s to be an implementation of the TaskExecutor interface.

If anyone ran into a similar issue, or has a suggestion how to configure my application in a way that transactions can be created for my persistence methods: Please give me a hint!

6
I'm not familiar with spring batch, but it looks as though some thread local stuff isn't getting cleaned up on completion of a job. I'd look into that, google may have some good results.Taylor
Can you post more results if you have them?Angular University
Any update on this ? Struggling with the same issueNitish Kumar

6 Answers

21
votes

The error comes from JpaTransactionManager line 403:

TransactionSynchronizationManager.bindResource(getDataSource(), conHolder);

The error means that the transaction manager is trying to bind the datasource (not the entity manager) to the thread, but the datasource is already there and this is unexpected.

Note that the transaction manager had not started yet to bind the Entity Manager to the thread, that would happen next at JpaTransactionManager line 416:

There are two possible explanations:

  • Somebody (another transaction manager?) is adding the datasource to the thread before the transaction manager and this is unexpected.

  • Or noone is adding the datasource to the transaction manager, is just that at the end of the task execution noone cleans the thread before returning it to the pool, maybe due an error or an unhandled exception.

One question, does this also happen for only one execution thread, or only when there are several?

To find out what the problem is, these are some steps:

  • run with a minimal number of threads that cause the problem

  • put a breakpoint in TransactionSynchronizationManager.bindResource() to see who adds the connection to the thread. The breakpoint can be a conditional breakpoint with a condition on the thread name: "jobLauncherTaskExecutor-1".equals(Thread.currentThread().getName())

  • put also a breakpoint in TransactionSynchronizationManager.unbindResource(), to see if the datasource is unbound from the thread. when the breakpoints hit, scroll down the stacktrace and see which classes are causing this.

7
votes

This normally happens when you have multiple transaction managers in place.

https://blog.codecentric.de/en/2013/06/spring-batch-2-2-javaconfig-part-3-profiles-and-environments/

You can specify transaction manager for tasklets as follows:

<tasklet transaction-manager="transactionManager">
6
votes

If you have 2 dataSource, I suggest you to read:

https://github.com/spring-projects/spring-boot/issues/3012

So... configure the main datasource (is important the transaction manager's name)

@Configuration
@EnableJpaRepositories(
        entityManagerFactoryRef = "entityManager",
        transactionManagerRef = "transactionManager",
        basePackages = "a.b.c")
@PropertySource({"classpath:db_persistence.properties"})
@EnableTransactionManagement

and the other datasource:

@Configuration
@EnableJpaRepositories(
        entityManagerFactoryRef = "another_EntityManager",
        transactionManagerRef = "another_transactionManager",
        basePackages = "x.y.z")
@PropertySource({"classpath:db_persistence.properties"})
@EnableTransactionManagement

I hope that this help you.

3
votes
return stepBuilderFactory.get("orderStep1").<sourceBean, destBean>chunk(5)
            .reader(reader)
            .processor(batchFileRowProcessor)
            .writer(batchFileRowDataWritter)
            .taskExecutor(taskExecutor)
            .transactionManager(platformTransactionManager)
            .throttleLimit(1).build();

platformTransactionManager is the qualified bean from the data source configuration

0
votes

I was able to solve a similar issue by implementing a spring batch configuration for JPA

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.configuration.BatchConfigurationException;
import org.springframework.batch.core.configuration.annotation.BatchConfigurer;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.explore.support.JobExplorerFactoryBean;
import org.springframework.batch.core.explore.support.MapJobExplorerFactoryBean;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.launch.support.SimpleJobLauncher;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.repository.support.JobRepositoryFactoryBean;
import org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean;
import org.springframework.batch.support.transaction.ResourcelessTransactionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.transaction.PlatformTransactionManager;


@Configuration
public class JpaBatchConfigurer implements BatchConfigurer {
    private static final Logger logger = LoggerFactory
            .getLogger(JpaBatchConfigurer.class);

    @Inject 
    private DataSource dataSource;

    @Inject
    private PlatformTransactionManager transactionManager;

    private JobRepository jobRepository;
    private JobLauncher jobLauncher;
    private JobExplorer jobExplorer;


    protected JpaBatchConfigurer() {
    }


    @Override
    @Bean
    public JobRepository getJobRepository() {
        return jobRepository;
    }

    @Override
    public PlatformTransactionManager getTransactionManager() {
        return transactionManager;
    }

    @Override
    @Bean
    public JobLauncher getJobLauncher() {
        return jobLauncher;
    }

    @Override
    @Bean
    public JobExplorer getJobExplorer() {
        return jobExplorer;
    }

    @PostConstruct
    public void initialize() {
        try {
            if (dataSource == null) {
                logger.warn("No datasource was provided...using a Map based JobRepository");

                if (this.transactionManager == null) {
                    this.transactionManager = new ResourcelessTransactionManager();
                }

                MapJobRepositoryFactoryBean jobRepositoryFactory = new MapJobRepositoryFactoryBean(
                        this.transactionManager);
                jobRepositoryFactory.afterPropertiesSet();
                this.jobRepository = jobRepositoryFactory.getObject();

                MapJobExplorerFactoryBean jobExplorerFactory = new MapJobExplorerFactoryBean(
                        jobRepositoryFactory);
                jobExplorerFactory.afterPropertiesSet();
                this.jobExplorer = jobExplorerFactory.getObject();
            } else {
                this.jobRepository = createJobRepository();

                JobExplorerFactoryBean jobExplorerFactoryBean = new JobExplorerFactoryBean();
                jobExplorerFactoryBean.setDataSource(this.dataSource);
                jobExplorerFactoryBean.afterPropertiesSet();
                this.jobExplorer = jobExplorerFactoryBean.getObject();
            }

            this.jobLauncher = createJobLauncher();
        } catch (Exception e) {
            throw new BatchConfigurationException(e);
        }
    }

    private JobLauncher createJobLauncher() throws Exception {
        SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
        jobLauncher.setJobRepository(jobRepository);
        jobLauncher.setTaskExecutor( new SimpleAsyncTaskExecutor());
        jobLauncher.afterPropertiesSet();
        return jobLauncher;
    }

    protected JobRepository createJobRepository() throws Exception {
        JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
        factory.setIsolationLevelForCreate("ISOLATION_SERIALIZABLE");
        factory.setDataSource(dataSource);
        factory.setTransactionManager(transactionManager);
        factory.setValidateTransactionState(false);
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    @Bean
    public JobBuilderFactory jobBuilderFactory(JobRepository jobRepository){
        return new JobBuilderFactory(jobRepository);
    }

    @Bean   
    public StepBuilderFactory stepBuilderFactory(JobRepository jobRepository, PlatformTransactionManager transactionManager){
        return new StepBuilderFactory(jobRepository, transactionManager);
    }
}

This was copied from:https://github.com/hantsy/spring4-sandbox/blob/master/batch-jpa/src/main/java/com/hantsylabs/example/spring/config/JpaBatchConfigurer.java

0
votes

These kind of problems occur with older version of java like jdk 6 or further lower versions.Upgrade your jdk version to 7 or above. Even i do had the same kinda issue before which gone when i updated my jdk version to 7.