单元测试链式调用2个Uni使用Panache.withTransaction()报TimeoutException
问题根因
抛出TimeoutException和事务执行耗时无关,是响应式上下文用法错误导致事件循环线程被阻塞,事务任务永远得不到调度:
- Hibernate Reactive Panache的所有数据库操作必须运行在Vert.x事件循环线程上,测试中频繁调用
.await().indefinitely()会直接阻塞当前线程,如果当前线程恰好是事件循环线程,提交到事件循环的事务任务会一直排队,直到触发默认5秒的超时阈值抛出异常 - 你在
Panache.withTransaction()外部提前初始化了insertKline、getOhlcList这类Uni实例,响应式流创建时就绑定了当前的非事务上下文,后续即使放到事务Supplier里执行,也无法加入当前开启的事务,会因为找不到活动事务会话一直挂起 - 你提前调用
.await().indefinitely()查询currencyPairEntity的操作,已经在事务开启前阻塞了执行线程,后续手动调用runOnContext()切换上下文也无法解决线程被占住的问题
解决方案
优先使用Quarkus官方提供的测试响应式事务注解,无需手动管理事务状态、无需手动标记回滚,也不需要自行阻塞等待结果,框架会自动处理上下文调度和测试完成后的事务回滚:
@QuarkusTest @Slf4j class KlineServiceTest { @Inject KlineRepository klineRepository; @Inject CurrencyPairRepository currencyPairRepository; @Inject KlineService service; @Test @DisplayName("ohlc matches inserted kline") @TestReactiveTransaction(rollback = true) // 测试方法自动包裹在回滚事务中,测试结束自动清理数据 Uni<Void> ohlcMatchesInsertedKline() { // 测试方法直接返回响应式Uni,Quarkus测试框架会自动订阅执行,无需手动阻塞 // GIVEN val volume = BigDecimal.valueOf(1d); val closeTime = LocalDateTime.now().withSecond(0).withNano(0); val currencyPair = new CurrencyPair("BTC", "USDT"); // 链式拼接所有数据库操作,全程不提前阻塞 return currencyPairRepository.findOrCreate(currencyPair) .chain(currencyPairEntity -> { val kline = KlineEntity.builder() .id(new KlineId(currencyPairEntity, closeTime)) .volume(volume) .build(); return klineRepository.persistAndFlush(kline); }) .chain(() -> service.listOhlcByCurrencyPairAndTimeWindow(currencyPair, ofMinutes(5))) // 断言直接写在响应式链路中 .invoke(ohlcList -> { assertThat(ohlcList).hasSize(1); val ohlc = ohlcList.get(0); assertThat(ohlc).extracting(Ohlc::getCloseTime, Ohlc::getVolume) .containsExactly(closeTime, volume); }); } }
如果必须手动控制事务逻辑,需遵守两个规则:
- 所有需要加入事务的响应式Uni,必须在
Panache.withTransaction()的Supplier内部创建,禁止提前在事务外初始化绑定非事务上下文 - 禁止在Vert.x事件循环线程上调用
.await().indefinitely()这类阻塞方法,阻塞操作需调度到Quarkus的阻塞工作线程池执行
内容的提问来源于stack exchange,提问作者Frédéric Thomas
相关产品推荐
相关产品推荐

