I am using Timefold 1.15 and Quarkus 3.1
I am trying to save the data during solving of the timetable
but I have some contexts that's required during solve, tenant context for row security and identity context
but I am facing issues related to unavailability of the contexts
Sample code:
solverManager.solveBuilder()
.withProblemId(finalRequestId)
.withProblemFinder((id) -> timeTableJob.get(finalRequestId).timetable)
.withBestSolutionConsumer(bestSolution -> timeTableJob.put(finalRequestId, Job.ofTimetable(bestSolution)))
.withFinalBestSolutionConsumer(solution -> {
timeTableJob.put(finalRequestId,Job.ofTimetable(solution));
plannedLessonRepository.save(solution);
})
.withExceptionHandler((id, exception) -> {
timeTableJob.put(finalRequestId, Job.ofException(exception));
LOGGER.error("Failed solving jobId ({}).", finalRequestId, exception);
})
.run();
Error:
2024-11-26 17:18:32,463 ERROR [org.acm.sch.res.TimeTableResource] (pool-13-thread-1) Failed solving jobId (101).: org.hibernate.HibernateException: SessionFactory configured for multi-tenancy, but no tenant identifier specified at org.hibernate.internal.AbstractSharedSessionContract.getTenantId(AbstractSharedSessionContract.java:292) at org.hibernate.internal.AbstractSharedSessionContract.(AbstractSharedSessionContract.java:193) at org.hibernate.internal.SessionImpl.(SessionImpl.java:230) at org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl.openSession(SessionFactoryImpl.java:1381) at org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl.openSession(SessionFactoryImpl.java:1247) at io.quarkus.hibernate.orm.runtime.session.JTASessionOpener.openSession(JTASessionOpener.java:46) at io.quarkus.hibernate.orm.runtime.session.TransactionScopedSession.acquireSession(TransactionScopedSession.java:92) at io.quarkus.hibernate.orm.runtime.session.TransactionScopedSession.find(TransactionScopedSession.java:175) at org.hibernate.engine.spi.SessionLazyDelegator.find(SessionLazyDelegator.java:825) at org.hibernate.Session_OpdLahisOZ9nWRPXMsEFQmQU03A_Synthetic_ClientProxy.find(Unknown Source) at io.quarkus.hibernate.orm.panache.common.runtime.AbstractJpaOperations.findById(AbstractJpaOperations.java:183) at org.acme.timetable.persistence.PlannedLessonRepository.findById(PlannedLessonRepository.java) at org.acme.timetable.persistence.PlannedLessonRepository.findById(PlannedLessonRepository.java) at org.acme.timetable.persistence.PlannedLessonRepository.save(PlannedLessonRepository.java:24) at org.acme.timetable.persistence.PlannedLessonRepository_Subclass.save$$superforward(Unknown Source) at org.acme.timetable.persistence.PlannedLessonRepository_Subclass$$function$$1.apply(Unknown Source) at io.quarkus.arc.impl.AroundInvokeInvocationContext.proceed(AroundInvokeInvocationContext.java:73) at io.quarkus.arc.impl.AroundInvokeInvocationContext.proceed(AroundInvokeInvocationContext.java:62) at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorBase.invokeInOurTx(TransactionalInterceptorBase.java:136) at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorBase.invokeInOurTx(TransactionalInterceptorBase.java:107) at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorRequired.doIntercept(TransactionalInterceptorRequired.java:38) at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorBase.intercept(TransactionalInterceptorBase.java:61) at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorRequired.intercept(TransactionalInterceptorRequired.java:32) at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorRequired_Bean.intercept(Unknown Source) at io.quarkus.arc.impl.InterceptorInvocation.invoke(InterceptorInvocation.java:42) at io.quarkus.arc.impl.AroundInvokeInvocationContext.perform(AroundInvokeInvocationContext.java:30) at io.quarkus.arc.impl.InvocationContexts.performAroundInvoke(InvocationContexts.java:27) at org.acme.timetable.persistence.PlannedLessonRepository_Subclass.save(Unknown Source) at org.acme.timetable.persistence.PlannedLessonRepository_ClientProxy.save(Unknown Source) at org.acme.timetable.rest.TimeTableResource.lambda$solve$2(TimeTableResource.java:112) at ai.timefold.solver.core.impl.solver.ConsumerSupport.lambda$consumeFinalBestSolution$1(ConsumerSupport.java:102) at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539) at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) at java.base/java.lang.Thread.run(Thread.java:833)
The best solution consumer, final best solution consumer, and exception handler are run in a different thread than the one accepting the API call. As a result, the transaction's context is not propagated.
What you can do is use QuarkusTransaction to start and end the transaction in the final best solution consumer (and remove the @Transactional
annotation).
solverManager.solveBuilder()
.withProblemId(finalRequestId)
.withProblemFinder((id) -> timeTableJob.get(finalRequestId).timetable)
.withBestSolutionConsumer(bestSolution -> timeTableJob.put(finalRequestId, Job.ofTimetable(bestSolution)))
.withFinalBestSolutionConsumer(solution -> {
QuarkusTransaction.joiningExisting().run(() -> {
timeTableJob.put(finalRequestId,Job.ofTimetable(solution));
plannedLessonRepository.save(solution);
});
})
.withExceptionHandler((id, exception) -> {
timeTableJob.put(finalRequestId, Job.ofException(exception));
LOGGER.error("Failed solving jobId ({}).", finalRequestId, exception);
})
.run();