Timefold 1.15+Quarkus3.1求解时上下文缺失致多租户存储失败
使用Timefold 1.15 + Quarkus 3.1开发时间表求解功能时,在求解完成后调用Repository保存最优解,出现多租户上下文缺失的问题,报错提示未指定租户ID。
示例代码
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();
错误日志
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)
解决方案
1. 手动传递租户上下文到求解线程
Timefold默认用独立线程池执行求解任务,主线程的租户上下文(通常存在ThreadLocal中)不会自动传递到异步线程。需要提前捕获当前租户ID,在保存逻辑中手动设置:
// 调用solveBuilder前获取当前租户ID String currentTenantId = TenantContext.getCurrentTenantId(); // 替换为项目实际获取租户ID的方法 // 修改FinalBestSolutionConsumer .withFinalBestSolutionConsumer(solution -> { timeTableJob.put(finalRequestId, Job.ofTimetable(solution)); // 手动设置租户上下文 TenantContext.setCurrentTenantId(currentTenantId); try { plannedLessonRepository.save(solution); } finally { // 清理上下文,避免线程复用导致的上下文污染 TenantContext.clear(); } })
2. 使用Quarkus上下文传播扩展自动传递
Quarkus提供上下文传播扩展,可自动将主线程的租户、身份等上下文传递到异步线程:
- 添加Maven依赖:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-context-propagation</artifactId> </dependency>
- 确保项目中的租户上下文实现支持上下文传播(或使用Quarkus内置多租户机制),启用后Timefold异步任务会自动继承主线程上下文,无需手动设置。
3. 手动管理带租户的EntityManager
如果上述方法不生效,可直接创建指定租户的EntityManager执行保存:
.withFinalBestSolutionConsumer(solution -> { timeTableJob.put(finalRequestId, Job.ofTimetable(solution)); String currentTenantId = TenantContext.getCurrentTenantId(); // 创建带租户ID的EntityManager EntityManager em = entityManagerFactory.createEntityManager( Map.of("hibernate.tenant_identifier", currentTenantId) ); try (em) { em.getTransaction().begin(); solution.getPlannedLessons().forEach(em::merge); em.getTransaction().commit(); } })
核心原因
Timefold求解线程属于独立线程池,主线程存储在ThreadLocal中的租户上下文不会自动传递到异步线程。Hibernate创建Session时无法获取租户ID,因此抛出异常。
内容的提问来源于stack exchange,提问作者Amro Mustafa

