升级至Rails 7.2后RSpec线程内启动嵌套事务时挂起
Rails 7.2测试挂起:线程内嵌套事务死锁的原因解析
问题重现
升级到Rails 7.2.2.1后,这段包含线程内嵌套事务的测试会永久冻结:
context 'error', :transactionless do let(:concurrent) do Thread.new do FlowLead.transaction do puts 'inside of the transaction' end end end it 'should not hang' do puts 'starting outer transaction' FlowLead.transaction do puts 'starting thread' concurrent.join puts 'after inner transaction' end end end shared_context 'transactionless' do uses_transaction(nil) after { DatabaseCleaner.clean_with(:deletion) } end
测试输出停在starting thread,Postgres的pg_stat_activity显示连接处于idle in transaction状态,最后执行的语句是RELEASE SAVEPOINT active_record_1。关闭RSpec的config.use_transactional_fixtures = false后,测试恢复正常。
根本原因
1. Rails 7.2的连接线程绑定强化
Rails 7.2对数据库连接的线程隔离做了严格限制:
- 每个数据库连接只能被一个线程持有和操作
- 嵌套事务依赖Postgres的保存点实现,而保存点必须在当前连接的事务上下文中执行
2. 测试中的循环等待死锁
你的测试代码触发了典型死锁场景:
- 主线程开启
FlowLead.transaction,占用一个数据库连接并进入事务状态 - 主线程调用
concurrent.join,卡住等待子线程执行完毕 - 子线程启动后,因为
uses_transaction(nil)的配置,测试框架没有为它分配独立连接,只能尝试复用主线程的连接 - Postgres不允许同一连接被多线程同时操作,子线程被阻塞等待连接释放;主线程又在等子线程完成,形成循环等待,最终测试永久挂起
3. 关闭事务性fixtures的修复逻辑
当设置config.use_transactional_fixtures = false后:
- RSpec不再强制为测试创建全局事务,也不会共享数据库连接
- 主线程和子线程会从连接池各自获取独立连接
- 两个线程的事务操作完全隔离,不会出现连接争抢,死锁自然解除
佐证信息
从pg_stat_activity的输出能直接验证问题:
- 只有一个活跃连接,说明主线程和子线程在争抢同一个连接资源
- 连接状态为
idle in transaction,说明主线程持有事务但未继续执行,正等待子线程,而子线程根本无法获取连接执行事务
内容的提问来源于stack exchange,提问作者Carlos Martinez
相关产品推荐
相关产品推荐

