lhm-shopify 3.5.5 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +20 -18
  3. data/Appraisals +8 -19
  4. data/CHANGELOG.md +16 -0
  5. data/Gemfile.lock +37 -20
  6. data/README.md +21 -14
  7. data/dev.yml +12 -8
  8. data/docker-compose-mysql-5.7.yml +1 -0
  9. data/docker-compose-mysql-8.0.yml +63 -0
  10. data/docker-compose.yml +5 -3
  11. data/gemfiles/activerecord_6.1.gemfile +1 -0
  12. data/gemfiles/activerecord_6.1.gemfile.lock +23 -13
  13. data/gemfiles/{activerecord_7.0.0.alpha2.gemfile → activerecord_7.0.gemfile} +2 -1
  14. data/gemfiles/{activerecord_7.0.0.alpha2.gemfile.lock → activerecord_7.0.gemfile.lock} +29 -19
  15. data/gemfiles/{activerecord_6.0.gemfile → activerecord_7.1.gemfile} +1 -1
  16. data/gemfiles/activerecord_7.1.gemfile.lock +83 -0
  17. data/lhm.gemspec +2 -1
  18. data/lib/lhm/atomic_switcher.rb +3 -3
  19. data/lib/lhm/chunker.rb +4 -4
  20. data/lib/lhm/connection.rb +9 -1
  21. data/lib/lhm/sql_helper.rb +1 -1
  22. data/lib/lhm/sql_retry.rb +36 -18
  23. data/lib/lhm/table.rb +3 -4
  24. data/lib/lhm/throttler/replica_lag.rb +166 -0
  25. data/lib/lhm/throttler/slave_lag.rb +5 -155
  26. data/lib/lhm/throttler/threads_running.rb +3 -1
  27. data/lib/lhm/throttler.rb +7 -3
  28. data/lib/lhm/version.rb +1 -1
  29. data/scripts/helpers/wait-for-dbs.sh +3 -3
  30. data/scripts/mysql/writer/create_users.sql +1 -1
  31. data/spec/.lhm.example +1 -1
  32. data/spec/README.md +8 -9
  33. data/spec/integration/atomic_switcher_spec.rb +6 -10
  34. data/spec/integration/chunk_insert_spec.rb +2 -2
  35. data/spec/integration/chunker_spec.rb +54 -44
  36. data/spec/integration/database.yml +4 -4
  37. data/spec/integration/entangler_spec.rb +4 -4
  38. data/spec/integration/integration_helper.rb +23 -15
  39. data/spec/integration/lhm_spec.rb +70 -44
  40. data/spec/integration/locked_switcher_spec.rb +2 -2
  41. data/spec/integration/proxysql_spec.rb +10 -10
  42. data/spec/integration/sql_retry/db_connection_helper.rb +2 -4
  43. data/spec/integration/sql_retry/lock_wait_spec.rb +7 -8
  44. data/spec/integration/sql_retry/lock_wait_timeout_test_helper.rb +18 -10
  45. data/spec/integration/sql_retry/proxysql_helper.rb +1 -1
  46. data/spec/integration/sql_retry/retry_with_proxysql_spec.rb +1 -2
  47. data/spec/integration/table_spec.rb +1 -1
  48. data/spec/integration/toxiproxy_helper.rb +1 -1
  49. data/spec/test_helper.rb +27 -3
  50. data/spec/unit/atomic_switcher_spec.rb +2 -2
  51. data/spec/unit/chunker_spec.rb +43 -43
  52. data/spec/unit/connection_spec.rb +2 -2
  53. data/spec/unit/entangler_spec.rb +14 -24
  54. data/spec/unit/printer_spec.rb +2 -6
  55. data/spec/unit/sql_helper_spec.rb +2 -2
  56. data/spec/unit/throttler/{slave_lag_spec.rb → replica_lag_spec.rb} +84 -92
  57. data/spec/unit/throttler/threads_running_spec.rb +18 -0
  58. data/spec/unit/throttler_spec.rb +8 -8
  59. metadata +26 -12
  60. data/.travis.yml +0 -21
  61. data/gemfiles/activerecord_5.2.gemfile +0 -9
  62. data/gemfiles/activerecord_5.2.gemfile.lock +0 -65
  63. data/gemfiles/activerecord_6.0.gemfile.lock +0 -67
@@ -1,54 +1,63 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- lhm-shopify (3.5.5)
4
+ lhm-shopify (4.1.0)
5
5
  retriable (>= 3.0.0)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activemodel (7.0.0.alpha2)
11
- activesupport (= 7.0.0.alpha2)
12
- activerecord (7.0.0.alpha2)
13
- activemodel (= 7.0.0.alpha2)
14
- activesupport (= 7.0.0.alpha2)
15
- activesupport (7.0.0.alpha2)
10
+ activemodel (7.0.8)
11
+ activesupport (= 7.0.8)
12
+ activerecord (7.0.8)
13
+ activemodel (= 7.0.8)
14
+ activesupport (= 7.0.8)
15
+ activerecord-trilogy-adapter (3.1.2)
16
+ activerecord (>= 6.0.a, < 7.1.a)
17
+ trilogy (>= 2.4.0)
18
+ activesupport (7.0.8)
16
19
  concurrent-ruby (~> 1.0, >= 1.0.2)
17
20
  i18n (>= 1.6, < 2)
18
21
  minitest (>= 5.1)
19
22
  tzinfo (~> 2.0)
20
23
  after_do (0.4.0)
21
- appraisal (2.4.1)
24
+ appraisal (2.5.0)
22
25
  bundler
23
26
  rake
24
27
  thor (>= 0.14.0)
25
28
  byebug (11.1.3)
26
- concurrent-ruby (1.1.9)
29
+ concurrent-ruby (1.2.2)
27
30
  docile (1.4.0)
28
- i18n (1.8.11)
31
+ i18n (1.14.1)
29
32
  concurrent-ruby (~> 1.0)
30
- minitest (5.14.4)
31
- mocha (1.13.0)
32
- mysql2 (0.5.3)
33
+ minitest (5.20.0)
34
+ mocha (2.1.0)
35
+ ruby2_keywords (>= 0.0.5)
36
+ mysql2 (0.5.5)
33
37
  rake (13.0.6)
34
38
  retriable (3.1.2)
35
- simplecov (0.21.2)
39
+ ruby2_keywords (0.0.5)
40
+ simplecov (0.22.0)
36
41
  docile (~> 1.1)
37
42
  simplecov-html (~> 0.11)
38
43
  simplecov_json_formatter (~> 0.1)
39
44
  simplecov-html (0.12.3)
40
- simplecov_json_formatter (0.1.3)
41
- thor (1.1.0)
42
- toxiproxy (2.0.0)
43
- tzinfo (2.0.4)
45
+ simplecov_json_formatter (0.1.4)
46
+ thor (1.2.2)
47
+ toxiproxy (2.0.2)
48
+ trilogy (2.6.0)
49
+ tzinfo (2.0.6)
44
50
  concurrent-ruby (~> 1.0)
45
51
 
46
52
  PLATFORMS
53
+ arm64-darwin-21
54
+ arm64-darwin-22
47
55
  x86_64-darwin-20
48
56
  x86_64-linux
49
57
 
50
58
  DEPENDENCIES
51
- activerecord (= 7.0.0.alpha2)
59
+ activerecord (= 7.0.8)
60
+ activerecord-trilogy-adapter
52
61
  after_do
53
62
  appraisal
54
63
  byebug
@@ -59,6 +68,7 @@ DEPENDENCIES
59
68
  rake
60
69
  simplecov
61
70
  toxiproxy
71
+ trilogy
62
72
 
63
73
  BUNDLED WITH
64
74
  2.2.22
@@ -2,6 +2,6 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "activerecord", "6.0.0"
5
+ gem "activerecord", "7.1.1"
6
6
 
7
7
  gemspec path: "../"
