8

I am kind of frustrated right now as I thought this will be much easier and the issue would be documented way better but I just can not find a solution. Therefore I seek help here.

I am working on a Kotlin project which leverages spring boot version 2.5.3 and uses spring data jpa for database access and schema definition. Pretty common and straight-forward. Now assume we have some kind of UserService which contains a method updateUsername which gets a username as a parameter und updates the username after it verified its validity by an external service. For demo purposes of the issue I want to highlight, before we verify the username we set the username manually to "foo". This whole unit of work should happend in a transaction which is why the method is annotated with @Transactional. But because of the call to the external service the method will suspend when we wait for the http response (note the suspend keyword on both methods).

@Service
class UserService(private val userRepository: UserRepository) {
    @Transactional
    suspend fun setUsername(id: UUID, username: String): Person {
        logger.info { "Updating username..." }
        val user = userRepository.findByIdentityId(identityId = id)
            ?: throw IllegalArgumentException("User does not exist!")

        // we update the username here but the change will be overridden after the verification to the actual username!
        user.userName = "foo"

        verifyUsername(username)

        user.userName = username
        return userRepository.save(user)
    }
    
    private suspend fun verifyUsername(username: String) {
        // assume we are doing some kind of network call here which will suspend while waiting got a response
        logger.info { "Verifying..." }
        delay(1000)
        logger.info { "Finished verifying!" }
    }
}

This compiles successfully and I can also execute the method and it starts a new transaction, but the transaction will be commited as soon as we suspend the invocation of the verifyUsername method on calling delay(1000). Therefore our database will acually hold the value "foo" as the username until it is overwritten. But if the code after verifyUsername would fail and throw an exception, we could not rollback this change as the transaction was already commited and foo would stay in the database forever!!! This is definitely not the expected behavior as we only want to commit the transaction at the very end of our method, so we can rollback the transaction at any time if something went wrong. Here you can see the logs:

DEBUG o.s.orm.jpa.JpaTransactionManager - Creating new transaction with name [x.x.x.UserService.setUsername]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG o.s.orm.jpa.JpaTransactionManager - Opened new EntityManager [SessionImpl(1406394125<open>)] for JPA transaction
DEBUG o.s.orm.jpa.JpaTransactionManager - Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@9ec4d42]
INFO  x.x.x.UserService - Updating username...
DEBUG org.hibernate.SQL - select person0_.id as id1_6_, person0_.email as email2_6_, person0_.family_name as family_n3_6_, person0_.given_name as given_na4_6_, person0_.identity_id as identity5_6_, person0_.user_name as user_nam6_6_ from person person0_ where person0_.identity_id=?
INFO  x.x.x.UserService - Verifying...
DEBUG o.s.orm.jpa.JpaTransactionManager - Initiating transaction commit
DEBUG o.s.orm.jpa.JpaTransactionManager - Committing JPA transaction on EntityManager [SessionImpl(1406394125<open>)]
DEBUG org.hibernate.SQL - update person set email=?, family_name=?, given_name=?, identity_id=?, user_name=? where id=?
DEBUG o.s.orm.jpa.JpaTransactionManager - Closing JPA EntityManager [SessionImpl(1406394125<open>)] after transaction
INFO  x.x.x.UserService - Finished verifying
DEBUG o.s.orm.jpa.JpaTransactionManager - Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG o.s.orm.jpa.JpaTransactionManager - Opened new EntityManager [SessionImpl(319912425<open>)] for JPA transaction
DEBUG o.s.orm.jpa.JpaTransactionManager - Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@1dd261da]
DEBUG org.hibernate.SQL - select person0_.id as id1_6_0_, person0_.email as email2_6_0_, person0_.family_name as family_n3_6_0_, person0_.given_name as given_na4_6_0_, person0_.identity_id as identity5_6_0_, person0_.user_name as user_nam6_6_0_ from person person0_ where person0_.id=?
DEBUG o.s.orm.jpa.JpaTransactionManager - Initiating transaction commit
DEBUG o.s.orm.jpa.JpaTransactionManager - Committing JPA transaction on EntityManager [SessionImpl(319912425<open>)]
DEBUG org.hibernate.SQL - update person set email=?, family_name=?, given_name=?, identity_id=?, user_name=? where id=?
DEBUG o.s.orm.jpa.JpaTransactionManager - Closing JPA EntityManager [SessionImpl(319912425<open>)] after transaction

