You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Timefold 1.15+Quarkus3.1求解时上下文缺失致多租户存储失败

问题:Timefold求解过程中多租户上下文丢失导致数据库保存失败

使用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提供上下文传播扩展,可自动将主线程的租户、身份等上下文传递到异步线程:

  1. 添加Maven依赖:
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-context-propagation</artifactId>
</dependency>
  1. 确保项目中的租户上下文实现支持上下文传播(或使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 22:47:32