Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Ruby 3.2, 3.3 and Rails 7.1 #3

Merged
merged 9 commits into from
May 26, 2024

Conversation

ohbarye
Copy link
Contributor

@ohbarye ohbarye commented May 26, 2024

Hi @abicky ,

Changes

My first motivation is to run tests against Ruby 3.2 & 3.3. But I found that this gem doesn't work with Rails 7.1, so I created some patches for that and added version combinations to the test matrix.

Thoughts

I wonder if it's okay to remove 2.6 to 3.0 support since they are already EOL.
https://endoflife.date/ruby

```
  1) ActiveRecord::DebugErrors::DisplayConnectionOwners#execute when ActiveRecord::Deadlocked occurs when the user has the permission to execute 'SHOW ENGINE INNODB STATUS' displays latest detected deadlock
     Failure/Error: ActiveRecord::Base.legacy_connection_handling = false

     NoMethodError:
       undefined method `legacy_connection_handling=' for class ActiveRecord::Base
     # ./vendor/bundle/ruby/3.3.0/gems/activerecord-7.1.3.3/lib/active_record/dynamic_matchers.rb:22:in `method_missing'
     # ./spec/spec_helper.rb:29:in `block (2 levels) in <top (required)>'
```
@ohbarye
Copy link
Contributor Author

ohbarye commented May 26, 2024

Oops, I found the latest Rails breaks something... Let me check the CI failures.

…ecute`

Starting with Rails 7.1, `raw_execute` may not be called when executing a query. This pull request introduces `with_raw_connection`, and this method is now called when executing a query.

https://github.com/rails/rails/pull/44576/files#diff-460f4e7973c5dd945c51d24df5b0173961190d3645f4e2585fd3003fa1fc0ff7R865

Overriding this method and calling super will result in a LocalJumpError, which is not safe.

In this commit, I tried to call the log by hooking into `translate_exception_class`. This method has been around since 4.2, and there should be no changes to the interface since 6.0.

rails/rails@5e5118a#diff-460f4e7973c5dd945c51d24df5b0173961190d3645f4e2585fd3003fa1fc0ff7R356
`acquire_connection` in connection_pool is now smarter in Rails 7.1.3. Even when running existing tests in Rails7.1.3, `ActiveRecord::ConnectionTimeoutError` no longer occurs because a connection can be obtained by `try_to_checkout_new_connection` after reap.

https://github.com/rails/rails/compare/v7.1.2..v7.1.3#diff-642b90553b888bd2c724c093a1a685a5408a7d8293f3751366c25dc548936eb7R660
@ohbarye ohbarye force-pushed the run-test-against-newer-rubies branch from bf0b8b7 to 6214941 Compare May 26, 2024 11:22
@ohbarye
Copy link
Contributor Author

ohbarye commented May 26, 2024

Although the diff got larger than I expected, I was finally able to pass test for all supported versions: 6.0, 6,1, 7.0, 7.1.

See passed tests here: https://github.com/ohbarye/activerecord-debug_errors/actions/runs/9242922830

@ohbarye ohbarye changed the title Run test against Ruby 3.2, 3.3 Support Ruby 3.2, 3.3 and Rails 7.1 May 26, 2024
raise
end
private method_name if method_name == :raw_execute
elsif ActiveRecord.version >= Gem::Version.new("7.1.0")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As commented in 5f9a8c9, there is a case when neither raw_execute nor execute is called in Rails 7.1.

I'm not sure if this way is the best, I tried to use translate_exception_class as a hook for Rails 7.1.

Copy link
Owner

@abicky abicky May 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the following approach is similar to the original one and more readable. What do you think about them?

diff --git a/lib/activerecord/debug_errors/ext/connection_adapters/abstract_mysql_adapter.rb b/lib/activerecord/debug_errors/ext/connection_adapters/abstract_mysql_adapter.rb
index 77f3f25..680e2e6 100644
--- a/lib/activerecord/debug_errors/ext/connection_adapters/abstract_mysql_adapter.rb
+++ b/lib/activerecord/debug_errors/ext/connection_adapters/abstract_mysql_adapter.rb
@@ -80,6 +80,12 @@ module ActiveRecord
   end
 end

+ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.descendants.each do |adapter|
+  adapter.prepend(ActiveRecord::DebugErrors::DisplayMySQLInformation)
+end
 class ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
