Skip to content

JdbcLockRegistry is unable to retry a lock when there is a serialization problem and you are using JPATransactionManager instead of DataSourceTransactionManager #3733

@estigma88

Description

@estigma88

In what version(s) of Spring Integration are you seeing this issue?

Spring boot version: 2.6.2
Spring data jpa: 2.6.0
Spring Integration version: 5.5.6

Describe the bug

JdbcLockRegistry is unable to retry a lock when there is a serialization problem and you are using JPATransactionManager instead of DataSourceTransactionManager.

As I have an application that uses the JdbcLockRegistry and JPA with Hibernate, by default, the application uses JPATransactionManager, but seems like for some transactional errors, JdbcLockRegistry is unable to retry the lock, as you can see in the following stack trace:

Caused by: org.springframework.dao.CannotAcquireLockException: Failed to lock mutex at 19414b49-c76b-3b77-a05d-3aca07a855aa; nested exception is org.springframework.orm.jpa.JpaSystemException: Unable to commit against JDBC Connection; nested exception is org.hibernate.TransactionException: Unable to commit against JDBC Connection
	at org.springframework.integration.jdbc.lock.JdbcLockRegistry$JdbcLock.rethrowAsLockException(JdbcLockRegistry.java:197)
	at org.springframework.integration.jdbc.lock.JdbcLockRegistry$JdbcLock.tryLock(JdbcLockRegistry.java:262)
	at com.geniussports.geniuslive.ingressmanager.domain.pipeline.ingress.IngressPipelineService.create(IngressPipelineService.kt:35)
	at com.geniussports.geniuslive.ingressmanager.application.adapters.pipeline.ingress.gateway.graphql.PipelineMutationResolver.createFromJSON(PipelineMutationResolver.kt:51)
	at jdk.internal.reflect.GeneratedMethodAccessor254.invoke(Unknown Source)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at org.springframework.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:282)
	at com.netflix.graphql.dgs.internal.DgsSchemaProvider.invokeDataFetcher(DgsSchemaProvider.kt:402)
	at com.netflix.graphql.dgs.internal.DgsSchemaProvider.access$invokeDataFetcher(DgsSchemaProvider.kt:64)
	at com.netflix.graphql.dgs.internal.DgsSchemaProvider$createBasicDataFetcher$1.get(DgsSchemaProvider.kt:279)
	at com.netflix.graphql.dgs.metrics.micrometer.DgsGraphQLMetricsInstrumentation$instrumentDataFetcher$1.get(DgsGraphQLMetricsInstrumentation.kt:101)
	at graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation.lambda$instrumentDataFetcher$0(DataLoaderDispatcherInstrumentation.java:86)
	at graphql.execution.ExecutionStrategy.fetchField(ExecutionStrategy.java:270)
	... 76 more
Caused by: org.springframework.orm.jpa.JpaSystemException: Unable to commit against JDBC Connection; nested exception is org.hibernate.TransactionException: Unable to commit against JDBC Connection
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:331)
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:233)
	at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:566)
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:743)
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711)
	at org.springframework.cloud.sleuth.instrument.tx.TracePlatformTransactionManager.commit(TracePlatformTransactionManager.java:121)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:654)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:407)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698)
	at org.springframework.integration.jdbc.lock.DefaultLockRepository$$EnhancerBySpringCGLIB$$29ab5ce4.acquire(<generated>)
	at org.springframework.integration.jdbc.lock.JdbcLockRegistry$JdbcLock.doLock(JdbcLockRegistry.java:268)
	at org.springframework.integration.jdbc.lock.JdbcLockRegistry$JdbcLock.tryLock(JdbcLockRegistry.java:249)
	... 88 more
Caused by: org.hibernate.TransactionException: Unable to commit against JDBC Connection
			3 lines skipped for [org.hibernate]
	at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:562)
	... 100 more
Caused by: org.postgresql.util.PSQLException: ERROR: could not serialize access due to read/write dependencies among transactions
  Detail: Reason code: Canceled on identification as a pivot, during commit attempt.
  Hint: The transaction might succeed if retried.
	at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2674)
	at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2364)
	at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:354)
	at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:314)
	at org.postgresql.jdbc.PgConnection.executeTransactionCommand(PgConnection.java:853)
	at org.postgresql.jdbc.PgConnection.commit(PgConnection.java:875)
	at com.zaxxer.hikari.pool.ProxyConnection.commit(ProxyConnection.java:387)
	at com.zaxxer.hikari.pool.HikariProxyConnection.commit(HikariProxyConnection.java)
			1 line skipped for [org.hibernate]

As far as I can tell, seems like to retry a lock we expect the following exceptions to be thrown, as you can see here:

TransientDataAccessException | TransactionTimedOutException | TransactionSystemException

However, Hibernate is throwing a org.hibernate.TransactionException, and after wrapped in the JpaSystemException, which is not part of any of the previous exceptions hierarchy, and therefore, the retry is not applied. I created this bug on the Hibernate side as I think we might not have a lot of options in Spring side.

Now, regarding workarounds, I can tell the following:

  1. Override the JPATransactionManager to catch this particular error and throw the right exception, like org.springframework.dao.CannotAcquireLockException, which is a TransientDataAccessException. This stack overflow question shows how. This solution seems pretty fragile.
  2. Creating a new XXXXXLockRespository, which inherit from DefaultLockRepository, and override the @Transactional annotation to explicitly tell which transaction manager to use, and declare a new DataSourceTransactionManager only to be used for the new XXXXXLockRepository.

Not sure if we have more options.

To Reproduce

Use a JPATransactionManager instead of DataSourceTransactionManager for JdbcLockRegistry, and generating a serialization database error.

Expected behavior

JdbcLockRegistry should work with JPATransactionManager and DataSourceTransactionManager

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions