diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 09977bc4..e96db0e1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,8 +26,12 @@ jobs: ruby: head - activerecord: 8.0 ruby: 3.1 + - activerecord: 8.0 + ruby: head - activerecord: head ruby: 3.1 + - activerecord: head + ruby: head env: BUNDLE_GEMFILE: "${{ github.workspace }}/gemfiles/activerecord_${{ matrix.activerecord }}.gemfile" diff --git a/CHANGELOG.md b/CHANGELOG.md index 034ea0b9..9cc02a49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Unreleased +# 4.5.2 (Jun, 2026) +* Use concrete Trilogy connection exception classes in retry configuration. +* Use a retriable-compatible effectively unbounded max elapsed time value. + # 4.5.1 (Jul, 2025) * Create update before insert trigger diff --git a/Gemfile.lock b/Gemfile.lock index 6e8cd32b..67976c0a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - lhm-shopify (4.5.1) + lhm-shopify (4.5.2) retriable (>= 3.0.0) GEM diff --git a/gemfiles/activerecord_7.2.gemfile.lock b/gemfiles/activerecord_7.2.gemfile.lock index addbd25e..57700f3f 100644 --- a/gemfiles/activerecord_7.2.gemfile.lock +++ b/gemfiles/activerecord_7.2.gemfile.lock @@ -1,7 +1,7 @@ PATH remote: .. specs: - lhm-shopify (4.5.1) + lhm-shopify (4.5.2) retriable (>= 3.0.0) GEM diff --git a/gemfiles/activerecord_8.0.gemfile.lock b/gemfiles/activerecord_8.0.gemfile.lock index 8217011a..4b67a09a 100644 --- a/gemfiles/activerecord_8.0.gemfile.lock +++ b/gemfiles/activerecord_8.0.gemfile.lock @@ -1,7 +1,7 @@ PATH remote: .. specs: - lhm-shopify (4.5.1) + lhm-shopify (4.5.2) retriable (>= 3.0.0) GEM diff --git a/gemfiles/activerecord_head.gemfile.lock b/gemfiles/activerecord_head.gemfile.lock index 3fb89740..4a8ea557 100644 --- a/gemfiles/activerecord_head.gemfile.lock +++ b/gemfiles/activerecord_head.gemfile.lock @@ -26,7 +26,7 @@ GIT PATH remote: .. specs: - lhm-shopify (4.5.1) + lhm-shopify (4.5.2) retriable (>= 3.0.0) GEM diff --git a/lib/lhm/invoker.rb b/lib/lhm/invoker.rb index c9d3484a..6b9f5f74 100644 --- a/lib/lhm/invoker.rb +++ b/lib/lhm/invoker.rb @@ -65,7 +65,11 @@ def run end def triggers_still_exist?(conn, entangler) - triggers = conn.select_values("SHOW TRIGGERS LIKE '%#{migrator.origin.name}'").select { |name| name =~ /^lhmt/ } + triggers = conn.select_values( + "SHOW TRIGGERS LIKE '%#{migrator.origin.name}'", + should_retry: true, + log_prefix: "Invoker" + ).select { |name| name =~ /^lhmt/ } triggers.sort == entangler.expected_triggers.sort end diff --git a/lib/lhm/sql_retry.rb b/lib/lhm/sql_retry.rb index 152edc2b..cf73e314 100644 --- a/lib/lhm/sql_retry.rb +++ b/lib/lhm/sql_retry.rb @@ -158,7 +158,7 @@ def default_retry_config base_interval: 1, # the initial interval in seconds between tries. tries: 20, # Number of attempts to make at running your code block (includes initial attempt). rand_factor: 0, # percentage to randomize the next retry interval time - max_elapsed_time: Float::INFINITY, # max total time in seconds that code is allowed to keep being retried + max_elapsed_time: Float::MAX, # max total time in seconds that code is allowed to keep being retried on_retry: Proc.new do |exception, try_number, total_elapsed_time, next_interval| log_with_prefix("#{exception.class}: '#{exception.message}' - #{try_number} tries in #{total_elapsed_time} seconds and #{next_interval} seconds until the next try.", :error) end @@ -198,15 +198,27 @@ def retriable_trilogy_errors /connection is locked to hostgroup/, /The MySQL server is running with the --read-only option so it cannot execute this statement/, ], - Trilogy::ConnectionError => nil, - Trilogy::TimeoutError => nil, } + retriable_trilogy_connection_error_classes.each do |error_class| + errors[error_class] = nil + end + if ActiveRecord::VERSION::STRING >= "7.1" errors[ActiveRecord::ConnectionFailed] = nil end errors end + + def retriable_trilogy_connection_error_classes + errors = [] + errors << Trilogy::BaseConnectionError if defined?(Trilogy::BaseConnectionError) + errors << Trilogy::SSLError if defined?(Trilogy::SSLError) + errors << Trilogy::ConnectionClosed if defined?(Trilogy::ConnectionClosed) + errors.concat(Trilogy::SyscallError::ERRORS.values) if defined?(Trilogy::SyscallError::ERRORS) + + errors.uniq.select { |error_class| error_class.is_a?(Class) && error_class <= Exception } + end end end diff --git a/lib/lhm/version.rb b/lib/lhm/version.rb index 850d0334..1be7453c 100644 --- a/lib/lhm/version.rb +++ b/lib/lhm/version.rb @@ -2,5 +2,5 @@ # Schmidt module Lhm - VERSION = '4.5.1' + VERSION = '4.5.2' end diff --git a/spec/integration/lhm_spec.rb b/spec/integration/lhm_spec.rb index 747d88e1..539e33c4 100644 --- a/spec/integration/lhm_spec.rb +++ b/spec/integration/lhm_spec.rb @@ -646,11 +646,13 @@ 50.times { |n| execute("insert into users set reference = '#{ n }'") } insert = Thread.new do + connection = new_mysql_connection 10.times do |n| - connect_master! - execute("insert into users set reference = '#{ 100 + n }'") + connection.query("insert into users set reference = '#{ 100 + n }'") sleep(0.17) end + ensure + connection&.close end sleep 2 @@ -670,10 +672,13 @@ 50.times { |n| execute("insert into users set reference = '#{ n }'") } delete = Thread.new do + connection = new_mysql_connection 10.times do |n| - execute("delete from users where reference = '#{ n }'") + connection.query("delete from users where reference = '#{ n }'") sleep(0.17) end + ensure + connection&.close end sleep 2 diff --git a/spec/unit/sql_retry_spec.rb b/spec/unit/sql_retry_spec.rb new file mode 100644 index 00000000..d6ff5b5e --- /dev/null +++ b/spec/unit/sql_retry_spec.rb @@ -0,0 +1,35 @@ +require 'minitest/autorun' +require 'minitest/spec' +require 'active_record' +require 'lhm/sql_retry' +require 'trilogy' + +describe Lhm::SqlRetry do + describe '#default_retry_config' do + it 'uses an effectively unbounded max elapsed time value compatible with retriable validation' do + retry_config = Lhm::SqlRetry.new(nil).send(:default_retry_config) + + assert_equal Float::MAX, retry_config[:max_elapsed_time] + end + end + + describe '#retriable_trilogy_errors' do + it 'only uses exception classes as retriable keys' do + retry_errors = Lhm::SqlRetry.new(nil).send(:retriable_trilogy_errors) + + retry_errors.each_key do |error_class| + assert error_class.is_a?(Class), "#{error_class.inspect} is not a class" + assert error_class <= Exception, "#{error_class.inspect} is not an exception class" + end + end + + it 'matches trilogy connection error classes without using the ConnectionError module' do + retry_errors = Lhm::SqlRetry.new(nil).send(:retriable_trilogy_errors) + + refute_includes retry_errors.keys, Trilogy::ConnectionError + assert_includes retry_errors.keys, Trilogy::BaseConnectionError + assert_includes retry_errors.keys, Trilogy::ConnectionClosed + assert_includes retry_errors.keys, Trilogy::SSLError + end + end +end