In this spring article it says that "Transactions on Coroutines are supported via the programmatic variant of the Reactive transaction management provided as of Spring Framework 5.2. For suspending functions, a TransactionalOperator.executeAndAwait extension is provided."

Does that mean that @Transactional just cannot be used on suspending methods and you should programatically handle transaction management? Update: This thread states that @Transactional should work for suspend functions.

I am also aware, that all my database operations are running synchronously, and I could use r2dbc to make them asynchronously (as long as my database provides the driver implementing the specification), but I think that my question does not relate to how I communicate with the database but more about how spring handles suspending calls withing @Transactional annotated methods.

What are your thoughts and recommendations here? I am definitely not the first dev who does suspending work in a transactional method in Kotlin, and still I am not able to find helpful resources on this issue.

Thanks guys!

1
  • I'm have been trying to customize TransactionInterceptor to support coroutine, the approach I took was to restore TransacionSyncronizationManager state using a ThreadContextElement. The spring has a large codebase therefore I'm restrict the exploration to JPA context, for reference if anyone would like to go deeper and improve the code could be found here it's NOT production ready but could be used to explore. Commented Sep 13, 2021 at 17:49

2 Answers 2

8

JPA isn't supported with Coroutine Transactions as JPA is fully synchronous. Coroutine Transaction propagation works only with technologies that provide a reactive integration such as MongoDB, R2DBC, or Neo4j.

JPA assumes an imperative programming model hence its transaction manager stores transactional state in ThreadLocal storage. Reactive integrations and specifically Coroutines employ Coroutines context/Reactor's subscription context to keep track of the transaction status. There's no link between ThreadLocal and the context feature of Coroutines/Project Reactor.

Going forward, using blocking integrations such as JPA require special attention in the context of coroutines and their transactional scope needs to be constrained onto a single thread.

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks for your reply! :) I understand that JPA is fully synchronous and transactional state is bound to ThreadLocal storage. But this github issue states, that @Transactional is now supported for suspend functions. Technically I guess they check if the @Transactional annotation is on a suspend method and if so they bind the transactional state to the coroutine context instead of the ThreadLocal storage maybe.
There's a bit more to that. Coroutine support is built on top of reactive infrastructure. Spring's AbstractPlatformTransactionManager and JpaTransactionManager are not aware of anything reactive going on. Also, JPA's EntityManager is only valid on the thread that hosts the transaction and since that concept isn't applicable for Coroutines or reactive programming, there's nothing we can do.
Alright I understand the limitations. So regarding my use case stated above, I think I have two options, right? 1. Methods marked as @Transactional should not be suspending. Therefore, I should call verifyUsername inside runBlocking which would unfortunately block the thread we are currently working on until verifyUsername completes. 2. Use an R2DBC database driver (e.g. io.r2dbc:r2dbc-postgresql) and spring-boot-starter-data-r2dbc to leverage reactive database transactions. So I could mark my suspend methods as @Transactional again. Would you agree?
What do you think @mp911de ?
@yezper FYI) I think you don't have to use runBlocking, but just withContext(Dispatchers.IO).
0

If you really want to do transactional stuff, like updating multiple tables in single transaction while keeping your function suspend - you can do that.

Personally I don't use @Transactional on suspend methods but instead I open transaction programmatically like so:

@Autowired
val tx: org.springframework.transaction.support.TransactionOperations

suspend fun doTransactionStuff(...) = withContext(IO) {
 tx.execute {
    dao1.updateSomething(...)
    // downside is that you cannot suspend here, but you can block
    dao2.updateSomethingElse(...)
 }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.