@@ -0,0 +1,83 @@
1
+ PATH
2
+ remote: ..
3
+ specs:
4
+ lhm-shopify (4.1.0)
5
+ retriable (>= 3.0.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (7.1.1)
11
+ activesupport (= 7.1.1)
12
+ activerecord (7.1.1)
13
+ activemodel (= 7.1.1)
14
+ activesupport (= 7.1.1)
15
+ timeout (>= 0.4.0)
16
+ activesupport (7.1.1)
17
+ base64
18
+ bigdecimal
19
+ concurrent-ruby (~> 1.0, >= 1.0.2)
20
+ connection_pool (>= 2.2.5)
21
+ drb
22
+ i18n (>= 1.6, < 2)
23
+ minitest (>= 5.1)
24
+ mutex_m
25
+ tzinfo (~> 2.0)
26
+ after_do (0.4.0)
27
+ appraisal (2.5.0)
28
+ bundler
29
+ rake
30
+ thor (>= 0.14.0)
31
+ base64 (0.1.1)
32
+ bigdecimal (3.1.4)
33
+ byebug (11.1.3)
34
+ concurrent-ruby (1.2.2)
35
+ connection_pool (2.4.1)
36
+ docile (1.4.0)
37
+ drb (2.1.1)
38
+ ruby2_keywords
39
+ i18n (1.14.1)
40
+ concurrent-ruby (~> 1.0)
41
+ minitest (5.20.0)
42
+ mocha (2.1.0)
43
+ ruby2_keywords (>= 0.0.5)
44
+ mutex_m (0.1.2)
45
+ mysql2 (0.5.5)
46
+ rake (13.0.6)
47
+ retriable (3.1.2)
48
+ ruby2_keywords (0.0.5)
49
+ simplecov (0.22.0)
50
+ docile (~> 1.1)
51
+ simplecov-html (~> 0.11)
52
+ simplecov_json_formatter (~> 0.1)
53
+ simplecov-html (0.12.3)
54
+ simplecov_json_formatter (0.1.4)
55
+ thor (1.2.2)
56
+ timeout (0.4.0)
57
+ toxiproxy (2.0.2)
58
+ trilogy (2.6.0)
59
+ tzinfo (2.0.6)
60
+ concurrent-ruby (~> 1.0)
61
+
62
+ PLATFORMS
63
+ arm64-darwin-21
64
+ arm64-darwin-22
65
+ x86_64-darwin-20
66
+ x86_64-linux
67
+
68
+ DEPENDENCIES
69
+ activerecord (= 7.1.1)
70
+ after_do
71
+ appraisal
72
+ byebug
73
+ lhm-shopify!
74
+ minitest
75
+ mocha
76
+ mysql2
77
+ rake
78
+ simplecov
79
+ toxiproxy
80
+ trilogy
81
+
82
+ BUNDLED WITH
83
+ 2.2.22
data/lhm.gemspec CHANGED
@@ -21,7 +21,7 @@ Gem::Specification.new do |s|
21
21
  s.executables = []
22
22
  s.metadata['allowed_push_host'] = "https://rubygems.org"
23
23
 
24
- s.required_ruby_version = '>= 2.3.0'
24
+ s.required_ruby_version = '>= 3.0.0'
25
25
 
26
26
  s.add_dependency 'retriable', '>= 3.0.0'
27
27
 
@@ -31,6 +31,7 @@ Gem::Specification.new do |s|
31
31
  s.add_development_dependency 'after_do'
32
32
  s.add_development_dependency 'rake'
33
33
  s.add_development_dependency 'mysql2'
34
+ s.add_development_dependency 'trilogy'
34
35
  s.add_development_dependency 'simplecov'
35
36
  s.add_development_dependency 'toxiproxy'
36
37
  s.add_development_dependency 'appraisal'
@@ -26,14 +26,14 @@ module Lhm
26
26
  end
27
27
 
28
28
  def atomic_switch
29
- "rename table `#{ @origin.name }` to `#{ @migration.archive_name }`, " \
30
- "`#{ @destination.name }` to `#{ @origin.name }`"
29
+ "RENAME TABLE `#{ @origin.name }` TO `#{ @migration.archive_name }`, " \
30
+ "`#{ @destination.name }` TO `#{ @origin.name }`"
31
31
  end
32
32
 
33
33
  def validate
34
34
  unless @connection.data_source_exists?(@origin.name) &&
35
35
  @connection.data_source_exists?(@destination.name)
36
- error "`#{ @origin.name }` and `#{ @destination.name }` must exist"
36
+ error "`#{ @origin.name }` AND `#{ @destination.name }` MUST EXIST"
37
37
  end
38
38
  end
39
39
 
data/lib/lhm/chunker.rb CHANGED
@@ -79,9 +79,9 @@ module Lhm
79
79
  private
80
80
 
81
81
  def raise_on_non_pk_duplicate_warning
82
- @connection.execute("show warnings", should_retry: true, log_prefix: LOG_PREFIX).each do |level, code, message|
83
- unless message.match?(/Duplicate entry .+ for key 'PRIMARY'/)
84
- m = "Unexpected warning found for inserted row: #{message}"
82
+ @connection.select_all("SHOW WARNINGS", should_retry: true, log_prefix: LOG_PREFIX).each do |row|
83
+ unless row["Message"].match?(/Duplicate entry .+ for key 'PRIMARY'/)
84
+ m = "Unexpected warning found for inserted row: #{row["Message"]}"
85
85
  Lhm.logger.warn(m)
86
86
  raise Error.new(m) if @raise_on_warnings
87
87
  end
@@ -100,7 +100,7 @@ module Lhm
100
100
  end
101
101
 
102
102
  def upper_id(next_id, stride)
103
- sql = "select id from `#{ @migration.origin_name }` where id >= #{ next_id } order by id limit 1 offset #{ stride - 1}"
103
+ sql = "SELECT id FROM `#{ @migration.origin_name }` WHERE id >= #{ next_id } ORDER BY id LIMIT 1 OFFSET #{ stride - 1}"
104
104
  top = @connection.select_value(sql, should_retry: true, log_prefix: LOG_PREFIX)
105
105
 
106
106
  [top ? top.to_i : @limit, @limit].min
@@ -75,6 +75,14 @@ module Lhm
75
75
  end
76
76
  end
77
77
 
78
+ def select_all(query, should_retry: false, log_prefix: nil)
79
+ if should_retry
80
+ exec_with_retries(:select_all, query, log_prefix)
81
+ else
82
+ exec(:select_all, query)
83
+ end
84
+ end
85
+
78
86
  private
79
87
 
80
88
  def exec(method, sql)
@@ -105,4 +113,4 @@ module Lhm
105
113
  lhm_stack.at(first_candidate_index)
106
114
  end
107
115
  end
108
- end
116
+ end
@@ -34,7 +34,7 @@ module Lhm
34
34
 
35
35
  def column_definition(cols)
36
36
  Array(cols).map do |column|
37
- column.to_s.match(/`?([^\(]+)`?(\([^\)]+\))?/).captures
37
+ column.to_s.match(/`?([^\(\s]+)`?\s*(\([^\)]+\))?/).captures
38
38
  end
