-
-
Notifications
You must be signed in to change notification settings - Fork 202
Description
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
EntityManagerinstance
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 toSessionContextHolder
before {
SessionContextHolder.setEntityManager(require(EntityManager::class.java))
}- In the
serviceorrepository, everytime I need theEntityManager, I will get it from SessionContextHolder instead of inject to constructor.
interface JpaRepository {
fun getEntityManager(): EntityManager = SessionContextHolder.getEntityManager()
}- In async context, I change the
AsyncExecutorlike:
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.