From 7d45d813b20d0640ab4d2cf66224046b7685f062 Mon Sep 17 00:00:00 2001 From: abicky Date: Sun, 16 Aug 2026 12:32:55 +0900 Subject: [PATCH 1/2] Fix ActiveRecord::ReadOnlyError with Active Record 8.1 This uses a dedicated connection role for the low-privilege MySQL user instead of the reading role. The deadlock test needs to execute SELECT ... FOR UPDATE, which Active Record 8.1 rejects for connections configured as the reading role. Active Record 8.1 prevents pessimistic locking while using the reading role: https://github.com/rails/rails/pull/54580 --- .../connection_adapters/abstract_mysql_adapter_spec.rb | 4 +--- spec/spec_helper.rb | 10 +++++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/spec/activerecord/debug_errors/ext/connection_adapters/abstract_mysql_adapter_spec.rb b/spec/activerecord/debug_errors/ext/connection_adapters/abstract_mysql_adapter_spec.rb index 03d0dfd..cf66ff7 100644 --- a/spec/activerecord/debug_errors/ext/connection_adapters/abstract_mysql_adapter_spec.rb +++ b/spec/activerecord/debug_errors/ext/connection_adapters/abstract_mysql_adapter_spec.rb @@ -59,9 +59,7 @@ def cause_deadlock(role:) context "when the user doesn't have the permission to execute 'SHOW ENGINE INNODB STATUS'" do it "displays an error message" do expect { - ActiveRecord::Base.connected_to(role: :reading) do - cause_deadlock(role: :reading) - end + cause_deadlock(role: :restricted) }.to raise_error(ActiveRecord::Deadlocked) expect(log.string).to include("Failed to execute") end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 534d250..fa85908 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -29,11 +29,11 @@ }, } - user_for_replica = 'activerecord-debug_errors' + restricted_user = 'activerecord-debug_errors' ActiveRecord::Base.configurations = { default_env: { primary: base_db_config, - primary_replica: base_db_config.merge(username: user_for_replica, replica: true), + restricted: base_db_config.merge(username: restricted_user), } } @@ -48,7 +48,7 @@ class ApplicationRecord < ActiveRecord::Base self.abstract_class = true - connects_to database: { writing: :primary, reading: :primary_replica } + connects_to database: { writing: :primary, restricted: :restricted } end class User < ApplicationRecord; end @@ -56,10 +56,10 @@ class User < ApplicationRecord; end User.find_or_create_by!(name: 'bar') ActiveRecord::Base.connection.execute(<<~SQL) - CREATE USER IF NOT EXISTS '#{user_for_replica}'@'%' IDENTIFIED BY '#{ENV['MYSQL_PASSWORD']}' + CREATE USER IF NOT EXISTS '#{restricted_user}'@'%' IDENTIFIED BY '#{ENV['MYSQL_PASSWORD']}' SQL ActiveRecord::Base.connection.execute(<<~SQL) - GRANT SELECT, LOCK TABLES ON *.* To '#{user_for_replica}'@'%' + GRANT SELECT, LOCK TABLES ON *.* To '#{restricted_user}'@'%' SQL end end From 85b4ba10d90f9f0d1ca5677634107cacc0ce240f Mon Sep 17 00:00:00 2001 From: abicky Date: Sun, 16 Aug 2026 16:06:02 +0900 Subject: [PATCH 2/2] Add "Prerequisites" to README --- README.md | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 69aa4ad..95f16f8 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,33 @@ Or install it yourself as: $ gem install activerecord-debug_errors +## Prerequisites + +### MySQL permissions + +To collect complete MySQL diagnostics, the account used by Active Record needs the +global [`PROCESS`](https://dev.mysql.com/doc/refman/8.4/en/privileges-provided.html#priv_process) +privilege: + +```sql +GRANT PROCESS ON *.* TO 'application_user'@'application_host'; +``` + +The gem runs `SHOW ENGINE INNODB STATUS` for lock wait timeouts and deadlocks. +MySQL requires `PROCESS` to execute this statement. Without the privilege, the +gem logs the resulting permission error instead of the InnoDB diagnostic +section. + +For lock wait timeouts, the gem also runs `SHOW FULL PROCESSLIST`. This +statement works without `PROCESS`, but only shows threads owned by the current +MySQL account. With `PROCESS`, it shows threads for all accounts, which may be +necessary to identify the session holding a lock. + +Because `PROCESS` can expose statements executed by other users on the same +server, grant it only when the additional diagnostic visibility is acceptable. +The privilege is global and cannot be limited to the application's database. + + ## Usage You only have to load the gem: @@ -118,8 +145,6 @@ Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 *** WE ROLL BACK TRANSACTION (2) ``` -Note that the user requires the PROCESS priviledge to collect the information. - ### ActiveRecord::ConnectionTimeoutError When `ActiveRecord::ConnectionTimeoutError` occurs, you can see the information of connection owners (threads):