39
39
  end
40
40
 
data/lib/lhm/sql_retry.rb CHANGED
@@ -105,9 +105,7 @@ module Lhm
105
105
  def mysql_single_value(name)
106
106
  query = Lhm::ProxySQLHelper.tagged("SELECT #{name} LIMIT 1")
107
107
 
108
- @connection.execute(query).to_a.first.tap do |record|
109
- return record&.first
110
- end
108
+ @connection.select_value(query)
111
109
  end
112
110
 
113
111
  def same_host_as_initial?
@@ -140,32 +138,22 @@ module Lhm
140
138
  log_with_prefix("Reconnected to wrong host. Started migration on: #{@initial_hostname} (server_id: #{@initial_server_id}), but reconnected to: #{hostname} (server_id: #{server_id}).", :error)
141
139
  return false
142
140
  end
143
- rescue StandardError => e
141
+ rescue ActiveRecord::ConnectionNotEstablished
144
142
  # Retry if ActiveRecord cannot reach host
145
- next if /Lost connection to MySQL server at 'reading initial communication packet'/ === e.message
143
+ next
144
+ rescue StandardError => e
146
145
  log_with_prefix("Encountered error: [#{e.class}] #{e.message}. Will stop reconnection procedure.", :info)
147
146
  return false
148
147
  end
149
148
  end
149
+
150
150
  false
151
151
  end
152
152
 
153
153
  # For a full list of configuration options see https://github.com/kamui/retriable
154
154
  def default_retry_config
155
155
  {
156
- on: {
157
- StandardError => [
158
- /Lock wait timeout exceeded/,
159
- /Timeout waiting for a response from the last query/,
160
- /Deadlock found when trying to get lock/,
161
- /Query execution was interrupted/,
162
- /Lost connection to MySQL server during query/,
163
- /Max connect timeout reached/,
164
- /Unknown MySQL server host/,
165
- /connection is locked to hostgroup/,
166
- /The MySQL server is running with the --read-only option so it cannot execute this statement/,
167
- ]
168
- },
156
+ on: retriable_mysql2_errors || retriable_trilogy_errors,
169
157
  multiplier: 1, # each successive interval grows by this factor
170
158
  base_interval: 1, # the initial interval in seconds between tries.
171
159
  tries: 20, # Number of attempts to make at running your code block (includes initial attempt).
@@ -176,5 +164,35 @@ module Lhm
176
164
  end
177
165
  }.freeze