-  prepend ActiveRecord::DebugErrors::DisplayMySQLInformation
+  def self.inherited(base)
+    super
+    base.prepend(ActiveRecord::DebugErrors::DisplayMySQLInformation)
+  end
 end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the code you presented is quite neat!

I think, I'm probably misunderstanding something big (and my implementation has based on the misunderstanding), but could you please explain to me why your code still works in Rails 7.1 for further studies? (and why the original code doesn't work.)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you can see below, ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#raw_execute is not implemented, whereas the subclasses have the method raw_execute, so appending the module to the subclasses works expectedly:

% bundle exec irb
irb(main):001> require "active_record"
=> true
irb(main):002> require "active_record/connection_adapters/abstract_mysql_adapter"
=> true
irb(main):003> require "active_record/connection_adapters/mysql2_adapter"
=> true
irb(main):004> require "active_record/connection_adapters/trilogy_adapter"
=> true
irb(main):005> show_source ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#raw_execute

From: /Users/arabiki/ghq/src/github.com/abicky/activerecord-debug_errors/gemfiles/vendor/bundle/ruby/3.3.0/gems/activerecord-7.1.3.3/lib/active_record/connection_adapters/abstract/database_statements.rb:530

        def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
          raise NotImplementedError
        end

=> nil
irb(main):006> show_source ActiveRecord::ConnectionAdapters::Mysql2Adapter#raw_execute

From: /Users/arabiki/ghq/src/github.com/abicky/activerecord-debug_errors/gemfiles/vendor/bundle/ruby/3.3.0/gems/activerecord-7.1.3.3/lib/active_record/connection_adapters/mysql2/database_statements.rb:96

          def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
            log(sql, name, async: async) do
              with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
                sync_timezone_changes(conn)
                result = conn.query(sql)
                verified!
                handle_warnings(sql)
                result
              end
            end
          end

=> nil
irb(main):007> show_source ActiveRecord::ConnectionAdapters::TrilogyAdapter#raw_execute

From: /Users/arabiki/ghq/src/github.com/abicky/activerecord-debug_errors/gemfiles/vendor/bundle/ruby/3.3.0/gems/activerecord-7.1.3.3/lib/active_record/connection_adapters/trilogy/database_statements.rb:44

          def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
            log(sql, name, async: async) do
              with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
                sync_timezone_changes(conn)
                result = conn.query(sql)
                verified!
                handle_warnings(sql)
                result
              end
            end
          end

=> nil

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I got it now. Thanks for your explanation!

Thread.new { sleep 10 } # another thread

