TransactionRollbackError発生時のROLLBACK文を省略
COMMIT時にTransactionRollbackErrorが発生した場合、ActiveRecordは不要なROLLBACK文の実行を省略するようになりました。これによりPostgreSQLのlibpqから直接出力される「WARNING: there is no transaction in progress」という警告ログを防ぎます。
背景
Amazon Aurora DSQLのような並行トランザクションの競合が頻繁に発生するPostgreSQL互換データベースでは、SerializationFailure(TransactionRollbackErrorの一種)が日常的に発生します。この状況は通常のPostgreSQLのSERIALIZABLE分離レベルと同様ですが、既存の実装ではCOMMIT失敗後にROLLBACKを実行しようとしていました。
データベースエンジンはCOMMITの失敗時に既にトランザクションをロールバックしているため、その後のROLLBACK文は「進行中のトランザクションがありません」という警告を生成します。rails/rails内のSERIALIZABLEトランザクションのテストでは、この警告をclient_min_messagesで抑制していましたが、本番環境のワークロードでログ抑制は望ましくありません。
技術的な変更
トランザクション無効化の追加
activerecord/lib/active_record/connection_adapters/abstract/transaction.rbのwithin_new_transactionメソッドに、TransactionRollbackErrorを捕捉する処理が追加されました。
変更後:
rescue ActiveRecord::TransactionRollbackError
unless transaction.state.completed?
transaction.invalidate!
rollback_transaction(transaction)
end
raise
COMMIT中にTransactionRollbackErrorが発生すると、トランザクションを無効化(invalidate!)してから通常のロールバック処理を実行します。この時点でトランザクションは無効化されているため、RealTransaction#rollback内のrollback_db_transaction呼び出しはスキップされます。
ROLLBACK文の条件付き実行
RealTransaction#rollbackメソッドが、トランザクションの無効化状態を確認するように変更されました。
変更前:
def rollback
if materialized?
connection.rollback_db_transaction
connection.reset_isolation_level if isolation_level
end
@state.full_rollback!
end
変更後:
def rollback
if materialized?
connection.rollback_db_transaction unless @state.invalidated?
connection.reset_isolation_level if isolation_level
end
@state.full_rollback!
end
@state.invalidated?がtrueの場合、データベースへのROLLBACK文の送信を省略します。トランザクションの状態管理は通常通りfull_rollback!で完了します。
テストコードの簡素化
警告抑制が不要になったため、activerecord/test/cases/adapters/postgresql/transaction_test.rbとtransaction_nested_test.rbからwith_warning_suppressionブロックが削除されました。SERIALIZABLEトランザクションのデッドロックやシリアライゼーション失敗のテストケースは、警告なしで実行できるようになっています。
設計判断
トランザクション無効化のタイミング
既存の無効化ロジックは使用できませんでした。6a8a90eでCOMMIT発行前にトランザクションが@stateからポップされるため、current_transactionはNullTransactionを返します。この制約により、TransactionRollbackErrorの捕捉時に明示的に無効化する必要がありました。
MySQL互換性の維持
PostgreSQLはセーブポイントのロールバックを許可しますが、MySQLはシリアライゼーション失敗やデッドロック発生後にセーブポイントのロールバックを処理できません。本PRの変更はRealTransaction(トップレベルトランザクション)のみに影響し、MySQLのセーブポイント制約(#30922で導入)には干渉しません。
まとめ
本PRは、データベースエンジンによって既にロールバックされたトランザクションに対する冗長なROLLBACK文を省略する変更です。TransactionRollbackError発生時のトランザクション無効化により、PostgreSQL特有の警告ログを根本的に解決し、本番環境でのログ品質を向上させています。