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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +20 -18
- data/Appraisals +8 -19
- data/CHANGELOG.md +16 -0
- data/Gemfile.lock +37 -20
- data/README.md +21 -14
- data/dev.yml +12 -8
- data/docker-compose-mysql-5.7.yml +1 -0
- data/docker-compose-mysql-8.0.yml +63 -0
- data/docker-compose.yml +5 -3
- data/gemfiles/activerecord_6.1.gemfile +1 -0
- data/gemfiles/activerecord_6.1.gemfile.lock +23 -13
- data/gemfiles/{activerecord_7.0.0.alpha2.gemfile → activerecord_7.0.gemfile} +2 -1
- data/gemfiles/{activerecord_7.0.0.alpha2.gemfile.lock → activerecord_7.0.gemfile.lock} +29 -19
- data/gemfiles/{activerecord_6.0.gemfile → activerecord_7.1.gemfile} +1 -1
- data/gemfiles/activerecord_7.1.gemfile.lock +83 -0
- data/lhm.gemspec +2 -1
- data/lib/lhm/atomic_switcher.rb +3 -3
- data/lib/lhm/chunker.rb +4 -4
- data/lib/lhm/connection.rb +9 -1
- data/lib/lhm/sql_helper.rb +1 -1
- data/lib/lhm/sql_retry.rb +36 -18
- data/lib/lhm/table.rb +3 -4
- data/lib/lhm/throttler/replica_lag.rb +166 -0
- data/lib/lhm/throttler/slave_lag.rb +5 -155
- data/lib/lhm/throttler/threads_running.rb +3 -1
- data/lib/lhm/throttler.rb +7 -3
- data/lib/lhm/version.rb +1 -1
- data/scripts/helpers/wait-for-dbs.sh +3 -3
- data/scripts/mysql/writer/create_users.sql +1 -1
- data/spec/.lhm.example +1 -1
- data/spec/README.md +8 -9
- data/spec/integration/atomic_switcher_spec.rb +6 -10
- data/spec/integration/chunk_insert_spec.rb +2 -2
- data/spec/integration/chunker_spec.rb +54 -44
- data/spec/integration/database.yml +4 -4
- data/spec/integration/entangler_spec.rb +4 -4
- data/spec/integration/integration_helper.rb +23 -15
- data/spec/integration/lhm_spec.rb +70 -44
- data/spec/integration/locked_switcher_spec.rb +2 -2
- data/spec/integration/proxysql_spec.rb +10 -10
- data/spec/integration/sql_retry/db_connection_helper.rb +2 -4
- data/spec/integration/sql_retry/lock_wait_spec.rb +7 -8
- data/spec/integration/sql_retry/lock_wait_timeout_test_helper.rb +18 -10
- data/spec/integration/sql_retry/proxysql_helper.rb +1 -1
- data/spec/integration/sql_retry/retry_with_proxysql_spec.rb +1 -2
- data/spec/integration/table_spec.rb +1 -1
- data/spec/integration/toxiproxy_helper.rb +1 -1
- data/spec/test_helper.rb +27 -3
- data/spec/unit/atomic_switcher_spec.rb +2 -2
- data/spec/unit/chunker_spec.rb +43 -43
- data/spec/unit/connection_spec.rb +2 -2
- data/spec/unit/entangler_spec.rb +14 -24
- data/spec/unit/printer_spec.rb +2 -6
- data/spec/unit/sql_helper_spec.rb +2 -2
- data/spec/unit/throttler/{slave_lag_spec.rb → replica_lag_spec.rb} +84 -92
- data/spec/unit/throttler/threads_running_spec.rb +18 -0
- data/spec/unit/throttler_spec.rb +8 -8
- metadata +26 -12
- data/.travis.yml +0 -21
- data/gemfiles/activerecord_5.2.gemfile +0 -9
- data/gemfiles/activerecord_5.2.gemfile.lock +0 -65
- 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 (
|
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.
|
11
|
-
activesupport (= 7.0.
|
12
|
-
activerecord (7.0.
|
13
|
-
activemodel (= 7.0.
|
14
|
-
activesupport (= 7.0.
|
15
|
-
|
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.
|
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.
|
29
|
+
concurrent-ruby (1.2.2)
|
27
30
|
docile (1.4.0)
|
28
|
-
i18n (1.
|
31
|
+
i18n (1.14.1)
|
29
32
|
concurrent-ruby (~> 1.0)
|
30
|
-
minitest (5.
|
31
|
-
mocha (1.
|
32
|
-
|
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
|
-
|
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.
|
41
|
-
thor (1.
|
42
|
-
toxiproxy (2.0.
|
43
|
-
|
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.
|
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
|
@@ -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 = '>=
|
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'
|
data/lib/lhm/atomic_switcher.rb
CHANGED
@@ -26,14 +26,14 @@ module Lhm
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def atomic_switch
|
29
|
-
"
|
30
|
-
"`#{ @destination.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 }`
|
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.
|
83
|
-
unless
|
84
|
-
m = "Unexpected warning found for inserted row: #{
|
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 = "
|
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
|
data/lib/lhm/connection.rb
CHANGED
@@ -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
|
data/lib/lhm/sql_helper.rb
CHANGED
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.
|
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
|
141
|
+
rescue ActiveRecord::ConnectionNotEstablished
|
144
142
|
# Retry if ActiveRecord cannot reach host
|
145
|
-
next
|
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
|
-
|
43
|
-
|
44
|
-
@connection.
|
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
|