expect {
ActiveRecord::Base.connection # Ensure to acquire a connection
Array.new(ActiveRecord::Base.connection_pool.size) do
Thread.new do
ActiveRecord::Base.connection_pool.checkout(0.1)
ActiveRecord::Base.connection_pool.checkout
sleep 0.001
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Connection pool handling in Rails 7.1 is so smarter that the existing test cannot cause ConnectionTimeoutError. To cause it, I tweaked the code.

ref: 1688bd0

Copy link
Owner

@abicky abicky May 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid that this test becomes flaky because some threads might end before the last thread calls checkout. How about the following changes?

diff --git a/spec/activerecord/debug_errors/ext/connection_adapters/connection_pool_spec.rb b/spec/activerecord/debug_errors/ext/connection_adapters/connection_pool_spec.rb
index 7a86bc7..6f2b1fe 100644
--- a/spec/activerecord/debug_errors/ext/connection_adapters/connection_pool_spec.rb
+++ b/spec/activerecord/debug_errors/ext/connection_adapters/connection_pool_spec.rb
@@ -20,11 +20,20 @@ RSpec.describe ActiveRecord::DebugErrors::DisplayConnectionOwners do
     it "displays connection owners and other threads" do
       Thread.new { sleep 10 } # another thread

+      mutex = Mutex.new
+      cv = ConditionVariable.new
+
       expect {
         ActiveRecord::Base.connection # Ensure to acquire a connection
         Array.new(ActiveRecord::Base.connection_pool.size) do
           Thread.new do
-            ActiveRecord::Base.connection_pool.checkout(0.1)
+            mutex.synchronize do
+              ActiveRecord::Base.connection_pool.checkout(0.1)
+              cv.wait(mutex, 1)
+            rescue
+              cv.broadcast
+              raise
+            end
           end
         end.each(&:join)
       }.to raise_error(ActiveRecord::ConnectionTimeoutError)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly. And to be honest, I couldn't think of any other way to do it, so I'm glad to know the cool way you suggested it. Updated d2a4654

# For compatibility. Rails deprecated since 6.1 and removed this option since 7.1.
# https://github.com/rails/rails/pull/44827/commits/ad52c0a19714a1b87a7d0c626a8b364cf95414cf
if ActiveRecord::Base.respond_to?(:legacy_connection_handling)
ActiveRecord::Base.legacy_connection_handling = false
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is simply broken in Rails 7.1.

Copy link
Owner

@abicky abicky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your pull request! I appreciate your contribution.
I've checked the changes and made some comments.

raise
end
private method_name if method_name == :raw_execute
elsif ActiveRecord.version >= Gem::Version.new("7.1.0")
Copy link
Owner

@abicky abicky May 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the following approach is similar to the original one and more readable. What do you think about them?

diff --git a/lib/activerecord/debug_errors/ext/connection_adapters/abstract_mysql_adapter.rb b/lib/activerecord/debug_errors/ext/connection_adapters/abstract_mysql_adapter.rb
index 77f3f25..680e2e6 100644
--- a/lib/activerecord/debug_errors/ext/connection_adapters/abstract_mysql_adapter.rb
+++ b/lib/activerecord/debug_errors/ext/connection_adapters/abstract_mysql_adapter.rb
@@ -80,6 +80,12 @@ module ActiveRecord
   end
 end

+ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.descendants.each do |adapter|
+  adapter.prepend(ActiveRecord::DebugErrors::DisplayMySQLInformation)
+end
 class ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
-  prepend ActiveRecord::DebugErrors::DisplayMySQLInformation
+  def self.inherited(base)
+    super
+    base.prepend(ActiveRecord::DebugErrors::DisplayMySQLInformation)
+  end
 end

Thread.new { sleep 10 } # another thread

expect {
ActiveRecord::Base.connection # Ensure to acquire a connection
Array.new(ActiveRecord::Base.connection_pool.size) do
Thread.new do
ActiveRecord::Base.connection_pool.checkout(0.1)
ActiveRecord::Base.connection_pool.checkout
sleep 0.001
Copy link
Owner

@abicky abicky May 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid that this test becomes flaky because some threads might end before the last thread calls checkout. How about the following changes?

diff --git a/spec/activerecord/debug_errors/ext/connection_adapters/connection_pool_spec.rb b/spec/activerecord/debug_errors/ext/connection_adapters/connection_pool_spec.rb
index 7a86bc7..6f2b1fe 100644
--- a/spec/activerecord/debug_errors/ext/connection_adapters/connection_pool_spec.rb
+++ b/spec/activerecord/debug_errors/ext/connection_adapters/connection_pool_spec.rb
@@ -20,11 +20,20 @@ RSpec.describe ActiveRecord::DebugErrors::DisplayConnectionOwners do
     it "displays connection owners and other threads" do
       Thread.new { sleep 10 } # another thread

+      mutex = Mutex.new
+      cv = ConditionVariable.new
+
       expect {
         ActiveRecord::Base.connection # Ensure to acquire a connection
         Array.new(ActiveRecord::Base.connection_pool.size) do
           Thread.new do
-            ActiveRecord::Base.connection_pool.checkout(0.1)
+            mutex.synchronize do
+              ActiveRecord::Base.connection_pool.checkout(0.1)
+              cv.wait(mutex, 1)
+            rescue
+              cv.broadcast
+              raise
+            end
           end
         end.each(&:join)
       }.to raise_error(ActiveRecord::ConnectionTimeoutError)

Copy link
Owner

@abicky abicky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, thank you for your contribution! Your investigation helped a lot.

I wonder if it's okay to remove 2.6 to 3.0 support since they are already EOL.

I think we can stop supporting the versions but it might be better not to deal with the issue on this PR.

@abicky abicky merged commit 78fe080 into abicky:master May 26, 2024
22 checks passed
@abicky
Copy link
Owner

abicky commented May 26, 2024

I've released v0.1.3.

@ohbarye ohbarye deleted the run-test-against-newer-rubies branch May 27, 2024 00:37
@ohbarye
Copy link
Contributor Author

ohbarye commented May 27, 2024

I think we can stop supporting the versions but it might be better not to deal with the issue on this PR.

Agree.

Thanks for your quick review and release too!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants