ActiveRecord异常跳过内层rescue被外层捕获问题排查
问题根因及修复方案
核心根因
- 内层异常捕获范围存在严重漏判:你只把
reconnect!调用放在了内层begin-rescue块中,但前置的ActiveRecord::Base.connection.active?判断、甚至ActiveRecord::Base.connection获取连接的动作本身,都可能在数据库完全停摆、连接池无可用连接、TCP连接被系统回收等场景下直接抛出ActiveRecord::ActiveRecordError或PG::Error。这类异常发生时根本不会进入if块内的重连逻辑,自然也不会触发你预期的抛出RuntimeError触发重试的逻辑,会直接向上逃逸到外层的异常捕获块。 - 日志顺序错位导致误判:你看到的“剩余3个任务输出重连日志后直接输出持久化失败日志”是多线程环境下日志缓冲导致的顺序错乱。这3个任务根本没有进入if块执行重连逻辑——它们在执行if条件判断、获取数据库连接的阶段就已经抛出了数据库异常,你看到的重连日志是第一个任务或其他并发线程提前输出、因为IO缓冲延迟刷出的,和后续3个任务的持久化失败日志交错在了一起。这也和你观察到的“业务逻辑日志、副作用完全未触发”完全吻合:异常发生在业务逻辑执行之前,根本没有走到业务代码段。
active?方法本身不可靠:PostgreSQL适配器的active?方法仅能做基础的连接存活检查,在连接处于半断开、连接池已标记连接为死亡但连接对象未同步状态等场景下,要么会误判连接状态,要么会直接抛出驱动层异常,无法作为执行业务前的可靠校验依据。- 连接池并发逻辑放大问题:第一个任务重连失败后,ActiveRecord连接池会自动清理失效连接,后续并发任务在从连接池获取连接时,会直接触发新连接创建流程,这个流程的异常发生在if条件求值阶段,完全绕过了你内层的异常捕获。
修复方案
- 扩大内层异常捕获范围,把所有连接相关操作都包裹在begin-rescue块内,不要让任何数据库调用逃逸到捕获逻辑外:
# Model class WorkReceipt < ActiveRecord::Base def self.do_work begin unless ActiveRecord::Base.connection.active? Rails.logger.error("DB connection is inactive. Reconnecting...") ActiveRecord::Base.connection.reconnect! end rescue ActiveRecord::ActiveRecordError, PG::Error => e Rails.logger.error("Could not reestablish connection: #{e}") raise RuntimeError, "Could not connect to database" end # Lots of hard work self.create!( # Some args ) end end
- 外层增加业务执行标记位,从逻辑上避免“业务未执行就被判定为成功”的问题,不要依赖异常类型判断业务执行状态:
# Service def process business_executed = false begin WorkReceipt.do_work business_executed = true rescue ActiveRecord::ActiveRecordError, PG::Error if business_executed Rails.logger.error("Work was done successfully, but not persisted") else Rails.logger.error("DB error occurred before business execution, trigger retry") raise # 重新抛出异常触发PubSub重试 end end end
- 尽量不要手写连接检查逻辑,ActiveRecord自带
connection.verify!方法,会自动完成连接存活校验、坏连接重连的工作,失败时会抛出标准的连接异常,可靠性远高于自定义的active?判断。
内容的提问来源于stack exchange,提问作者Jess The Witch
相关产品推荐
相关产品推荐

