spring-projects / spring-projects/spring-batch
Unable to correctly save task metadata when registering JobExecutionShutdownHook
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 3k
- Forks
- 2.5k
- Avg merge
- 6d 53m
- Merged PRs (30d)
- 3
Description
Bug description
By registering a JVM exit callback using JobExecutionShutdownHook, and then using kill to shut down the JVM program during task execution, you will see an exception indicating that the database operation failed.
Environment
openjdk 25.0.2 2026-01-20 LTS
spring batch 6.0.2
spring boot 4.0.3
Steps to reproduce
registering a JVM exit callback, and execute command kill 62989
val hook = JobExecutionShutdownHook(jobExecution, jobOperator)
Runtime.getRuntime().addShutdownHook(hook)
I also tried SpringApplication.getShutdownHandlers().add(hook), but it still couldn't save the work to the DB.
The exception log shows that this line threw a database operation exception:
org.springframework.batch.core.launch.support.SimpleJobOperator.stop(SimpleJobOperator.java:349)
full log:
2026-03-01T15:19:45.071+08:00 INFO 34906 --- [sast-web3-consumer] [ Thread-12] o.s.b.c.l.s.JobExecutionShutdownHook : Received JVM shutdown signal
2026-03-01T15:19:45.071+08:00 INFO 34906 --- [sast-web3-consumer] [ Thread-12] o.s.b.c.l.s.JobExecutionShutdownHook : Attempting to gracefully stop job execution 228
2026-03-01T15:19:45.072+08:00 INFO 34906 --- [sast-web3-consumer] [ Thread-12] o.s.b.c.l.s.TaskExecutorJobOperator : Stopping job execution: JobExecution: id=228, version=1, startTime=2026-03-01T15:19:09.782276, endTime=null, lastUpdated=2026-03-01T15:19:09.782424, status=STARTED, exitStatus=exitCode=UNKNOWN;exitDescription=, job=[JobInstance: id=225, version=0, Job=[contractAnalysisJob]], jobParameters=[{JobParameter{name='task', value=ContractScanTask(analyzer=Semgrep, analysisId=1190, filePath=null, objKey=adds, extra=null, contractAddress=x faf88wf aaw a]odwdded 9aw d, chainType=ETH, dataType=SRC), type=class io.github.xhstormr.sast.model.contract.ContractScanTask, identifying=true}}]
2026-03-01T15:19:45.578+08:00 INFO 34906 --- [sast-web3-consumer] [ionShutdownHook] i.g.x.s.d.c.handler.ApplicationLogger : Application is destroyed!
2026-03-01T15:19:45.589+08:00 WARN 34906 --- [sast-web3-consumer] [ Thread-12] com.zaxxer.hikari.pool.ProxyConnection : HikariPool-1 - Connection org.postgresql.jdbc.PgConnection@665f7826 marked as broken because of SQLSTATE(08006), ErrorCode(0)
org.postgresql.util.PSQLException: An I/O error occurred while sending to the backend.
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:456)
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:387)
at org.postgresql.jdbc.PgConnection.executeTransactionCommand(PgConnection.java:998)
at org.postgresql.jdbc.PgConnection.commit(PgConnection.java:1020)
at com.zaxxer.hikari.pool.ProxyConnection.commit(ProxyConnection.java:378)
at com.zaxxer.hikari.pool.HikariProxyConnection.commit(HikariProxyConnection.java)
at org.hibernate.resource.jdbc.internal.AbstractLogicalConnectionImplementor.commit(AbstractLogicalConnectionImplementor.java:84)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commitNoRollbackOnly(JdbcResourceLocalTransactionCoordinatorImpl.java:249)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:242)
at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:89)
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:553)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:794)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:757)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:687)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:408)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:130)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:222)
at jdk.proxy2/jdk.proxy2.$Proxy248.update(Unknown Source)
at org.springframework.batch.core.launch.support.SimpleJobOperator.stop(SimpleJobOperator.java:349)
at org.springframework.batch.core.launch.support.TaskExecutorJobOperator.stop(TaskExecutorJobOperator.java:139)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:158)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:133)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:371)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:130)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:222)
at jdk.proxy2/jdk.proxy2.$Proxy266.stop(Unknown Source)
at org.springframework.batch.core.launch.support.JobExecutionShutdownHook.run(JobExecutionShutdownHook.java:55)
Caused by: java.net.SocketException: Socket closed
at java.base/sun.nio.ch.NioSocketImpl.endRead(NioSocketImpl.java:242)
at java.base/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:321)
at java.base/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:354)
at java.base/sun.nio.ch.NioSocketImpl$1.read(NioSocketImpl.java:798)
at java.base/java.net.Socket$SocketInputStream.implRead(Socket.java:974)
at java.base/java.net.Socket$SocketInputStream.read(Socket.java:964)
at org.postgresql.core.VisibleBufferedInputStream.readMore(VisibleBufferedInputStream.java:192)
at org.postgresql.core.VisibleBufferedInputStream.ensureBytes(VisibleBufferedInputStream.java:159)
at org.postgresql.core.VisibleBufferedInputStream.ensureBytes(VisibleBufferedInputStream.java:144)
at org.postgresql.core.VisibleBufferedInputStream.read(VisibleBufferedInputStream.java:76)
at org.postgresql.core.PGStream.receiveChar(PGStream.java:477)
at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2314)
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:429)
... 32 common frames omitted
Exception in thread "Thread-12" java.lang.RuntimeException: Unable to gracefully stop job execution 228
at org.springframework.batch.core.launch.support.JobExecutionShutdownHook.run(JobExecutionShutdownHook.java:62)
Caused by: org.springframework.dao.DataAccessResourceFailureException: Hibernate transaction: Unable to commit against JDBC Connection; An I/O error occurred while sending to the backend.
at org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:152)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:102)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:111)
at org.springframework.orm.jpa.hibernate.HibernateExceptionTranslator.convertHibernateAccessException(HibernateExceptionTranslator.java:144)
at org.springframework.orm.jpa.hibernate.HibernateExceptionTranslator.convertHibernateAccessException(HibernateExceptionTranslator.java:131)
at org.springframework.orm.jpa.hibernate.HibernateExceptionTranslator.translateExceptionIfPossible(HibernateExceptionTranslator.java:105)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:223)
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:557)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:794)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:757)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:687)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:408)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:130)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:222)
at jdk.proxy2/jdk.proxy2.$Proxy248.update(Unknown Source)
at org.springframework.batch.core.launch.support.SimpleJobOperator.stop(SimpleJobOperator.java:349)
at org.springframework.batch.core.launch.support.TaskExecutorJobOperator.stop(TaskExecutorJobOperator.java:139)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:565)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:158)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:133)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:371)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:130)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:222)
at jdk.proxy2/jdk.proxy2.$Proxy266.stop(Unknown Source)
at org.springframework.batch.core.launch.support.JobExecutionShutdownHook.run(JobExecutionShutdownHook.java:55)
Caused by: org.postgresql.util.PSQLException: An I/O error occurred while sending to the backend.
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:456)
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:387)
at org.postgresql.jdbc.PgConnection.executeTransactionCommand(PgConnection.java:998)
at org.postgresql.jdbc.PgConnection.commit(PgConnection.java:1020)
at com.zaxxer.hikari.pool.ProxyConnection.commit(ProxyConnection.java:378)
at com.zaxxer.hikari.pool.HikariProxyConnection.commit(HikariProxyConnection.java)
at org.hibernate.resource.jdbc.internal.AbstractLogicalConnectionImplementor.commit(AbstractLogicalConnectionImplementor.java:84)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commitNoRollbackOnly(JdbcResourceLocalTransactionCoordinatorImpl.java:249)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:242)
at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:89)
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:553)
... 22 more
Caused by: java.net.SocketException: Socket closed
at java.base/sun.nio.ch.NioSocketImpl.endRead(NioSocketImpl.java:242)
at java.base/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:321)
at java.base/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:354)
at java.base/sun.nio.ch.NioSocketImpl$1.read(NioSocketImpl.java:798)
at java.base/java.net.Socket$SocketInputStream.implRead(Socket.java:974)
at java.base/java.net.Socket$SocketInputStream.read(Socket.java:964)
at org.postgresql.core.VisibleBufferedInputStream.readMore(VisibleBufferedInputStream.java:192)
at org.postgresql.core.VisibleBufferedInputStream.ensureBytes(VisibleBufferedInputStream.java:159)
at org.postgresql.core.VisibleBufferedInputStream.ensureBytes(VisibleBufferedInputStream.java:144)
at org.postgresql.core.VisibleBufferedInputStream.read(VisibleBufferedInputStream.java:76)
at org.postgresql.core.PGStream.receiveChar(PGStream.java:477)
at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2314)
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:429)
... 32 more
Expected behavior
Able to save task metadata and gracefully exit, no exception occurs.
Minimal Complete Reproducible example
Please provide a failing test or a minimal complete verifiable example that reproduces the issue.
Bug reports that are reproducible will take priority in resolution over reports that are not reproducible.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at JobExecutionShutdownHook.run (JobExecutionShutdownHook.java:55) and follow the stop call through TaskExecutorJobOperator to SimpleJobOperator.stop (SimpleJobOperator.java:349). Reproduce the failure with a JVM shutdown triggered by kill and inspect whether the PostgreSQL connection remains available during shutdown. Done means the job metadata is saved without the reported database exception.
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
- 45/100