178
166
  end
167
+
168
+ def retriable_mysql2_errors
169
+ return unless defined?(Mysql2::Error)
170
+
171
+ {
172
+ StandardError => [
173
+ /Lock wait timeout exceeded/,
174
+ /Timeout waiting for a response from the last query/,
175
+ /Deadlock found when trying to get lock/,
176
+ /Query execution was interrupted/,
177
+ /Lost connection to MySQL server during query/,
178
+ /Max connect timeout reached/,
179
+ /Unknown MySQL server host/,
180
+ /connection is locked to hostgroup/,
181
+ /The MySQL server is running with the --read-only option so it cannot execute this statement/,
182
+ ],
183
+ }
184
+ end
185
+
186
+ def retriable_trilogy_errors
187
+ return unless defined?(Trilogy::BaseError)
188
+
189
+ {
190
+ ActiveRecord::StatementInvalid => [
191
+ # Those sometimes appear as Trilogy::TimeoutError, and sometimes as ActiveRecord::StatementInvalid
192
+ /Lock wait timeout exceeded/,
193
+ ],
194
+ Trilogy::ConnectionError => nil,
195
+ }
196
+ end
179
197
  end
180
198
  end
data/lib/lhm/table.rb CHANGED
@@ -39,10 +39,9 @@ module Lhm
39
39
  end
40
40
 
41
41
  def ddl
42
- sql = "show create table `#{ @table_name }`"
43
- specification = nil
44
- @connection.execute(sql).each { |row| specification = row.last }
45
- specification
42
+ query = "SHOW CREATE TABLE #{ @connection.quote_table_name(@table_name) }"
43
+
44
+ @connection.select_one(query)["Create Table"]
46
45
  end
47
46
 
48
47
  def parse
