Skip to content

[Question] EntityManager Transaction Issues in Asynchronous Kotlin/Hibernate Context #3601

@jonaskahn

Description

@jonaskahn

Dealing with EntityManager & Async Issues (took me a whole day 😅)

So here's what I was struggling with - I wanted to do some async processing with Hibernate's EntityManager but ran into a tricky situation.

The Jooby docs (hibernate section) mention that transactions auto-commit when the request finishes. That's cool for normal requests, but I needed to handle some long-running tasks without making the client wait around.

I tried setting up an AsyncExecutor like this:

class AsyncExecutor {
   fun <T> exec(fn: () -> T): Deferred<T> {
       val lang = LanguageContextHolder.getLanguage()
       val userContext = UserContextHolder.getCurrentUser()
       return CoroutineScope(Dispatchers.Default).async {
           LanguageContextHolder.setLanguage(lang)
           UserContextHolder.setCurrentUserInfo(userContext)
           fn()
       }
   }
}

And used it in my controller:

class DomainController @Inject constructor(
    private val domainService: DomainService,
    private val asyncExecutor: AsyncExecutor
) {
    @Transactional
    fun analysis(@Valid request: AnalysisDomainRequest) {
        asyncExecutor.exec {
            domainService.analysis(request.domainId!!)
        }
    }
}

Here's the problem though - since AsyncExecutor makes function analysis run in a different thread, the EntityManager (Hibernate Session) is already closed then any access to database will cause a infinity waiting.

My workaround

  • First, I made a class to hold the current EntityManager instance
class SessionContextHolder {
    companion object {
        private val threadLocalData: ThreadLocal<EntityManager> = ThreadLocal()

        fun getEntityManager(): EntityManager {
            return threadLocalData.get()
        }

        fun setEntityManager(entityManager: EntityManager) {
            threadLocalData.set(entityManager)
        }
    }
}
  • Next step, in before {} function I injected current EntityManager to SessionContextHolder
before {
    SessionContextHolder.setEntityManager(require(EntityManager::class.java))
}
  • In the service or repository, everytime I need the EntityManager, I will get it from SessionContextHolder instead of inject to constructor.
interface JpaRepository {
    fun getEntityManager(): EntityManager = SessionContextHolder.getEntityManager()
}
  • In async context, I change the AsyncExecutor like:
class AsyncExecutor @Inject constructor(private val entityManagerFactory: EntityManagerFactory) {
    @Suppress("EXPERIMENTAL_API_USAGE")
    fun <T> exec(fn: () -> T): Deferred<T?> {
        val lang = LanguageContextHolder.getLanguage()
        val userContext = UserContextHolder.getCurrentUser()
        return CoroutineScope(Dispatchers.IO).async {
            log.debug("EXECUTING EXECUTION: {}", fn)
            LanguageContextHolder.setLanguage(lang)
            UserContextHolder.setCurrentUserInfo(userContext)

            // Get new entityManager instance
            val entityManager = entityManagerFactory.createEntityManager()
            SessionContextHolder.setEntityManager(entityManager)
            entityManager.unwrap(Session::class.java).use {
                val tx = it.beginTransaction()
                try {
                    val result = fn()
                    tx.commit()
                    result
                } catch (ex: Exception) {
                    log.warn("EXECUTING EXECUTION: $fn", ex)
                    tx.rollback()
                    null
                }
            }
        }
    }
}

My problem now is solved. But I still do have a question, Can I do it by a better way?

P/s: Neither statefulSession nor statelessSession makes things work either.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions