lhm-shopify 3.5.4 → 4.0.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 +6 -6
- data/Appraisals +8 -13
- data/CHANGELOG.md +13 -0
- data/Gemfile.lock +22 -20
- data/README.md +14 -7
- data/dev.yml +12 -8
- data/docker-compose.yml +2 -0
- data/gemfiles/activerecord_6.0.gemfile +1 -1
- data/gemfiles/activerecord_6.0.gemfile.lock +25 -21
- data/gemfiles/activerecord_6.1.gemfile.lock +17 -13
- data/gemfiles/{activerecord_7.0.0.alpha2.gemfile → activerecord_7.0.gemfile} +1 -1
- data/gemfiles/{activerecord_7.0.0.alpha2.gemfile.lock → activerecord_7.0.gemfile.lock} +23 -19
- data/gemfiles/{activerecord_5.2.gemfile → activerecord_7.1.0.beta1.gemfile} +1 -3
- data/gemfiles/activerecord_7.1.0.beta1.gemfile.lock +81 -0
- data/lhm.gemspec +1 -1
- data/lib/lhm/sql_helper.rb +1 -1
- data/lib/lhm/sql_retry.rb +37 -47
- data/lib/lhm/throttler/replica_lag.rb +162 -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/spec/.lhm.example +1 -1
- data/spec/README.md +8 -9
- data/spec/integration/atomic_switcher_spec.rb +2 -2
- data/spec/integration/chunk_insert_spec.rb +2 -2
- data/spec/integration/chunker_spec.rb +33 -38
- data/spec/integration/database.yml +1 -1
- data/spec/integration/entangler_spec.rb +4 -4
- data/spec/integration/integration_helper.rb +12 -12
- data/spec/integration/lhm_spec.rb +41 -32
- data/spec/integration/locked_switcher_spec.rb +2 -2
- data/spec/integration/sql_retry/retry_with_proxysql_spec.rb +6 -5
- data/spec/integration/toxiproxy_helper.rb +1 -1
- data/spec/test_helper.rb +3 -0
- 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} +79 -79
- data/spec/unit/throttler/threads_running_spec.rb +18 -0
- data/spec/unit/throttler_spec.rb +8 -8
- metadata +10 -9
- data/gemfiles/activerecord_5.2.gemfile.lock +0 -65
data/lib/lhm/sql_retry.rb
CHANGED
@@ -18,6 +18,12 @@ module Lhm
|
|
18
18
|
class SqlRetry
|
19
19
|
RECONNECT_SUCCESSFUL_MESSAGE = "LHM successfully reconnected to initial host:"
|
20
20
|
CLOUDSQL_VERSION_COMMENT = "(Google)"
|
21
|
+
# Will retry for 120 seconds (approximately, since connecting takes time).
|
22
|
+
RECONNECT_RETRY_MAX_ITERATION = 120
|
23
|
+
RECONNECT_RETRY_INTERVAL = 1
|
24
|
+
# Will abort the LHM if it had to reconnect more than 25 times in a single run (indicator that there might be
|
25
|
+
# something wrong with the network and would be better to run the LHM at a later time).
|
26
|
+
RECONNECTION_MAXIMUM = 25
|
21
27
|
|
22
28
|
MYSQL_VAR_NAMES = {
|
23
29
|
hostname: "@@global.hostname",
|
@@ -25,9 +31,6 @@ module Lhm
|
|
25
31
|
version_comment: "@@version_comment",
|
26
32
|
}
|
27
33
|
|
28
|
-
# This internal error is used to trigger retries from the parent Retriable.retriable in #with_retries
|
29
|
-
class ReconnectToHostSuccessful < Lhm::Error; end
|
30
|
-
|
31
34
|
def initialize(connection, retry_options: {}, reconnect_with_consistent_host: false)
|
32
35
|
@connection = connection
|
33
36
|
self.retry_config = retry_options
|
@@ -38,11 +41,14 @@ module Lhm
|
|
38
41
|
def with_retries(log_prefix: nil)
|
39
42
|
@log_prefix = log_prefix || "" # No prefix. Just logs
|
40
43
|
|
44
|
+
# Amount of time LHM had to reconnect. Aborting if more than RECONNECTION_MAXIMUM
|
45
|
+
reconnection_counter = 0
|
46
|
+
|
41
47
|
Retriable.retriable(@retry_config) do
|
42
48
|
# Using begin -> rescue -> end for Ruby 2.4 compatibility
|
43
49
|
begin
|
44
50
|
if @reconnect_with_consistent_host
|
45
|
-
raise Lhm::Error.new("
|
51
|
+
raise Lhm::Error.new("MySQL host has changed since the start of the LHM. Aborting to avoid data-loss") unless same_host_as_initial?
|
46
52
|
end
|
47
53
|
|
48
54
|
yield(@connection)
|
@@ -51,7 +57,18 @@ module Lhm
|
|
51
57
|
# The error will be raised the connection is still active (i.e. no need to reconnect) or if the connection is
|
52
58
|
# dead (i.e. not active) and @reconnect_with_host is false (i.e. instructed not to reconnect)
|
53
59
|
raise e if @connection.active? || (!@connection.active? && !@reconnect_with_consistent_host)
|
54
|
-
|
60
|
+
|
61
|
+
# Lhm could be stuck in a weird state where it loses connection, reconnects and re looses-connection instantly
|
62
|
+
# after, creating an infinite loop (because of the usage of `retry`). Hence, abort after 25 reconnections
|
63
|
+
raise Lhm::Error.new("LHM reached host reconnection max of #{RECONNECTION_MAXIMUM} times. " \
|
64
|
+
"Please try again later.") if reconnection_counter > RECONNECTION_MAXIMUM
|
65
|
+
|
66
|
+
reconnection_counter += 1
|
67
|
+
if reconnect_with_host_check!
|
68
|
+
retry
|
69
|
+
else
|
70
|
+
raise Lhm::Error.new("LHM tried the reconnection procedure but failed. Aborting")
|
71
|
+
end
|
55
72
|
end
|
56
73
|
end
|
57
74
|
end
|
@@ -105,34 +122,32 @@ module Lhm
|
|
105
122
|
|
106
123
|
def reconnect_with_host_check!
|
107
124
|
log_with_prefix("Lost connection to MySQL, will retry to connect to same host")
|
108
|
-
|
109
|
-
|
125
|
+
|
126
|
+
RECONNECT_RETRY_MAX_ITERATION.times do
|
127
|
+
begin
|
128
|
+
sleep(RECONNECT_RETRY_INTERVAL)
|
129
|
+
|
110
130
|
# tries to reconnect. On failure will trigger a retry
|
111
131
|
@connection.reconnect!
|
112
132
|
|
113
133
|
if same_host_as_initial?
|
114
134
|
# This is not an actual error, but controlled way to get the parent `Retriable.retriable` to retry
|
115
135
|
# the statement that failed (since the Retriable gem only retries on errors).
|
116
|
-
|
136
|
+
log_with_prefix("LHM successfully reconnected to initial host: #{@initial_hostname} (server_id: #{@initial_server_id})")
|
137
|
+
return true
|
117
138
|
else
|
118
139
|
# New Master --> abort LHM (reconnecting will not change anything)
|
119
|
-
|
140
|
+
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
|
+
return false
|
120
142
|
end
|
143
|
+
rescue StandardError => e
|
144
|
+
# Retry if ActiveRecord cannot reach host
|
145
|
+
next if /Lost connection to MySQL server at 'reading initial communication packet'/ === e.message
|
146
|
+
log_with_prefix("Encountered error: [#{e.class}] #{e.message}. Will stop reconnection procedure.", :info)
|
147
|
+
return false
|
121
148
|
end
|
122
|
-
rescue StandardError => e
|
123
|
-
# The parent Retriable.retriable is configured to retry if it encounters an error with the success message.
|
124
|
-
# Therefore, if the connection is re-established successfully AND the host is the same, LHM can retry the query
|
125
|
-
# that originally failed.
|
126
|
-
raise e if reconnect_successful?(e)
|
127
|
-
# If the connection was not successful, the parent retriable will raise any error that originated from the
|
128
|
-
# `@connection.reconnect!`
|
129
|
-
# Therefore, this error will cause the LHM to abort
|
130
|
-
raise Lhm::Error.new("LHM tried the reconnection procedure but failed. Latest error: #{e.message}")
|
131
149
|
end
|
132
|
-
|
133
|
-
|
134
|
-
def reconnect_successful?(e)
|
135
|
-
e.is_a?(ReconnectToHostSuccessful)
|
150
|
+
false
|
136
151
|
end
|
137
152
|
|
138
153
|
# For a full list of configuration options see https://github.com/kamui/retriable
|
@@ -149,9 +164,6 @@ module Lhm
|
|
149
164
|
/Unknown MySQL server host/,
|
150
165
|
/connection is locked to hostgroup/,
|
151
166
|
/The MySQL server is running with the --read-only option so it cannot execute this statement/,
|
152
|
-
],
|
153
|
-
ReconnectToHostSuccessful => [
|
154
|
-
/#{RECONNECT_SUCCESSFUL_MESSAGE}/
|
155
167
|
]
|
156
168
|
},
|
157
169
|
multiplier: 1, # each successive interval grows by this factor
|
@@ -159,28 +171,6 @@ module Lhm
|
|
159
171
|
tries: 20, # Number of attempts to make at running your code block (includes initial attempt).
|
160
172
|
rand_factor: 0, # percentage to randomize the next retry interval time
|
161
173
|
max_elapsed_time: Float::INFINITY, # max total time in seconds that code is allowed to keep being retried
|
162
|
-
on_retry: Proc.new do |exception, try_number, total_elapsed_time, next_interval|
|
163
|
-
if reconnect_successful?(exception)
|
164
|
-
log_with_prefix("#{exception.message} -- triggering retry", :info)
|
165
|
-
else
|
166
|
-
log_with_prefix("#{exception.class}: '#{exception.message}' - #{try_number} tries in #{total_elapsed_time} seconds and #{next_interval} seconds until the next try.", :error)
|
167
|
-
end
|
168
|
-
end
|
169
|
-
}.freeze
|
170
|
-
end
|
171
|
-
|
172
|
-
def host_retry_config
|
173
|
-
{
|
174
|
-
on: {
|
175
|
-
StandardError => [
|
176
|
-
/Lost connection to MySQL server at 'reading initial communication packet'/
|
177
|
-
]
|
178
|
-
},
|
179
|
-
multiplier: 1, # each successive interval grows by this factor
|
180
|
-
base_interval: 0.25, # the initial interval in seconds between tries.
|
181
|
-
tries: 20, # Number of attempts to make at running your code block (includes initial attempt).
|
182
|
-
rand_factor: 0, # percentage to randomize the next retry interval time
|
183
|
-
max_elapsed_time: Float::INFINITY, # max total time in seconds that code is allowed to keep being retried
|
184
174
|
on_retry: Proc.new do |exception, try_number, total_elapsed_time, next_interval|
|
185
175
|
log_with_prefix("#{exception.class}: '#{exception.message}' - #{try_number} tries in #{total_elapsed_time} seconds and #{next_interval} seconds until the next try.", :error)
|
186
176
|
end
|
@@ -0,0 +1,162 @@
|
|
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 initialize(host, connection_config = nil)
|
95
|
+
@host = host
|
96
|
+
@connection_config = prepare_connection_config(connection_config)
|
97
|
+
@connection = client(@connection_config)
|
98
|
+
end
|
99
|
+
|
100
|
+
def replica_hosts
|
101
|
+
Throttler.format_hosts(query_connection(SQL_SELECT_REPLICA_HOSTS, 'host'))
|
102
|
+
end
|
103
|
+
|
104
|
+
def lag
|
105
|
+
query_connection(SQL_SELECT_MAX_REPLICA_LAG, 'Seconds_Behind_Master').first.to_i
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def client(config)
|
111
|
+
begin
|
112
|
+
Lhm.logger.info "Connecting to #{@host} on database: #{config[:database]}"
|
113
|
+
Mysql2::Client.new(config)
|
114
|
+
rescue Mysql2::Error => e
|
115
|
+
Lhm.logger.info "Error connecting to #{@host}: #{e}"
|
116
|
+
nil
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def prepare_connection_config(config_proc)
|
121
|
+
config = if config_proc
|
122
|
+
if config_proc.respond_to?(:call) # if we get a proc
|
123
|
+
config_proc.call
|
124
|
+
else
|
125
|
+
raise ArgumentError, "Expected #{config_proc.inspect} to respond to `call`"
|
126
|
+
end
|
127
|
+
else
|
128
|
+
db_config
|
129
|
+
end
|
130
|
+
config.deep_symbolize_keys!
|
131
|
+
config[:host] = @host
|
132
|
+
config
|
133
|
+
end
|
134
|
+
|
135
|
+
def query_connection(query, result)
|
136
|
+
begin
|
137
|
+
@connection.query(query).map { |row| row[result] }
|
138
|
+
rescue Mysql2::Error => e
|
139
|
+
Lhm.logger.info "Unable to connect and/or query #{host}: #{e}"
|
140
|
+
[nil]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
def db_config
|
147
|
+
if ar_supports_db_config?
|
148
|
+
ActiveRecord::Base.connection_pool.db_config.configuration_hash.dup
|
149
|
+
else
|
150
|
+
ActiveRecord::Base.connection_pool.spec.config.dup
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def ar_supports_db_config?
|
155
|
+
# https://api.rubyonrails.org/v6.0/classes/ActiveRecord/ConnectionAdapters/ConnectionPool.html <-- has spec
|
156
|
+
# vs
|
157
|
+
# https://api.rubyonrails.org/v6.1/classes/ActiveRecord/ConnectionAdapters/ConnectionPool.html <-- has db_config
|
158
|
+
ActiveRecord::VERSION::MAJOR > 6 || ActiveRecord::VERSION::MAJOR == 6 && ActiveRecord::VERSION::MINOR >= 1
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -1,161 +1,11 @@
|
|
1
|
+
require 'lhm/throttler/replica_lag'
|
2
|
+
|
1
3
|
module Lhm
|
2
4
|
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 SlaveLag
|
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
|
-
|
5
|
+
class SlaveLag < ReplicaLag
|
25
6
|
def initialize(options = {})
|
26
|
-
|
27
|
-
|
28
|
-
@allowed_lag = options[:allowed_lag] || DEFAULT_MAX_ALLOWED_LAG
|
29
|
-
@slaves = {}
|
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_slave_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 slave 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 slave 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 slaves
|
55
|
-
@slaves[@connection] ||= get_slaves
|
56
|
-
end
|
57
|
-
|
58
|
-
def get_slaves
|
59
|
-
slaves = []
|
60
|
-
if @check_only.nil? or !@check_only.respond_to?(:call)
|
61
|
-
slave_hosts = master_slave_hosts
|
62
|
-
while slave_hosts.any? do
|
63
|
-
host = slave_hosts.pop
|
64
|
-
slave = Slave.new(host, @get_config)
|
65
|
-
if !slaves.map(&:host).include?(host) && slave.connection
|
66
|
-
slaves << slave
|
67
|
-
slave_hosts.concat(slave.slave_hosts)
|
68
|
-
end
|
69
|
-
end
|
70
|
-
else
|
71
|
-
slave_config = @check_only.call
|
72
|
-
slaves << Slave.new(slave_config['host'], @get_config)
|
73
|
-
end
|
74
|
-
slaves
|
75
|
-
end
|
76
|
-
|
77
|
-
def master_slave_hosts
|
78
|
-
Throttler.format_hosts(@connection.select_values(Slave::SQL_SELECT_SLAVE_HOSTS))
|
79
|
-
end
|
80
|
-
|
81
|
-
def max_current_slave_lag
|
82
|
-
max = slaves.map { |slave| slave.lag }.push(0).max
|
83
|
-
Lhm.logger.info "Max current slave lag: #{max}"
|
84
|
-
max
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
class Slave
|
89
|
-
SQL_SELECT_SLAVE_HOSTS = "SELECT host FROM information_schema.processlist WHERE command LIKE 'Binlog Dump%'"
|
90
|
-
SQL_SELECT_MAX_SLAVE_LAG = 'SHOW SLAVE STATUS'
|
91
|
-
|
92
|
-
attr_reader :host, :connection
|
93
|
-
|
94
|
-
def initialize(host, connection_config = nil)
|
95
|
-
@host = host
|
96
|
-
@connection_config = prepare_connection_config(connection_config)
|
97
|
-
@connection = client(@connection_config)
|
98
|
-
end
|
99
|
-
|
100
|
-
def slave_hosts
|
101
|
-
Throttler.format_hosts(query_connection(SQL_SELECT_SLAVE_HOSTS, 'host'))
|
102
|
-
end
|
103
|
-
|
104
|
-
def lag
|
105
|
-
query_connection(SQL_SELECT_MAX_SLAVE_LAG, 'Seconds_Behind_Master').first.to_i
|
106
|
-
end
|
107
|
-
|
108
|
-
private
|
109
|
-
|
110
|
-
def client(config)
|
111
|
-
begin
|
112
|
-
Lhm.logger.info "Connecting to #{@host} on database: #{config[:database]}"
|
113
|
-
Mysql2::Client.new(config)
|
114
|
-
rescue Mysql2::Error => e
|
115
|
-
Lhm.logger.info "Error connecting to #{@host}: #{e}"
|
116
|
-
nil
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
def prepare_connection_config(config_proc)
|
121
|
-
config = if config_proc
|
122
|
-
if config_proc.respond_to?(:call) # if we get a proc
|
123
|
-
config_proc.call
|
124
|
-
else
|
125
|
-
raise ArgumentError, "Expected #{config_proc.inspect} to respond to `call`"
|
126
|
-
end
|
127
|
-
else
|
128
|
-
db_config
|
129
|
-
end
|
130
|
-
config.deep_symbolize_keys!
|
131
|
-
config[:host] = @host
|
132
|
-
config
|
133
|
-
end
|
134
|
-
|
135
|
-
def query_connection(query, result)
|
136
|
-
begin
|
137
|
-
@connection.query(query).map { |row| row[result] }
|
138
|
-
rescue Mysql2::Error => e
|
139
|
-
Lhm.logger.info "Unable to connect and/or query #{host}: #{e}"
|
140
|
-
[nil]
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
private
|
145
|
-
|
146
|
-
def db_config
|
147
|
-
if ar_supports_db_config?
|
148
|
-
ActiveRecord::Base.connection_pool.db_config.configuration_hash.dup
|
149
|
-
else
|
150
|
-
ActiveRecord::Base.connection_pool.spec.config.dup
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
def ar_supports_db_config?
|
155
|
-
# https://api.rubyonrails.org/v6.0/classes/ActiveRecord/ConnectionAdapters/ConnectionPool.html <-- has spec
|
156
|
-
# vs
|
157
|
-
# https://api.rubyonrails.org/v6.1/classes/ActiveRecord/ConnectionAdapters/ConnectionPool.html <-- has db_config
|
158
|
-
ActiveRecord::VERSION::MAJOR > 6 || ActiveRecord::VERSION::MAJOR == 6 && ActiveRecord::VERSION::MINOR >= 1
|
7
|
+
Lhm.logger.warn("Class `SlaveLag` is deprecated. Use `ReplicaLag` class instead.")
|
8
|
+
super(options)
|
159
9
|
end
|
160
10
|
end
|
161
11
|
end
|
@@ -3,14 +3,16 @@ module Lhm
|
|
3
3
|
class ThreadsRunning
|
4
4
|
include Command
|
5
5
|
|
6
|
+
DEFAULT_STRIDE = 2_000
|
6
7
|
DEFAULT_INITIAL_TIMEOUT = 0.1
|
7
8
|
DEFAULT_HEALTHY_RANGE = (0..50)
|
8
9
|
|
9
|
-
attr_accessor :timeout_seconds, :healthy_range, :connection
|
10
|
+
attr_accessor :timeout_seconds, :healthy_range, :connection, :stride
|
10
11
|
attr_reader :max_timeout_seconds, :initial_timeout_seconds
|
11
12
|
|
12
13
|
def initialize(options = {})
|
13
14
|
@initial_timeout_seconds = options[:initial_timeout] || DEFAULT_INITIAL_TIMEOUT
|
15
|
+
@stride = options[:stride] || DEFAULT_STRIDE
|
14
16
|
@max_timeout_seconds = options[:max_timeout] || (@initial_timeout_seconds * 1024)
|
15
17
|
@timeout_seconds = @initial_timeout_seconds
|
16
18
|
@healthy_range = options[:healthy_range] || DEFAULT_HEALTHY_RANGE
|
data/lib/lhm/throttler.rb
CHANGED
@@ -1,12 +1,16 @@
|
|
1
1
|
require 'lhm/throttler/time'
|
2
|
+
require 'lhm/throttler/replica_lag'
|
2
3
|
require 'lhm/throttler/slave_lag'
|
3
4
|
require 'lhm/throttler/threads_running'
|
4
5
|
|
5
6
|
module Lhm
|
6
7
|
module Throttler
|
7
|
-
CLASSES = {
|
8
|
-
|
9
|
-
|
8
|
+
CLASSES = {
|
9
|
+
:time_throttler => Throttler::Time,
|
10
|
+
:replica_lag_throttler => Throttler::ReplicaLag,
|
11
|
+
:slave_lag_throttler => Throttler::SlaveLag,
|
12
|
+
:threads_running_throttler => Throttler::ThreadsRunning
|
13
|
+
}
|
10
14
|
|
11
15
|
def throttler
|
12
16
|
@throttler ||= Throttler::Time.new
|
data/lib/lhm/version.rb
CHANGED
data/spec/.lhm.example
CHANGED
data/spec/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Preparing for master
|
1
|
+
# Preparing for master replica integration tests
|
2
2
|
|
3
3
|
## Configuration
|
4
4
|
|
@@ -7,10 +7,10 @@ create ~/.lhm:
|
|
7
7
|
mysqldir=/usr/local/mysql
|
8
8
|
basedir=~/lhm-cluster
|
9
9
|
master_port=3306
|
10
|
-
|
10
|
+
replica_port=3307
|
11
11
|
|
12
12
|
mysqldir specifies the location of your mysql install. basedir is the
|
13
|
-
directory master and
|
13
|
+
directory master and replica databases will get installed into.
|
14
14
|
|
15
15
|
## Automatic setup
|
16
16
|
|
@@ -18,8 +18,8 @@ directory master and slave databases will get installed into.
|
|
18
18
|
|
19
19
|
bin/lhm-spec-clobber.sh
|
20
20
|
|
21
|
-
You can set the integration specs up to run against a master
|
22
|
-
running the included that. This deletes the configured lhm master
|
21
|
+
You can set the integration specs up to run against a master replica setup by
|
22
|
+
running the included that. This deletes the configured lhm master replica setup and reinstalls and configures a master replica setup.
|
23
23
|
|
24
24
|
Follow the manual instructions if you want more control over this process.
|
25
25
|
|
@@ -33,7 +33,7 @@ Follow the manual instructions if you want more control over this process.
|
|
33
33
|
|
34
34
|
basedir=/opt/lhm-luster
|
35
35
|
mysqld --defaults-file="$basedir/master/my.cnf"
|
36
|
-
mysqld --defaults-file="$basedir/
|
36
|
+
mysqld --defaults-file="$basedir/replica/my.cnf"
|
37
37
|
|
38
38
|
### run the grants
|
39
39
|
|
@@ -46,13 +46,12 @@ Setup the dependency gems
|
|
46
46
|
export BUNDLE_GEMFILE=gemfiles/ar-4.2_mysql2.gemfile
|
47
47
|
bundle install
|
48
48
|
|
49
|
-
To run specs in
|
49
|
+
To run specs in replica mode, set the MASTER_REPLICA=1 when running tests:
|
50
50
|
|
51
|
-
|
51
|
+
MASTER_REPLICA=1 bundle exec rake specs
|
52
52
|
|
53
53
|
# connecting
|
54
54
|
|
55
55
|
you can connect by running (with the respective ports):
|
56
56
|
|
57
57
|
mysql --protocol=TCP -p3307
|
58
|
-
|
@@ -110,7 +110,7 @@ describe Lhm::AtomicSwitcher do
|
|
110
110
|
switcher = Lhm::AtomicSwitcher.new(@migration, connection)
|
111
111
|
switcher.run
|
112
112
|
|
113
|
-
|
113
|
+
replica do
|
114
114
|
value(data_source_exists?(@origin)).must_equal true
|
115
115
|
value(table_read(@migration.archive_name).columns.keys).must_include 'origin'
|
116
116
|
end
|
@@ -120,7 +120,7 @@ describe Lhm::AtomicSwitcher do
|
|
120
120
|
switcher = Lhm::AtomicSwitcher.new(@migration, connection)
|
121
121
|
switcher.run
|
122
122
|
|
123
|
-
|
123
|
+
replica do
|
124
124
|
value(data_source_exists?(@destination)).must_equal false
|
125
125
|
value(table_read(@origin.name).columns.keys).must_include 'destination'
|
126
126
|
end
|
@@ -19,10 +19,10 @@ describe Lhm::ChunkInsert do
|
|
19
19
|
assert_equal 1, @instance.insert_and_return_count_of_rows_created
|
20
20
|
end
|
21
21
|
|
22
|
-
it "inserts the record into the
|
22
|
+
it "inserts the record into the replica" do
|
23
23
|
@instance.insert_and_return_count_of_rows_created
|
24
24
|
|
25
|
-
|
25
|
+
replica do
|
26
26
|
value(count_all(@destination.name)).must_equal(1)
|
27
27
|
end
|
28
28
|
end
|