@@ -0,0 +1,166 @@
1
+ module Lhm
2
+ module Throttler
3
+
4
+ def self.format_hosts(hosts)
5
+ formatted_hosts = []
6
+ hosts.each do |host|
7
+ if host && !host.match(/localhost/) && !host.match(/127.0.0.1/)
8
+ formatted_hosts << host.partition(':')[0]
9
+ end
10
+ end
11
+ formatted_hosts
12
+ end
13
+
14
+ class ReplicaLag
15
+ include Command
16
+
17
+ INITIAL_TIMEOUT = 0.1
18
+ DEFAULT_STRIDE = 2_000
19
+ DEFAULT_MAX_ALLOWED_LAG = 10
20
+
21
+ MAX_TIMEOUT = INITIAL_TIMEOUT * 1024
22
+
23
+ attr_accessor :timeout_seconds, :allowed_lag, :stride, :connection
24
+
25
+ def initialize(options = {})
26
+ @timeout_seconds = INITIAL_TIMEOUT
27
+ @stride = options[:stride] || DEFAULT_STRIDE
28
+ @allowed_lag = options[:allowed_lag] || DEFAULT_MAX_ALLOWED_LAG
29
+ @replicas = {}
30
+ @get_config = options[:current_config]
31
+ @check_only = options[:check_only]
32
+ end
33
+
34
+ def execute
35
+ sleep(throttle_seconds)
36
+ end
37
+
38
+ private
39
+
40
+ def throttle_seconds
41
+ lag = max_current_replica_lag
42
+
43
+ if lag > @allowed_lag && @timeout_seconds < MAX_TIMEOUT
44
+ Lhm.logger.info("Increasing timeout between strides from #{@timeout_seconds} to #{@timeout_seconds * 2} because #{lag} seconds of replica lag detected is greater than the maximum of #{@allowed_lag} seconds allowed.")
45
+ @timeout_seconds = @timeout_seconds * 2
46
+ elsif lag <= @allowed_lag && @timeout_seconds > INITIAL_TIMEOUT
47
+ Lhm.logger.info("Decreasing timeout between strides from #{@timeout_seconds} to #{@timeout_seconds / 2} because #{lag} seconds of replica lag detected is less than or equal to the #{@allowed_lag} seconds allowed.")
48
+ @timeout_seconds = @timeout_seconds / 2
49
+ else
50
+ @timeout_seconds
51
+ end
52
+ end
53
+
54
+ def replicas
55
+ @replicas[@connection] ||= get_replicas
56
+ end
57
+
58
+ def get_replicas
59
+ replicas = []
60
+ if @check_only.nil? or !@check_only.respond_to?(:call)
61
+ replica_hosts = master_replica_hosts
62
+ while replica_hosts.any? do
63
+ host = replica_hosts.pop
64
+ replica = Replica.new(host, @get_config)
65
+ if !replicas.map(&:host).include?(host) && replica.connection
66
+ replicas << replica
67
+ replica_hosts.concat(replica.replica_hosts)
68
+ end
69
+ end
70
+ else
71
+ replica_config = @check_only.call
72
+ replicas << Replica.new(replica_config['host'], @get_config)
73
+ end
74
+ replicas
75
+ end
76
+
77
+ def master_replica_hosts
78
+ Throttler.format_hosts(@connection.select_values(Replica::SQL_SELECT_REPLICA_HOSTS))
79
+ end
80
+
81
+ def max_current_replica_lag
82
+ max = replicas.map { |replica| replica.lag }.push(0).max
83
+ Lhm.logger.info "Max current replica lag: #{max}"
84
+ max
85
+ end
86
+ end
87
+
88
+ class Replica
89
+ SQL_SELECT_REPLICA_HOSTS = "SELECT host FROM information_schema.processlist WHERE command LIKE 'Binlog Dump%'"
90
+ SQL_SELECT_MAX_REPLICA_LAG = 'SHOW SLAVE STATUS'
91
+
92
+ attr_reader :host, :connection
93
+
94
+ def self.client
95
+ defined?(Mysql2::Client) ? Mysql2::Client : Trilogy
96
+ end
97
+
98
+ def self.client_error
99
+ defined?(Mysql2::Error) ? Mysql2::Error : Trilogy::Error
100
+ end
101
+
102
+ def initialize(host, connection_config = nil)
103
+ @host = host
104
+ @connection_config = prepare_connection_config(connection_config)
105
+ @connection = client(@connection_config)
106
+ end
107
+
108
+ def replica_hosts
109
+ Throttler.format_hosts(query_connection(SQL_SELECT_REPLICA_HOSTS, 'host'))
110
+ end
111
+
112
+ def lag
113
+ query_connection(SQL_SELECT_MAX_REPLICA_LAG, 'Seconds_Behind_Master').first.to_i
114
+ end
115
+
116
+ private
117
+
118
+ def client(config)
119
+ Lhm.logger.info "Connecting to #{@host} on database: #{config[:database]}"
120
+ self.class.client.new(config)
121
+ rescue self.class.client_error => e
122
+ Lhm.logger.info "Error connecting to #{@host}: #{e}"
123
+ nil
124
+ end
125
+
126
+ def prepare_connection_config(config_proc)
127
+ config = if config_proc
128
+ if config_proc.respond_to?(:call) # if we get a proc
129
+ config_proc.call
130
+ else
131
+ raise ArgumentError, "Expected #{config_proc.inspect} to respond to `call`"
132
+ end
133
+ else
134
+ db_config
135
+ end
136
+ config.deep_symbolize_keys!
137
+ config[:host] = @host
138
+ config
139
+ end
140
+
141
+ def query_connection(query, result)
142
+ @connection.query(query).map { |row| row[result] }
143
+ rescue self.class.client_error => e
144
+ Lhm.logger.info "Unable to connect and/or query #{host}: #{e}"
145
+ [nil]
146
+ end
147
+
148
+ private
149
+
150
+ def db_config
151
+ if ar_supports_db_config?
152
+ ActiveRecord::Base.connection_pool.db_config.configuration_hash.dup
153
+ else
154
+ ActiveRecord::Base.connection_pool.spec.config.dup
155
+ end
156
+ end
157
+
158
+ def ar_supports_db_config?
159
+ # https://api.rubyonrails.org/v6.0/classes/ActiveRecord/ConnectionAdapters/ConnectionPool.html <-- has spec
160
+ # vs
161
+ # https://api.rubyonrails.org/v6.1/classes/ActiveRecord/ConnectionAdapters/ConnectionPool.html <-- has db_config
162
+ ActiveRecord::VERSION::MAJOR > 6 || ActiveRecord::VERSION::MAJOR == 6 && ActiveRecord::VERSION::MINOR >= 1
163
+ end
164
+ end
165
+ end
166
+ end