spring-projects / spring-projects/spring-batch

OptimisticLockingFailureException when calling `JobOperator.stop`

Open
#5,484 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

has: minimal-example in: core type: bug
Dominant language
Java
Stars
3k
Forks
2.5k
Avg merge
6d 53m
Merged PRs (30d)
3

Description

I implemented a class that implements the StoppableTasklet interface. I store the executing thread, and the task contains a Thread.sleep(). The stop() method calls interrupt() on the stored thread.

I start the job using JobOperator.start(), wait for a short time, and then call JobOperator.stop(). At that point, I get an OptimisticLockingFailureException.

I also create a ThreadPoolTaskExecutor using the @BatchTaskExecutor qualifier.

I put together a sample project with an integration test using Testcontainers and a PostgreSQL database. Java 25 is required, and the project can be built with:

./mvnw package

The exception does not occur every time, but it happens very frequently.

You can find the sample project here: https://github.com/vicziani/spring-batch-bug

Stack trace:

org.springframework.dao.OptimisticLockingFailureException: Attempt to update step execution id=1 with wrong version (2), where current version is 3
        at org.springframework.batch.core.repository.dao.jdbc.JdbcStepExecutionDao.updateStepExecution(JdbcStepExecutionDao.java:256) ~[spring-batch-core-6.0.4.jar:6.0.4]
        at org.springframework.batch.core.repository.support.SimpleJobRepository.update(SimpleJobRepository.java:171) ~[spring-batch-core-6.0.4.jar:6.0.4]
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[na:na]
        at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359) ~[spring-aop-7.0.8.jar:7.0.8]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) ~[spring-aop-7.0.8.jar:7.0.8]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:158) ~[spring-aop-7.0.8.jar:7.0.8]
        at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:133) ~[spring-tx-7.0.8.jar:7.0.8]
        at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:371) ~[spring-tx-7.0.8.jar:7.0.8]
        at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:130) ~[spring-tx-7.0.8.jar:7.0.8]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-7.0.8.jar:7.0.8]
        at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:222) ~[spring-aop-7.0.8.jar:7.0.8]
        at jdk.proxy2/jdk.proxy2.$Proxy86.update(Unknown Source) ~[na:na]
        at org.springframework.batch.core.step.AbstractStep.execute(AbstractStep.java:302) ~[spring-batch-core-6.0.4.jar:6.0.4]
        at org.springframework.batch.core.job.SimpleStepHandler.handleStep(SimpleStepHandler.java:128) ~[spring-batch-core-6.0.4.jar:6.0.4]
        at org.springframework.batch.core.job.AbstractJob.handleStep(AbstractJob.java:397) ~[spring-batch-core-6.0.4.jar:6.0.4]
        at org.springframework.batch.core.job.SimpleJob.doExecute(SimpleJob.java:129) ~[spring-batch-core-6.0.4.jar:6.0.4]
        at org.springframework.batch.core.job.AbstractJob.execute(AbstractJob.java:293) ~[spring-batch-core-6.0.4.jar:6.0.4]
        at org.springframework.batch.core.launch.support.TaskExecutorJobLauncher$1.run(TaskExecutorJobLauncher.java:220) ~[spring-batch-core-6.0.4.jar:6.0.4]
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1090) ~[na:na]
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614) ~[na:na]
        at java.base/java.lang.Thread.run(Thread.java:1474) ~[na:na]

The other thing I don't understand is that if the stop() method in StoppableTasklet is deprecated, why is it still necessary to implement it, and what is the recommended way to implement it?

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by building and running the linked sample project with Java 25 using ./mvnw package, then inspect its integration test and the StoppableTasklet implementation. Trace JobOperator.stop() alongside the JdbcStepExecutionDao.updateStepExecution stack-trace path and the ThreadPoolTaskExecutor configuration. Done means reproducing the optimistic-locking failure reliably and establishing the recommended behavior for deprecated StoppableTasklet.stop().

Written by the indexing model from the issue text.

Assessment

Tech stack
java, postgresql, spring
Domain
backend, databases
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.