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): 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