lhm-shopify 3.4.2 → 3.5.3
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 +15 -0
- data/Gemfile.lock +66 -0
- data/README.md +53 -4
- data/Rakefile +10 -0
- data/dev.yml +28 -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 +4 -11
- data/lib/lhm/chunk_insert.rb +4 -10
- data/lib/lhm/chunker.rb +7 -8
- data/lib/lhm/cleanup/current.rb +3 -10
- data/lib/lhm/connection.rb +109 -0
- data/lib/lhm/entangler.rb +4 -12
- data/lib/lhm/invoker.rb +3 -3
- data/lib/lhm/proxysql_helper.rb +10 -0
- data/lib/lhm/sql_retry.rb +132 -9
- data/lib/lhm/throttler/slave_lag.rb +19 -2
- data/lib/lhm/version.rb +1 -1
- data/lib/lhm.rb +25 -9
- 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 +38 -13
- data/spec/integration/chunk_insert_spec.rb +2 -1
- data/spec/integration/chunker_spec.rb +1 -1
- data/spec/integration/database.yml +25 -0
- data/spec/integration/entangler_spec.rb +3 -1
- data/spec/integration/integration_helper.rb +24 -9
- data/spec/integration/lhm_spec.rb +75 -0
- 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 +108 -0
- data/spec/integration/toxiproxy_helper.rb +40 -0
- data/spec/test_helper.rb +21 -0
- data/spec/unit/chunk_insert_spec.rb +7 -2
- data/spec/unit/chunker_spec.rb +46 -42
- data/spec/unit/connection_spec.rb +86 -0
- data/spec/unit/entangler_spec.rb +51 -10
- data/spec/unit/lhm_spec.rb +17 -0
- data/spec/unit/throttler/slave_lag_spec.rb +13 -8
- 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/lhm.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
lib = File.expand_path('
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
4
|
$:.unshift(lib) unless $:.include?(lib)
|
5
5
|
|
6
6
|
require 'lhm/version'
|
@@ -25,10 +25,14 @@ Gem::Specification.new do |s|
|
|
25
25
|
|
26
26
|
s.add_dependency 'retriable', '>= 3.0.0'
|
27
27
|
|
28
|
+
s.add_development_dependency 'activerecord'
|
28
29
|
s.add_development_dependency 'minitest'
|
29
30
|
s.add_development_dependency 'mocha'
|
31
|
+
s.add_development_dependency 'after_do'
|
30
32
|
s.add_development_dependency 'rake'
|
31
|
-
s.add_development_dependency 'activerecord'
|
32
33
|
s.add_development_dependency 'mysql2'
|
33
34
|
s.add_development_dependency 'simplecov'
|
35
|
+
s.add_development_dependency 'toxiproxy'
|
36
|
+
s.add_development_dependency 'appraisal'
|
37
|
+
s.add_development_dependency 'byebug'
|
34
38
|
end
|
data/lib/lhm/atomic_switcher.rb
CHANGED
@@ -16,17 +16,12 @@ module Lhm
|
|
16
16
|
|
17
17
|
attr_reader :connection
|
18
18
|
|
19
|
-
def initialize(migration, connection = nil, options
|
19
|
+
def initialize(migration, connection = nil, options={})
|
20
20
|
@migration = migration
|
21
21
|
@connection = connection
|
22
22
|
@origin = migration.origin
|
23
23
|
@destination = migration.destination
|
24
|
-
@
|
25
|
-
@connection,
|
26
|
-
{
|
27
|
-
log_prefix: "AtomicSwitcher"
|
28
|
-
}.merge!(options.fetch(:retriable, {}))
|
29
|
-
)
|
24
|
+
@retry_options = options[:retriable] || {}
|
30
25
|
end
|
31
26
|
|
32
27
|
def atomic_switch
|
@@ -36,7 +31,7 @@ module Lhm
|
|
36
31
|
|
37
32
|
def validate
|
38
33
|
unless @connection.data_source_exists?(@origin.name) &&
|
39
|
-
|
34
|
+
@connection.data_source_exists?(@destination.name)
|
40
35
|
error "`#{ @origin.name }` and `#{ @destination.name }` must exist"
|
41
36
|
end
|
42
37
|
end
|
@@ -44,9 +39,7 @@ module Lhm
|
|
44
39
|
private
|
45
40
|
|
46
41
|
def execute
|
47
|
-
@
|
48
|
-
retriable_connection.execute atomic_switch
|
49
|
-
end
|
42
|
+
@connection.execute(atomic_switch, should_retry: true, retry_options: @retry_options)
|
50
43
|
end
|
51
44
|
end
|
52
45
|
end
|
data/lib/lhm/chunk_insert.rb
CHANGED
@@ -1,24 +1,18 @@
|
|
1
1
|
require 'lhm/sql_retry'
|
2
|
+
require 'lhm/proxysql_helper'
|
2
3
|
|
3
4
|
module Lhm
|
4
5
|
class ChunkInsert
|
5
|
-
def initialize(migration, connection, lowest, highest,
|
6
|
+
def initialize(migration, connection, lowest, highest, retry_options = {})
|
6
7
|
@migration = migration
|
7
8
|
@connection = connection
|
8
9
|
@lowest = lowest
|
9
10
|
@highest = highest
|
10
|
-
@
|
11
|
-
@connection,
|
12
|
-
{
|
13
|
-
log_prefix: "Chunker Insert"
|
14
|
-
}.merge!(options.fetch(:retriable, {}))
|
15
|
-
)
|
11
|
+
@retry_options = retry_options
|
16
12
|
end
|
17
13
|
|
18
14
|
def insert_and_return_count_of_rows_created
|
19
|
-
@
|
20
|
-
retriable_connection.update sql
|
21
|
-
end
|
15
|
+
@connection.update(sql, should_retry: true, retry_options: @retry_options)
|
22
16
|
end
|
23
17
|
|
24
18
|
def sql
|
data/lib/lhm/chunker.rb
CHANGED
@@ -28,11 +28,12 @@ module Lhm
|
|
28
28
|
@start = @chunk_finder.start
|
29
29
|
@limit = @chunk_finder.limit
|
30
30
|
@printer = options[:printer] || Printer::Percentage.new
|
31
|
+
@retry_options = options[:retriable] || {}
|
31
32
|
@retry_helper = SqlRetry.new(
|
32
33
|
@connection,
|
33
|
-
{
|
34
|
+
retry_options: {
|
34
35
|
log_prefix: "Chunker"
|
35
|
-
}.merge!(
|
36
|
+
}.merge!(@retry_options)
|
36
37
|
)
|
37
38
|
end
|
38
39
|
|
@@ -46,7 +47,7 @@ module Lhm
|
|
46
47
|
top = upper_id(@next_to_insert, stride)
|
47
48
|
verify_can_run
|
48
49
|
|
49
|
-
affected_rows = ChunkInsert.new(@migration, @connection, bottom, top, @
|
50
|
+
affected_rows = ChunkInsert.new(@migration, @connection, bottom, top, @retry_options).insert_and_return_count_of_rows_created
|
50
51
|
expected_rows = top - bottom + 1
|
51
52
|
|
52
53
|
# Only log the chunker progress every 5 minutes instead of every iteration
|
@@ -78,7 +79,7 @@ module Lhm
|
|
78
79
|
private
|
79
80
|
|
80
81
|
def raise_on_non_pk_duplicate_warning
|
81
|
-
@connection.
|
82
|
+
@connection.execute("show warnings", should_retry: true, retry_options: @retry_options).each do |level, code, message|
|
82
83
|
unless message.match?(/Duplicate entry .+ for key 'PRIMARY'/)
|
83
84
|
m = "Unexpected warning found for inserted row: #{message}"
|
84
85
|
Lhm.logger.warn(m)
|
@@ -93,16 +94,14 @@ module Lhm
|
|
93
94
|
|
94
95
|
def verify_can_run
|
95
96
|
return unless @verifier
|
96
|
-
@retry_helper.with_retries do |retriable_connection|
|
97
|
+
@retry_helper.with_retries(@retry_options) do |retriable_connection|
|
97
98
|
raise "Verification failed, aborting early" if !@verifier.call(retriable_connection)
|
98
99
|
end
|
99
100
|
end
|
100
101
|
|
101
102
|
def upper_id(next_id, stride)
|
102
103
|
sql = "select id from `#{ @migration.origin_name }` where id >= #{ next_id } order by id limit 1 offset #{ stride - 1}"
|
103
|
-
top = @
|
104
|
-
retriable_connection.select_value(sql)
|
105
|
-
end
|
104
|
+
top = @connection.select_value(sql, should_retry: true, retry_options: @retry_options)
|
106
105
|
|
107
106
|
[top ? top.to_i : @limit, @limit].min
|
108
107
|
end
|
data/lib/lhm/cleanup/current.rb
CHANGED
@@ -4,17 +4,12 @@ require 'lhm/sql_retry'
|
|
4
4
|
module Lhm
|
5
5
|
module Cleanup
|
6
6
|
class Current
|
7
|
-
def initialize(run, origin_table_name, connection, options
|
7
|
+
def initialize(run, origin_table_name, connection, options={})
|
8
8
|
@run = run
|
9
9
|
@table_name = TableName.new(origin_table_name)
|
10
10
|
@connection = connection
|
11
11
|
@ddls = []
|
12
|
-
@
|
13
|
-
@connection,
|
14
|
-
{
|
15
|
-
log_prefix: "Cleanup::Current"
|
16
|
-
}.merge!(options.fetch(:retriable, {}))
|
17
|
-
)
|
12
|
+
@retry_config = options[:retriable] || {}
|
18
13
|
end
|
19
14
|
|
20
15
|
attr_reader :run, :connection, :ddls
|
@@ -59,9 +54,7 @@ module Lhm
|
|
59
54
|
|
60
55
|
def execute_ddls
|
61
56
|
ddls.each do |ddl|
|
62
|
-
|
63
|
-
retriable_connection.execute(ddl)
|
64
|
-
end
|
57
|
+
@connection.execute(ddl, should_retry: true, retry_options: @retry_config)
|
65
58
|
end
|
66
59
|
Lhm.logger.info("Dropped triggers on #{@lhm_triggers_for_origin.join(', ')}")
|
67
60
|
Lhm.logger.info("Dropped tables #{@lhm_triggers_for_origin.join(', ')}")
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
require 'lhm/sql_retry'
|
3
|
+
|
4
|
+
module Lhm
|
5
|
+
# Lhm::Connection inherits from SingleDelegator. It will forward any unknown method calls to the ActiveRecord
|
6
|
+
# connection.
|
7
|
+
class Connection < SimpleDelegator
|
8
|
+
|
9
|
+
def initialize(connection:, default_log_prefix: nil, options: {})
|
10
|
+
@default_log_prefix = default_log_prefix
|
11
|
+
@sql_retry = Lhm::SqlRetry.new(
|
12
|
+
connection,
|
13
|
+
retry_options: options[:retriable] || {},
|
14
|
+
reconnect_with_consistent_host: options[:reconnect_with_consistent_host] || false
|
15
|
+
)
|
16
|
+
|
17
|
+
# Creates delegation for the ActiveRecord Connection
|
18
|
+
super(connection)
|
19
|
+
end
|
20
|
+
|
21
|
+
def ar_connection
|
22
|
+
# Get object from the simple delegator
|
23
|
+
__getobj__
|
24
|
+
end
|
25
|
+
|
26
|
+
def ar_connection=(connection)
|
27
|
+
raise Lhm::Error.new("Lhm::Connection requires an active record connection to operate") if connection.nil?
|
28
|
+
|
29
|
+
@sql_retry.connection = connection
|
30
|
+
# Sets connection as the delegated object
|
31
|
+
__setobj__(connection)
|
32
|
+
end
|
33
|
+
|
34
|
+
def process_connection_options(options)
|
35
|
+
# If any other flags are added. Add the "processing" here
|
36
|
+
@sql_retry.reconnect_with_consistent_host = options[:reconnect_with_consistent_host] || false
|
37
|
+
end
|
38
|
+
|
39
|
+
def execute(query, should_retry: false, retry_options: {})
|
40
|
+
if should_retry
|
41
|
+
exec_with_retries(:execute, query, retry_options)
|
42
|
+
else
|
43
|
+
exec(:execute, query)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def update(query, should_retry: false, retry_options: {})
|
48
|
+
if should_retry
|
49
|
+
exec_with_retries(:update, query, retry_options)
|
50
|
+
else
|
51
|
+
exec(:update, query)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def select_value(query, should_retry: false, retry_options: {})
|
56
|
+
if should_retry
|
57
|
+
exec_with_retries(:select_value, query, retry_options)
|
58
|
+
else
|
59
|
+
exec(:select_value, query)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def select_values(query, should_retry: false, retry_options: {})
|
64
|
+
if should_retry
|
65
|
+
exec_with_retries(:select_values, query, retry_options)
|
66
|
+
else
|
67
|
+
exec(:select_values, query)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def select_one(query, should_retry: false, retry_options: {})
|
72
|
+
if should_retry
|
73
|
+
exec_with_retries(:select_one, query, retry_options)
|
74
|
+
else
|
75
|
+
exec(:select_one, query)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def exec(method, sql)
|
82
|
+
ar_connection.public_send(method, Lhm::ProxySQLHelper.tagged(sql))
|
83
|
+
end
|
84
|
+
|
85
|
+
def exec_with_retries(method, sql, retry_options = {})
|
86
|
+
retry_options[:log_prefix] ||= file
|
87
|
+
@sql_retry.with_retries(retry_options) do |conn|
|
88
|
+
conn.public_send(method, Lhm::ProxySQLHelper.tagged(sql))
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns camelized file name of caller (e.g. chunk_insert.rb -> ChunkInsert)
|
93
|
+
def file
|
94
|
+
# Find calling file and extract name
|
95
|
+
/[\/]*(\w+).rb:\d+:in/.match(relevant_caller)
|
96
|
+
name = $1&.camelize || "Connection"
|
97
|
+
"#{name}"
|
98
|
+
end
|
99
|
+
|
100
|
+
def relevant_caller
|
101
|
+
lhm_stack = caller.select { |x| x.include?("/lhm") }
|
102
|
+
first_candidate_index = lhm_stack.find_index { |line| !line.include?(__FILE__) }
|
103
|
+
|
104
|
+
# Find the file that called the `#execute` (fallbacks to current file)
|
105
|
+
return lhm_stack.first unless first_candidate_index
|
106
|
+
lhm_stack.at(first_candidate_index)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
data/lib/lhm/entangler.rb
CHANGED
@@ -4,6 +4,7 @@
|
|
4
4
|
require 'lhm/command'
|
5
5
|
require 'lhm/sql_helper'
|
6
6
|
require 'lhm/sql_retry'
|
7
|
+
require 'lhm/connection'
|
7
8
|
|
8
9
|
module Lhm
|
9
10
|
class Entangler
|
@@ -19,12 +20,7 @@ module Lhm
|
|
19
20
|
@origin = migration.origin
|
20
21
|
@destination = migration.destination
|
21
22
|
@connection = connection
|
22
|
-
@
|
23
|
-
@connection,
|
24
|
-
{
|
25
|
-
log_prefix: "Entangler"
|
26
|
-
}.merge!(options.fetch(:retriable, {}))
|
27
|
-
)
|
23
|
+
@retry_options = options[:retriable] || {}
|
28
24
|
end
|
29
25
|
|
30
26
|
def entangle
|
@@ -90,18 +86,14 @@ module Lhm
|
|
90
86
|
|
91
87
|
def before
|
92
88
|
entangle.each do |stmt|
|
93
|
-
@
|
94
|
-
retriable_connection.execute(stmt)
|
95
|
-
end
|
89
|
+
@connection.execute(stmt, should_retry: true, retry_options: @retry_options)
|
96
90
|
end
|
97
91
|
Lhm.logger.info("Created triggers on #{@origin.name}")
|
98
92
|
end
|
99
93
|
|
100
94
|
def after
|
101
95
|
untangle.each do |stmt|
|
102
|
-
@
|
103
|
-
retriable_connection.execute(stmt)
|
104
|
-
end
|
96
|
+
@connection.execute(stmt, should_retry: true, retry_options: @retry_options)
|
105
97
|
end
|
106
98
|
Lhm.logger.info("Dropped triggers on #{@origin.name}")
|
107
99
|
end
|
data/lib/lhm/invoker.rb
CHANGED
@@ -16,8 +16,8 @@ module Lhm
|
|
16
16
|
class Invoker
|
17
17
|
include SqlHelper
|
18
18
|
LOCK_WAIT_TIMEOUT_DELTA = 10
|
19
|
-
|
20
|
-
|
19
|
+
INNODB_LOCK_WAIT_TIMEOUT_MAX=1073741824.freeze # https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_lock_wait_timeout
|
20
|
+
LOCK_WAIT_TIMEOUT_MAX=31536000.freeze # https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html
|
21
21
|
|
22
22
|
attr_reader :migrator, :connection
|
23
23
|
|
@@ -56,7 +56,7 @@ module Lhm
|
|
56
56
|
Chunker.new(migration, @connection, options).run
|
57
57
|
raise "Required triggers do not exist" unless triggers_still_exist?(@connection, entangler)
|
58
58
|
if options[:atomic_switch]
|
59
|
-
AtomicSwitcher.new(migration, @connection
|
59
|
+
AtomicSwitcher.new(migration, @connection).run
|
60
60
|
else
|
61
61
|
LockedSwitcher.new(migration, @connection).run
|
62
62
|
end
|
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,119 @@ 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
|
+
|
22
|
+
MYSQL_VAR_NAMES = {
|
23
|
+
hostname: "@@global.hostname",
|
24
|
+
server_id: "@@global.server_id",
|
25
|
+
version_comment: "@@version_comment",
|
26
|
+
}
|
27
|
+
|
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
|
+
def initialize(connection, retry_options: {}, reconnect_with_consistent_host: false)
|
19
32
|
@connection = connection
|
20
|
-
@log_prefix =
|
21
|
-
@
|
33
|
+
@log_prefix = retry_options.delete(:log_prefix)
|
34
|
+
@global_retry_config = default_retry_config.dup.merge!(retry_options)
|
35
|
+
if (@reconnect_with_consistent_host = reconnect_with_consistent_host)
|
36
|
+
@initial_hostname = hostname
|
37
|
+
@initial_server_id = server_id
|
38
|
+
end
|
22
39
|
end
|
23
40
|
|
24
|
-
|
41
|
+
# Complete explanation of algorithm: https://github.com/Shopify/lhm/pull/112
|
42
|
+
def with_retries(retry_config = {})
|
43
|
+
@log_prefix = retry_config.delete(:log_prefix)
|
44
|
+
|
45
|
+
retry_config = @global_retry_config.dup.merge!(retry_config)
|
46
|
+
|
25
47
|
Retriable.retriable(retry_config) do
|
26
|
-
|
48
|
+
# Using begin -> rescue -> end for Ruby 2.4 compatibility
|
49
|
+
begin
|
50
|
+
if @reconnect_with_consistent_host
|
51
|
+
raise Lhm::Error.new("Could not reconnected to initial MySQL host. 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
|
+
reconnect_with_host_check! if @reconnect_with_consistent_host
|
61
|
+
end
|
27
62
|
end
|
28
63
|
end
|
29
64
|
|
30
|
-
attr_reader :
|
65
|
+
attr_reader :global_retry_config
|
66
|
+
attr_accessor :connection, :reconnect_with_consistent_host
|
31
67
|
|
32
68
|
private
|
33
69
|
|
70
|
+
def hostname
|
71
|
+
mysql_single_value(MYSQL_VAR_NAMES[:hostname])
|
72
|
+
end
|
73
|
+
|
74
|
+
def server_id
|
75
|
+
mysql_single_value(MYSQL_VAR_NAMES[:server_id])
|
76
|
+
end
|
77
|
+
|
78
|
+
def cloudsql?
|
79
|
+
mysql_single_value(MYSQL_VAR_NAMES[:version_comment]).include?(CLOUDSQL_VERSION_COMMENT)
|
80
|
+
end
|
81
|
+
|
82
|
+
def mysql_single_value(name)
|
83
|
+
query = Lhm::ProxySQLHelper.tagged("SELECT #{name} LIMIT 1")
|
84
|
+
|
85
|
+
@connection.execute(query).to_a.first.tap do |record|
|
86
|
+
return record&.first
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def same_host_as_initial?
|
91
|
+
return @initial_server_id == server_id if cloudsql?
|
92
|
+
@initial_hostname == hostname
|
93
|
+
end
|
94
|
+
|
95
|
+
def log_with_prefix(message, level = :info)
|
96
|
+
message.prepend("[#{@log_prefix}] ") if @log_prefix
|
97
|
+
Lhm.logger.public_send(level, message)
|
98
|
+
end
|
99
|
+
|
100
|
+
def reconnect_with_host_check!
|
101
|
+
log_with_prefix("Lost connection to MySQL, will retry to connect to same host")
|
102
|
+
begin
|
103
|
+
Retriable.retriable(host_retry_config) do
|
104
|
+
# tries to reconnect. On failure will trigger a retry
|
105
|
+
@connection.reconnect!
|
106
|
+
|
107
|
+
if same_host_as_initial?
|
108
|
+
# This is not an actual error, but controlled way to get the parent `Retriable.retriable` to retry
|
109
|
+
# the statement that failed (since the Retriable gem only retries on errors).
|
110
|
+
raise ReconnectToHostSuccessful.new("LHM successfully reconnected to initial host: #{@initial_hostname} (server_id: #{@initial_server_id})")
|
111
|
+
else
|
112
|
+
# New Master --> abort LHM (reconnecting will not change anything)
|
113
|
+
raise Lhm::Error.new("Reconnected to wrong host. Started migration on: #{@initial_hostname} (server_id: #{@initial_server_id}), but reconnected to: #{hostname} (server_id: #{@initial_server_id}).")
|
114
|
+
end
|
115
|
+
end
|
116
|
+
rescue StandardError => e
|
117
|
+
# The parent Retriable.retriable is configured to retry if it encounters an error with the success message.
|
118
|
+
# Therefore, if the connection is re-established successfully AND the host is the same, LHM can retry the query
|
119
|
+
# that originally failed.
|
120
|
+
raise e if reconnect_successful?(e)
|
121
|
+
# If the connection was not successful, the parent retriable will raise any error that originated from the
|
122
|
+
# `@connection.reconnect!`
|
123
|
+
# Therefore, this error will cause the LHM to abort
|
124
|
+
raise Lhm::Error.new("LHM tried the reconnection procedure but failed. Latest error: #{e.message}")
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def reconnect_successful?(e)
|
129
|
+
e.is_a?(ReconnectToHostSuccessful)
|
130
|
+
end
|
131
|
+
|
34
132
|
# For a full list of configuration options see https://github.com/kamui/retriable
|
35
133
|
def default_retry_config
|
36
134
|
{
|
@@ -43,6 +141,11 @@ module Lhm
|
|
43
141
|
/Lost connection to MySQL server during query/,
|
44
142
|
/Max connect timeout reached/,
|
45
143
|
/Unknown MySQL server host/,
|
144
|
+
/connection is locked to hostgroup/,
|
145
|
+
/The MySQL server is running with the --read-only option so it cannot execute this statement/,
|
146
|
+
],
|
147
|
+
ReconnectToHostSuccessful => [
|
148
|
+
/#{RECONNECT_SUCCESSFUL_MESSAGE}/
|
46
149
|
]
|
47
150
|
},
|
48
151
|
multiplier: 1, # each successive interval grows by this factor
|
@@ -51,9 +154,29 @@ module Lhm
|
|
51
154
|
rand_factor: 0, # percentage to randomize the next retry interval time
|
52
155
|
max_elapsed_time: Float::INFINITY, # max total time in seconds that code is allowed to keep being retried
|
53
156
|
on_retry: Proc.new do |exception, try_number, total_elapsed_time, next_interval|
|
54
|
-
|
55
|
-
|
56
|
-
|
157
|
+
if reconnect_successful?(exception)
|
158
|
+
log_with_prefix("#{exception.message} -- triggering retry", :info)
|
159
|
+
else
|
160
|
+
log_with_prefix("#{exception.class}: '#{exception.message}' - #{try_number} tries in #{total_elapsed_time} seconds and #{next_interval} seconds until the next try.", :error)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
}.freeze
|
164
|
+
end
|
165
|
+
|
166
|
+
def host_retry_config
|
167
|
+
{
|
168
|
+
on: {
|
169
|
+
StandardError => [
|
170
|
+
/Lost connection to MySQL server at 'reading initial communication packet'/
|
171
|
+
]
|
172
|
+
},
|
173
|
+
multiplier: 1, # each successive interval grows by this factor
|
174
|
+
base_interval: 0.25, # the initial interval in seconds between tries.
|
175
|
+
tries: 20, # Number of attempts to make at running your code block (includes initial attempt).
|
176
|
+
rand_factor: 0, # percentage to randomize the next retry interval time
|
177
|
+
max_elapsed_time: Float::INFINITY, # max total time in seconds that code is allowed to keep being retried
|
178
|
+
on_retry: Proc.new do |exception, try_number, total_elapsed_time, next_interval|
|
179
|
+
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
180
|
end
|
58
181
|
}.freeze
|
59
182
|
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
|
#
|
@@ -81,16 +83,30 @@ module Lhm
|
|
81
83
|
Lhm::Cleanup::Current.new(run, table_name, connection, options).execute
|
82
84
|
end
|
83
85
|
|
84
|
-
|
85
|
-
|
86
|
+
# Setups DB connection
|
87
|
+
#
|
88
|
+
# @param [ActiveRecord::Base] connection ActiveRecord Connection
|
89
|
+
# @param [Hash] connection_options Optional options (defaults to: empty hash)
|
90
|
+
# @option connection_options [Boolean] :reconnect_with_consistent_host
|
91
|
+
# Active / Deactivate ProxySQL-aware reconnection procedure (default to: false)
|
92
|
+
def setup(connection, connection_options = {})
|
93
|
+
@@connection = Connection.new(connection: connection, options: connection_options)
|
86
94
|
end
|
87
95
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
96
|
+
# Setups DB connection
|
97
|
+
#
|
98
|
+
# @param [Hash] connection_options Optional options (defaults to: empty hash)
|
99
|
+
# @option connection_options [Boolean] :reconnect_with_consistent_host
|
100
|
+
# Active / Deactivate ProxySQL-aware reconnection procedure (default to: false)
|
101
|
+
def connection(connection_options = nil)
|
102
|
+
@@connection ||= begin
|
103
|
+
raise 'Please call Lhm.setup' unless defined?(ActiveRecord)
|
104
|
+
@@connection = Connection.new(connection: ActiveRecord::Base.connection, options: connection_options || {})
|
105
|
+
end
|
106
|
+
|
107
|
+
@@connection.process_connection_options(connection_options) unless connection_options.nil?
|
108
|
+
|
109
|
+
@@connection
|
94
110
|
end
|
95
111
|
|
96
112
|
def self.logger=(new_logger)
|
@@ -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;
|