quarkusoptaplannertimefold

Timefold context propagation during solving


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)


Solution

  • 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();