lhm-shopify 3.4.0 → 3.5.5
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 +24 -15
- data/.gitignore +1 -6
- data/Appraisals +24 -0
- data/CHANGELOG.md +30 -0
- data/Gemfile.lock +66 -0
- data/README.md +55 -4
- data/Rakefile +11 -0
- data/dev.yml +31 -6
- data/docker-compose.yml +58 -0
- data/gemfiles/activerecord_5.2.gemfile +9 -0
- data/gemfiles/activerecord_5.2.gemfile.lock +65 -0
- data/gemfiles/activerecord_6.0.gemfile +7 -0
- data/gemfiles/activerecord_6.0.gemfile.lock +67 -0
- data/gemfiles/activerecord_6.1.gemfile +7 -0
- data/gemfiles/activerecord_6.1.gemfile.lock +66 -0
- data/gemfiles/activerecord_7.0.0.alpha2.gemfile +7 -0
- data/gemfiles/activerecord_7.0.0.alpha2.gemfile.lock +64 -0
- data/lhm.gemspec +7 -3
- data/lib/lhm/atomic_switcher.rb +5 -11
- data/lib/lhm/chunk_insert.rb +7 -10
- data/lib/lhm/chunker.rb +21 -10
- data/lib/lhm/cleanup/current.rb +9 -12
- data/lib/lhm/connection.rb +108 -0
- data/lib/lhm/entangler.rb +8 -13
- data/lib/lhm/invoker.rb +6 -4
- data/lib/lhm/locked_switcher.rb +2 -0
- data/lib/lhm/migrator.rb +2 -0
- data/lib/lhm/printer.rb +10 -6
- data/lib/lhm/proxysql_helper.rb +10 -0
- data/lib/lhm/sql_retry.rb +129 -10
- data/lib/lhm/throttler/slave_lag.rb +19 -2
- data/lib/lhm/version.rb +1 -1
- data/lib/lhm.rb +41 -16
- data/scripts/helpers/wait-for-dbs.sh +21 -0
- data/scripts/mysql/reader/create_replication.sql +10 -0
- data/scripts/mysql/writer/create_test_db.sql +1 -0
- data/scripts/mysql/writer/create_users.sql +6 -0
- data/scripts/proxysql/proxysql.cnf +117 -0
- data/spec/integration/atomic_switcher_spec.rb +53 -17
- data/spec/integration/chunk_insert_spec.rb +3 -2
- data/spec/integration/chunker_spec.rb +18 -16
- data/spec/integration/cleanup_spec.rb +49 -38
- data/spec/integration/database.yml +25 -0
- data/spec/integration/entangler_spec.rb +7 -5
- data/spec/integration/integration_helper.rb +25 -10
- data/spec/integration/lhm_spec.rb +114 -40
- data/spec/integration/lock_wait_timeout_spec.rb +2 -2
- data/spec/integration/locked_switcher_spec.rb +4 -4
- data/spec/integration/proxysql_spec.rb +34 -0
- data/spec/integration/sql_retry/db_connection_helper.rb +52 -0
- data/spec/integration/sql_retry/lock_wait_spec.rb +8 -6
- data/spec/integration/sql_retry/lock_wait_timeout_test_helper.rb +17 -4
- data/spec/integration/sql_retry/proxysql_helper.rb +22 -0
- data/spec/integration/sql_retry/retry_with_proxysql_spec.rb +109 -0
- data/spec/integration/table_spec.rb +11 -19
- data/spec/integration/toxiproxy_helper.rb +40 -0
- data/spec/test_helper.rb +24 -0
- data/spec/unit/atomic_switcher_spec.rb +4 -6
- data/spec/unit/chunk_insert_spec.rb +7 -2
- data/spec/unit/chunker_spec.rb +47 -42
- data/spec/unit/connection_spec.rb +111 -0
- data/spec/unit/entangler_spec.rb +85 -22
- data/spec/unit/intersection_spec.rb +4 -4
- data/spec/unit/lhm_spec.rb +23 -6
- data/spec/unit/locked_switcher_spec.rb +13 -18
- data/spec/unit/migrator_spec.rb +17 -19
- data/spec/unit/printer_spec.rb +14 -26
- data/spec/unit/sql_helper_spec.rb +8 -12
- data/spec/unit/table_spec.rb +5 -5
- data/spec/unit/throttler/slave_lag_spec.rb +14 -9
- data/spec/unit/throttler_spec.rb +12 -12
- data/spec/unit/unit_helper.rb +13 -0
- metadata +85 -14
- data/bin/.gitkeep +0 -0
- data/dbdeployer/config.json +0 -32
- data/dbdeployer/install.sh +0 -64
- data/gemfiles/ar-2.3_mysql.gemfile +0 -6
- data/gemfiles/ar-3.2_mysql.gemfile +0 -5
- data/gemfiles/ar-3.2_mysql2.gemfile +0 -5
- data/gemfiles/ar-4.0_mysql2.gemfile +0 -5
- data/gemfiles/ar-4.1_mysql2.gemfile +0 -5
- data/gemfiles/ar-4.2_mysql2.gemfile +0 -5
- data/gemfiles/ar-5.0_mysql2.gemfile +0 -5
data/lib/lhm/sql_retry.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'retriable'
|
2
2
|
require 'lhm/sql_helper'
|
3
|
+
require 'lhm/proxysql_helper'
|
3
4
|
|
4
5
|
module Lhm
|
5
6
|
# SqlRetry standardizes the interface for retry behavior in components like
|
@@ -15,22 +16,140 @@ module Lhm
|
|
15
16
|
# https://github.com/kamui/retriable. Additionally, a "log_prefix" option,
|
16
17
|
# which is unique to SqlRetry can be used to prefix log output.
|
17
18
|
class SqlRetry
|
18
|
-
|
19
|
+
RECONNECT_SUCCESSFUL_MESSAGE = "LHM successfully reconnected to initial host:"
|
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
|
27
|
+
|
28
|
+
MYSQL_VAR_NAMES = {
|
29
|
+
hostname: "@@global.hostname",
|
30
|
+
server_id: "@@global.server_id",
|
31
|
+
version_comment: "@@version_comment",
|
32
|
+
}
|
33
|
+
|
34
|
+
def initialize(connection, retry_options: {}, reconnect_with_consistent_host: false)
|
19
35
|
@connection = connection
|
20
|
-
|
21
|
-
|
36
|
+
self.retry_config = retry_options
|
37
|
+
self.reconnect_with_consistent_host = reconnect_with_consistent_host
|
22
38
|
end
|
23
39
|
|
24
|
-
|
25
|
-
|
26
|
-
|
40
|
+
# Complete explanation of algorithm: https://github.com/Shopify/lhm/pull/112
|
41
|
+
def with_retries(log_prefix: nil)
|
42
|
+
@log_prefix = log_prefix || "" # No prefix. Just logs
|
43
|
+
|
44
|
+
# Amount of time LHM had to reconnect. Aborting if more than RECONNECTION_MAXIMUM
|
45
|
+
reconnection_counter = 0
|
46
|
+
|
47
|
+
Retriable.retriable(@retry_config) do
|
48
|
+
# Using begin -> rescue -> end for Ruby 2.4 compatibility
|
49
|
+
begin
|
50
|
+
if @reconnect_with_consistent_host
|
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?
|
52
|
+
end
|
53
|
+
|
54
|
+
yield(@connection)
|
55
|
+
rescue StandardError => e
|
56
|
+
# Not all errors should trigger a reconnect. Some errors such be raised and abort the LHM (such as reconnecting to the wrong host).
|
57
|
+
# The error will be raised the connection is still active (i.e. no need to reconnect) or if the connection is
|
58
|
+
# dead (i.e. not active) and @reconnect_with_host is false (i.e. instructed not to reconnect)
|
59
|
+
raise e if @connection.active? || (!@connection.active? && !@reconnect_with_consistent_host)
|
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
|
72
|
+
end
|
27
73
|
end
|
28
74
|
end
|
29
75
|
|
30
|
-
|
76
|
+
# Both attributes will have defined setters
|
77
|
+
attr_reader :retry_config, :reconnect_with_consistent_host
|
78
|
+
attr_accessor :connection
|
79
|
+
|
80
|
+
def retry_config=(retry_options)
|
81
|
+
@retry_config = default_retry_config.dup.merge!(retry_options)
|
82
|
+
end
|
83
|
+
|
84
|
+
def reconnect_with_consistent_host=(reconnect)
|
85
|
+
if (@reconnect_with_consistent_host = reconnect)
|
86
|
+
@initial_hostname = hostname
|
87
|
+
@initial_server_id = server_id
|
88
|
+
end
|
89
|
+
end
|
31
90
|
|
32
91
|
private
|
33
92
|
|
93
|
+
def hostname
|
94
|
+
mysql_single_value(MYSQL_VAR_NAMES[:hostname])
|
95
|
+
end
|
96
|
+
|
97
|
+
def server_id
|
98
|
+
mysql_single_value(MYSQL_VAR_NAMES[:server_id])
|
99
|
+
end
|
100
|
+
|
101
|
+
def cloudsql?
|
102
|
+
mysql_single_value(MYSQL_VAR_NAMES[:version_comment]).include?(CLOUDSQL_VERSION_COMMENT)
|
103
|
+
end
|
104
|
+
|
105
|
+
def mysql_single_value(name)
|
106
|
+
query = Lhm::ProxySQLHelper.tagged("SELECT #{name} LIMIT 1")
|
107
|
+
|
108
|
+
@connection.execute(query).to_a.first.tap do |record|
|
109
|
+
return record&.first
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def same_host_as_initial?
|
114
|
+
return @initial_server_id == server_id if cloudsql?
|
115
|
+
@initial_hostname == hostname
|
116
|
+
end
|
117
|
+
|
118
|
+
def log_with_prefix(message, level = :info)
|
119
|
+
message.prepend("[#{@log_prefix}] ") if @log_prefix
|
120
|
+
Lhm.logger.public_send(level, message)
|
121
|
+
end
|
122
|
+
|
123
|
+
def reconnect_with_host_check!
|
124
|
+
log_with_prefix("Lost connection to MySQL, will retry to connect to same host")
|
125
|
+
|
126
|
+
RECONNECT_RETRY_MAX_ITERATION.times do
|
127
|
+
begin
|
128
|
+
sleep(RECONNECT_RETRY_INTERVAL)
|
129
|
+
|
130
|
+
# tries to reconnect. On failure will trigger a retry
|
131
|
+
@connection.reconnect!
|
132
|
+
|
133
|
+
if same_host_as_initial?
|
134
|
+
# This is not an actual error, but controlled way to get the parent `Retriable.retriable` to retry
|
135
|
+
# the statement that failed (since the Retriable gem only retries on errors).
|
136
|
+
log_with_prefix("LHM successfully reconnected to initial host: #{@initial_hostname} (server_id: #{@initial_server_id})")
|
137
|
+
return true
|
138
|
+
else
|
139
|
+
# New Master --> abort LHM (reconnecting will not change anything)
|
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
|
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
|
148
|
+
end
|
149
|
+
end
|
150
|
+
false
|
151
|
+
end
|
152
|
+
|
34
153
|
# For a full list of configuration options see https://github.com/kamui/retriable
|
35
154
|
def default_retry_config
|
36
155
|
{
|
@@ -43,6 +162,8 @@ module Lhm
|
|
43
162
|
/Lost connection to MySQL server during query/,
|
44
163
|
/Max connect timeout reached/,
|
45
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/,
|
46
167
|
]
|
47
168
|
},
|
48
169
|
multiplier: 1, # each successive interval grows by this factor
|
@@ -51,9 +172,7 @@ module Lhm
|
|
51
172
|
rand_factor: 0, # percentage to randomize the next retry interval time
|
52
173
|
max_elapsed_time: Float::INFINITY, # max total time in seconds that code is allowed to keep being retried
|
53
174
|
on_retry: Proc.new do |exception, try_number, total_elapsed_time, next_interval|
|
54
|
-
|
55
|
-
log.prepend("[#{@log_prefix}] ") if @log_prefix
|
56
|
-
Lhm.logger.info(log)
|
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)
|
57
176
|
end
|
58
177
|
}.freeze
|
59
178
|
end
|
@@ -124,8 +124,8 @@ module Lhm
|
|
124
124
|
else
|
125
125
|
raise ArgumentError, "Expected #{config_proc.inspect} to respond to `call`"
|
126
126
|
end
|
127
|
-
else
|
128
|
-
|
127
|
+
else
|
128
|
+
db_config
|
129
129
|
end
|
130
130
|
config.deep_symbolize_keys!
|
131
131
|
config[:host] = @host
|
@@ -140,6 +140,23 @@ module Lhm
|
|
140
140
|
[nil]
|
141
141
|
end
|
142
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
|
143
160
|
end
|
144
161
|
end
|
145
162
|
end
|
data/lib/lhm/version.rb
CHANGED
data/lib/lhm.rb
CHANGED
@@ -8,6 +8,8 @@ require 'lhm/throttler'
|
|
8
8
|
require 'lhm/version'
|
9
9
|
require 'lhm/cleanup/current'
|
10
10
|
require 'lhm/sql_retry'
|
11
|
+
require 'lhm/proxysql_helper'
|
12
|
+
require 'lhm/connection'
|
11
13
|
require 'lhm/test_support'
|
12
14
|
require 'lhm/railtie' if defined?(Rails::Railtie)
|
13
15
|
require 'logger'
|
@@ -26,7 +28,7 @@ module Lhm
|
|
26
28
|
extend Throttler
|
27
29
|
extend self
|
28
30
|
|
29
|
-
DEFAULT_LOGGER_OPTIONS =
|
31
|
+
DEFAULT_LOGGER_OPTIONS = { level: Logger::INFO, file: STDOUT }
|
30
32
|
|
31
33
|
# Alters a table with the changes described in the block
|
32
34
|
#
|
@@ -44,15 +46,19 @@ module Lhm
|
|
44
46
|
# Use atomic switch to rename tables (defaults to: true)
|
45
47
|
# If using a version of mysql affected by atomic switch bug, LHM forces user
|
46
48
|
# to set this option (see SqlHelper#supports_atomic_switch?)
|
49
|
+
# @option options [Boolean] :reconnect_with_consistent_host
|
50
|
+
# Active / Deactivate ProxySQL-aware reconnection procedure (default to: false)
|
47
51
|
# @yield [Migrator] Yielded Migrator object records the changes
|
48
52
|
# @return [Boolean] Returns true if the migration finishes
|
49
53
|
# @raise [Error] Raises Lhm::Error in case of a error and aborts the migration
|
50
54
|
def change_table(table_name, options = {}, &block)
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
55
|
+
with_flags(options) do
|
56
|
+
origin = Table.parse(table_name, connection)
|
57
|
+
invoker = Invoker.new(origin, connection)
|
58
|
+
block.call(invoker.migrator)
|
59
|
+
invoker.run(options)
|
60
|
+
true
|
61
|
+
end
|
56
62
|
end
|
57
63
|
|
58
64
|
# Cleanup tables and triggers
|
@@ -81,16 +87,19 @@ module Lhm
|
|
81
87
|
Lhm::Cleanup::Current.new(run, table_name, connection, options).execute
|
82
88
|
end
|
83
89
|
|
90
|
+
# Setups DB connection
|
91
|
+
#
|
92
|
+
# @param [ActiveRecord::Base] connection ActiveRecord Connection
|
84
93
|
def setup(connection)
|
85
|
-
@@connection = connection
|
94
|
+
@@connection = Connection.new(connection: connection)
|
86
95
|
end
|
87
96
|
|
97
|
+
# Returns DB connection (or initializes it if not created yet)
|
88
98
|
def connection
|
89
|
-
@@connection ||=
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
end
|
99
|
+
@@connection ||= begin
|
100
|
+
raise 'Please call Lhm.setup' unless defined?(ActiveRecord)
|
101
|
+
@@connection = Connection.new(connection: ActiveRecord::Base.connection)
|
102
|
+
end
|
94
103
|
end
|
95
104
|
|
96
105
|
def self.logger=(new_logger)
|
@@ -114,18 +123,34 @@ module Lhm
|
|
114
123
|
triggers.each do |trigger|
|
115
124
|
connection.execute("drop trigger if exists #{trigger}")
|
116
125
|
end
|
126
|
+
logger.info("Dropped triggers #{triggers.join(', ')}")
|
127
|
+
|
117
128
|
tables.each do |table|
|
118
129
|
connection.execute("drop table if exists #{table}")
|
119
130
|
end
|
131
|
+
logger.info("Dropped tables #{tables.join(', ')}")
|
132
|
+
|
120
133
|
true
|
121
134
|
elsif tables.empty? && triggers.empty?
|
122
|
-
|
135
|
+
logger.info('Everything is clean. Nothing to do.')
|
123
136
|
true
|
124
137
|
else
|
125
|
-
|
126
|
-
|
127
|
-
|
138
|
+
logger.info("Would drop LHM backup tables: #{tables.join(', ')}.")
|
139
|
+
logger.info("Would drop LHM triggers: #{triggers.join(', ')}.")
|
140
|
+
logger.info('Run with Lhm.cleanup(true) to drop all LHM triggers and tables, or Lhm.cleanup_current_run(true, table_name) to clean up a specific LHM.')
|
128
141
|
false
|
129
142
|
end
|
130
143
|
end
|
144
|
+
|
145
|
+
def with_flags(options)
|
146
|
+
old_flags = {
|
147
|
+
reconnect_with_consistent_host: Lhm.connection.reconnect_with_consistent_host,
|
148
|
+
}
|
149
|
+
|
150
|
+
Lhm.connection.reconnect_with_consistent_host = options[:reconnect_with_consistent_host] || false
|
151
|
+
|
152
|
+
yield
|
153
|
+
ensure
|
154
|
+
Lhm.connection.reconnect_with_consistent_host = old_flags[:reconnect_with_consistent_host]
|
155
|
+
end
|
131
156
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
# Wait for writer
|
3
|
+
echo "Waiting for MySQL-1: "
|
4
|
+
while ! (mysqladmin ping --host="127.0.0.1" --port=33006 --user=root --password=password --silent 2> /dev/null); do
|
5
|
+
echo -ne "."
|
6
|
+
sleep 1
|
7
|
+
done
|
8
|
+
# Wait for reader
|
9
|
+
echo "Waiting for MySQL-2: "
|
10
|
+
while ! (mysqladmin ping --host="127.0.0.1" --port=33007 --user=root --password=password --silent 2> /dev/null); do
|
11
|
+
echo -ne "."
|
12
|
+
sleep 1
|
13
|
+
done
|
14
|
+
# Wait for proxysql
|
15
|
+
echo "Waiting for ProxySQL:"
|
16
|
+
while ! (mysqladmin ping --host="127.0.0.1" --port=33005 --user=root --password=password --silent 2> /dev/null); do
|
17
|
+
echo -ne "."
|
18
|
+
sleep 1
|
19
|
+
done
|
20
|
+
|
21
|
+
echo "All DBs are ready"
|
@@ -0,0 +1 @@
|
|
1
|
+
CREATE DATABASE test;
|
@@ -0,0 +1,6 @@
|
|
1
|
+
# Creates replication user in Writer
|
2
|
+
CREATE USER IF NOT EXISTS 'writer'@'%' IDENTIFIED BY 'password';
|
3
|
+
CREATE USER IF NOT EXISTS 'reader'@'%' IDENTIFIED BY 'password';
|
4
|
+
|
5
|
+
CREATE USER IF NOT EXISTS 'replication'@'%' IDENTIFIED BY 'password';
|
6
|
+
GRANT REPLICATION SLAVE ON *.* TO' replication'@'%' IDENTIFIED BY 'password';
|
@@ -0,0 +1,117 @@
|
|
1
|
+
#file proxysql.cfg
|
2
|
+
|
3
|
+
datadir="/var/lib/proxysql"
|
4
|
+
restart_on_missing_heartbeats=999999
|
5
|
+
query_parser_token_delimiters=","
|
6
|
+
query_parser_key_value_delimiters=":"
|
7
|
+
unit_of_work_identifiers="consistent_read_id"
|
8
|
+
|
9
|
+
admin_variables=
|
10
|
+
{
|
11
|
+
mysql_ifaces="0.0.0.0:6032"
|
12
|
+
admin_credentials="admin:password;remote-admin:password"
|
13
|
+
}
|
14
|
+
|
15
|
+
mysql_servers =
|
16
|
+
(
|
17
|
+
{
|
18
|
+
address="mysql-1"
|
19
|
+
port=3306
|
20
|
+
hostgroup=0
|
21
|
+
max_connections=200
|
22
|
+
},
|
23
|
+
{
|
24
|
+
address="mysql-2"
|
25
|
+
port=3306
|
26
|
+
hostgroup=1
|
27
|
+
max_connections=200
|
28
|
+
}
|
29
|
+
)
|
30
|
+
|
31
|
+
mysql_variables=
|
32
|
+
{
|
33
|
+
session_idle_ms=1
|
34
|
+
auto_increment_delay_multiplex=0
|
35
|
+
|
36
|
+
threads=8
|
37
|
+
max_connections=100000
|
38
|
+
interfaces="0.0.0.0:3306"
|
39
|
+
server_version="5.7.18-proxysql"
|
40
|
+
connect_timeout_server=10000
|
41
|
+
connect_timeout_server_max=10000
|
42
|
+
connect_retries_on_failure=0
|
43
|
+
default_charset="utf8mb4"
|
44
|
+
free_connections_pct=100
|
45
|
+
connection_warming=true
|
46
|
+
max_allowed_packet=16777216
|
47
|
+
monitor_enabled=false
|
48
|
+
query_retries_on_failure=0
|
49
|
+
shun_on_failures=999999
|
50
|
+
shun_recovery_time_sec=0
|
51
|
+
kill_backend_connection_when_disconnect=false
|
52
|
+
stats_time_backend_query=false
|
53
|
+
stats_time_query_processor=false
|
54
|
+
max_stmts_per_connection=5
|
55
|
+
default_max_latency_ms=999999
|
56
|
+
wait_timeout=1800000
|
57
|
+
eventslog_format=3
|
58
|
+
log_multiplexing_disabled=true
|
59
|
+
log_unhealthy_connections=false
|
60
|
+
}
|
61
|
+
|
62
|
+
# defines all the MySQL users
|
63
|
+
mysql_users:
|
64
|
+
(
|
65
|
+
{
|
66
|
+
username = "root"
|
67
|
+
password = "password"
|
68
|
+
default_hostgroup = 0
|
69
|
+
max_connections=1000
|
70
|
+
active = 1
|
71
|
+
},
|
72
|
+
{
|
73
|
+
username = "writer"
|
74
|
+
password = "password"
|
75
|
+
default_hostgroup = 0
|
76
|
+
max_connections=50000
|
77
|
+
active = 1
|
78
|
+
transaction_persistent=1
|
79
|
+
},
|
80
|
+
{
|
81
|
+
username = "reader"
|
82
|
+
password = "password"
|
83
|
+
default_hostgroup = 1
|
84
|
+
max_connections=50000
|
85
|
+
active = 1
|
86
|
+
transaction_persistent=1
|
87
|
+
}
|
88
|
+
)
|
89
|
+
|
90
|
+
#defines MySQL Query Rules
|
91
|
+
mysql_query_rules:
|
92
|
+
(
|
93
|
+
{
|
94
|
+
rule_id = 1
|
95
|
+
active = 1
|
96
|
+
match_digest = "@@SESSION"
|
97
|
+
multiplex = 2
|
98
|
+
},
|
99
|
+
{
|
100
|
+
rule_id = 2
|
101
|
+
active = 1
|
102
|
+
match_digest = "@@global\.server_id"
|
103
|
+
multiplex = 2
|
104
|
+
},
|
105
|
+
{
|
106
|
+
rule_id = 3
|
107
|
+
active = 1
|
108
|
+
match_digest = "@@global\.hostname"
|
109
|
+
multiplex = 2
|
110
|
+
},
|
111
|
+
{
|
112
|
+
rule_id = 4
|
113
|
+
active = 1
|
114
|
+
match_pattern = "maintenance:lhm"
|
115
|
+
destination_hostgroup = 0
|
116
|
+
}
|
117
|
+
)
|
@@ -6,6 +6,7 @@ require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
|
|
6
6
|
require 'lhm/table'
|
7
7
|
require 'lhm/migration'
|
8
8
|
require 'lhm/atomic_switcher'
|
9
|
+
require 'lhm/connection'
|
9
10
|
|
10
11
|
describe Lhm::AtomicSwitcher do
|
11
12
|
include IntegrationHelper
|
@@ -15,9 +16,9 @@ describe Lhm::AtomicSwitcher do
|
|
15
16
|
describe 'switching' do
|
16
17
|
before(:each) do
|
17
18
|
Thread.abort_on_exception = true
|
18
|
-
@origin
|
19
|
+
@origin = table_create('origin')
|
19
20
|
@destination = table_create('destination')
|
20
|
-
@migration
|
21
|
+
@migration = Lhm::Migration.new(@origin, @destination)
|
21
22
|
@logs = StringIO.new
|
22
23
|
Lhm.logger = Logger.new(@logs)
|
23
24
|
@connection.execute('SET GLOBAL innodb_lock_wait_timeout=3')
|
@@ -29,26 +30,59 @@ describe Lhm::AtomicSwitcher do
|
|
29
30
|
end
|
30
31
|
|
31
32
|
it 'should retry and log on lock wait timeouts' do
|
32
|
-
|
33
|
-
|
34
|
-
|
33
|
+
ar_connection = mock()
|
34
|
+
ar_connection.stubs(:data_source_exists?).returns(true)
|
35
|
+
ar_connection.stubs(:active?).returns(true)
|
36
|
+
ar_connection.stubs(:execute).returns([["dummy"]], [["dummy"]], [["dummy"]])
|
37
|
+
.then
|
38
|
+
.raises(ActiveRecord::StatementInvalid, 'Lock wait timeout exceeded; try restarting transaction.')
|
39
|
+
.then
|
40
|
+
.returns([["dummy"]]) # Matches initial host -> triggers retry
|
41
|
+
|
42
|
+
connection = Lhm::Connection.new(connection: ar_connection, options: {
|
43
|
+
reconnect_with_consistent_host: true,
|
44
|
+
retriable: {
|
45
|
+
tries: 3,
|
46
|
+
base_interval: 0
|
47
|
+
}
|
48
|
+
})
|
35
49
|
|
36
|
-
switcher = Lhm::AtomicSwitcher.new(@migration, connection
|
50
|
+
switcher = Lhm::AtomicSwitcher.new(@migration, connection)
|
37
51
|
|
38
52
|
assert switcher.run
|
39
53
|
|
40
54
|
log_messages = @logs.string.split("\n")
|
41
55
|
assert_equal(2, log_messages.length)
|
42
56
|
assert log_messages[0].include? "Starting run of class=Lhm::AtomicSwitcher"
|
57
|
+
# On failure of this assertion, check for Lhm::Connection#file
|
43
58
|
assert log_messages[1].include? "[AtomicSwitcher] ActiveRecord::StatementInvalid: 'Lock wait timeout exceeded; try restarting transaction.' - 1 tries"
|
44
59
|
end
|
45
60
|
|
46
61
|
it 'should give up on lock wait timeouts after a configured number of tries' do
|
47
|
-
|
48
|
-
|
49
|
-
|
62
|
+
ar_connection = mock()
|
63
|
+
ar_connection.stubs(:data_source_exists?).returns(true)
|
64
|
+
ar_connection.stubs(:active?).returns(true)
|
65
|
+
ar_connection.stubs(:execute).returns([["dummy"]], [["dummy"]], [["dummy"]])
|
66
|
+
.then
|
67
|
+
.raises(ActiveRecord::StatementInvalid, 'Lock wait timeout exceeded; try restarting transaction.')
|
68
|
+
.then
|
69
|
+
.returns([["dummy"]]) # triggers retry 1
|
70
|
+
.then
|
71
|
+
.raises(ActiveRecord::StatementInvalid, 'Lock wait timeout exceeded; try restarting transaction.')
|
72
|
+
.then
|
73
|
+
.returns([["dummy"]]) # triggers retry 2
|
74
|
+
.then
|
75
|
+
.raises(ActiveRecord::StatementInvalid, 'Lock wait timeout exceeded; try restarting transaction.') # triggers retry 2
|
76
|
+
|
77
|
+
connection = Lhm::Connection.new(connection: ar_connection, options: {
|
78
|
+
reconnect_with_consistent_host: true,
|
79
|
+
retriable: {
|
80
|
+
tries: 2,
|
81
|
+
base_interval: 0
|
82
|
+
}
|
83
|
+
})
|
50
84
|
|
51
|
-
switcher = Lhm::AtomicSwitcher.new(@migration, connection
|
85
|
+
switcher = Lhm::AtomicSwitcher.new(@migration, connection)
|
52
86
|
|
53
87
|
assert_raises(ActiveRecord::StatementInvalid) { switcher.run }
|
54
88
|
end
|
@@ -58,12 +92,14 @@ describe Lhm::AtomicSwitcher do
|
|
58
92
|
switcher.send :define_singleton_method, :atomic_switch do
|
59
93
|
'SELECT * FROM nonexistent'
|
60
94
|
end
|
61
|
-
-> { switcher.run }.must_raise(ActiveRecord::StatementInvalid)
|
95
|
+
value(-> { switcher.run }).must_raise(ActiveRecord::StatementInvalid)
|
62
96
|
end
|
63
97
|
|
64
98
|
it "should raise when destination doesn't exist" do
|
65
|
-
|
66
|
-
|
99
|
+
ar_connection = mock()
|
100
|
+
ar_connection.stubs(:data_source_exists?).returns(false)
|
101
|
+
|
102
|
+
connection = Lhm::Connection.new(connection: ar_connection)
|
67
103
|
|
68
104
|
switcher = Lhm::AtomicSwitcher.new(@migration, connection)
|
69
105
|
|
@@ -75,8 +111,8 @@ describe Lhm::AtomicSwitcher do
|
|
75
111
|
switcher.run
|
76
112
|
|
77
113
|
slave do
|
78
|
-
data_source_exists?(@origin).must_equal true
|
79
|
-
table_read(@migration.archive_name).columns.keys.must_include 'origin'
|
114
|
+
value(data_source_exists?(@origin)).must_equal true
|
115
|
+
value(table_read(@migration.archive_name).columns.keys).must_include 'origin'
|
80
116
|
end
|
81
117
|
end
|
82
118
|
|
@@ -85,8 +121,8 @@ describe Lhm::AtomicSwitcher do
|
|
85
121
|
switcher.run
|
86
122
|
|
87
123
|
slave do
|
88
|
-
data_source_exists?(@destination).must_equal false
|
89
|
-
table_read(@origin.name).columns.keys.must_include 'destination'
|
124
|
+
value(data_source_exists?(@destination)).must_equal false
|
125
|
+
value(table_read(@origin.name).columns.keys).must_include 'destination'
|
90
126
|
end
|
91
127
|
end
|
92
128
|
end
|
@@ -11,7 +11,8 @@ describe Lhm::ChunkInsert do
|
|
11
11
|
@destination = table_create(:destination)
|
12
12
|
@migration = Lhm::Migration.new(@origin, @destination)
|
13
13
|
execute("insert into origin set id = 1001")
|
14
|
-
@
|
14
|
+
@connection = Lhm::Connection.new(connection: connection)
|
15
|
+
@instance = Lhm::ChunkInsert.new(@migration, @connection, 1001, 1001)
|
15
16
|
end
|
16
17
|
|
17
18
|
it "returns the count" do
|
@@ -22,7 +23,7 @@ describe Lhm::ChunkInsert do
|
|
22
23
|
@instance.insert_and_return_count_of_rows_created
|
23
24
|
|
24
25
|
slave do
|
25
|
-
count_all(@destination.name).must_equal(1)
|
26
|
+
value(count_all(@destination.name)).must_equal(1)
|
26
27
|
end
|
27
28
|
end
|
28
29
|
end
|