lhm-shopify 3.5.0 → 3.5.4
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 +17 -4
- data/.gitignore +0 -2
- data/Appraisals +24 -0
- data/CHANGELOG.md +23 -0
- data/Gemfile.lock +66 -0
- data/README.md +53 -0
- data/Rakefile +6 -5
- data/dev.yml +18 -3
- data/docker-compose.yml +15 -3
- 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 -3
- data/lib/lhm/chunk_insert.rb +7 -3
- data/lib/lhm/chunker.rb +6 -6
- data/lib/lhm/cleanup/current.rb +4 -1
- data/lib/lhm/connection.rb +66 -19
- data/lib/lhm/entangler.rb +5 -4
- data/lib/lhm/invoker.rb +5 -3
- data/lib/lhm/locked_switcher.rb +2 -0
- data/lib/lhm/proxysql_helper.rb +10 -0
- data/lib/lhm/sql_retry.rb +135 -11
- data/lib/lhm/throttler/slave_lag.rb +19 -2
- data/lib/lhm/version.rb +1 -1
- data/lib/lhm.rb +32 -12
- data/scripts/mysql/writer/create_users.sql +3 -0
- data/spec/integration/atomic_switcher_spec.rb +38 -10
- data/spec/integration/chunk_insert_spec.rb +2 -1
- data/spec/integration/chunker_spec.rb +8 -6
- data/spec/integration/database.yml +10 -0
- data/spec/integration/entangler_spec.rb +3 -1
- data/spec/integration/integration_helper.rb +20 -4
- 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 +19 -9
- 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 +51 -8
- data/spec/unit/entangler_spec.rb +71 -19
- data/spec/unit/lhm_spec.rb +17 -0
- data/spec/unit/throttler/slave_lag_spec.rb +14 -9
- metadata +76 -11
- 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,12 +16,13 @@ module Lhm
|
|
16
16
|
|
17
17
|
attr_reader :connection
|
18
18
|
|
19
|
-
|
19
|
+
LOG_PREFIX = "AtomicSwitcher"
|
20
|
+
|
21
|
+
def initialize(migration, connection = nil)
|
20
22
|
@migration = migration
|
21
23
|
@connection = connection
|
22
24
|
@origin = migration.origin
|
23
25
|
@destination = migration.destination
|
24
|
-
@retry_options = options[:retriable] || {}
|
25
26
|
end
|
26
27
|
|
27
28
|
def atomic_switch
|
@@ -39,7 +40,7 @@ module Lhm
|
|
39
40
|
private
|
40
41
|
|
41
42
|
def execute
|
42
|
-
@connection.execute(atomic_switch,
|
43
|
+
@connection.execute(atomic_switch, should_retry: true, log_prefix: LOG_PREFIX)
|
43
44
|
end
|
44
45
|
end
|
45
46
|
end
|
data/lib/lhm/chunk_insert.rb
CHANGED
@@ -1,17 +1,21 @@
|
|
1
1
|
require 'lhm/sql_retry'
|
2
|
+
require 'lhm/proxysql_helper'
|
2
3
|
|
3
4
|
module Lhm
|
4
5
|
class ChunkInsert
|
5
|
-
|
6
|
+
|
7
|
+
LOG_PREFIX = "ChunkInsert"
|
8
|
+
|
9
|
+
def initialize(migration, connection, lowest, highest, retry_options = {})
|
6
10
|
@migration = migration
|
7
11
|
@connection = connection
|
8
12
|
@lowest = lowest
|
9
13
|
@highest = highest
|
10
|
-
@retry_options =
|
14
|
+
@retry_options = retry_options
|
11
15
|
end
|
12
16
|
|
13
17
|
def insert_and_return_count_of_rows_created
|
14
|
-
@connection.update(sql,
|
18
|
+
@connection.update(sql, should_retry: true, log_prefix: LOG_PREFIX)
|
15
19
|
end
|
16
20
|
|
17
21
|
def sql
|
data/lib/lhm/chunker.rb
CHANGED
@@ -13,6 +13,8 @@ module Lhm
|
|
13
13
|
|
14
14
|
attr_reader :connection
|
15
15
|
|
16
|
+
LOG_PREFIX = "Chunker"
|
17
|
+
|
16
18
|
# Copy from origin to destination in chunks of size `stride`.
|
17
19
|
# Use the `throttler` class to sleep between each stride.
|
18
20
|
def initialize(migration, connection = nil, options = {})
|
@@ -31,9 +33,7 @@ module Lhm
|
|
31
33
|
@retry_options = options[:retriable] || {}
|
32
34
|
@retry_helper = SqlRetry.new(
|
33
35
|
@connection,
|
34
|
-
|
35
|
-
log_prefix: "Chunker"
|
36
|
-
}.merge!(@retry_options)
|
36
|
+
retry_options: @retry_options
|
37
37
|
)
|
38
38
|
end
|
39
39
|
|
@@ -79,7 +79,7 @@ module Lhm
|
|
79
79
|
private
|
80
80
|
|
81
81
|
def raise_on_non_pk_duplicate_warning
|
82
|
-
@connection.execute("show warnings",
|
82
|
+
@connection.execute("show warnings", should_retry: true, log_prefix: LOG_PREFIX).each do |level, code, message|
|
83
83
|
unless message.match?(/Duplicate entry .+ for key 'PRIMARY'/)
|
84
84
|
m = "Unexpected warning found for inserted row: #{message}"
|
85
85
|
Lhm.logger.warn(m)
|
@@ -94,14 +94,14 @@ module Lhm
|
|
94
94
|
|
95
95
|
def verify_can_run
|
96
96
|
return unless @verifier
|
97
|
-
@retry_helper.with_retries(
|
97
|
+
@retry_helper.with_retries(log_prefix: LOG_PREFIX) do |retriable_connection|
|
98
98
|
raise "Verification failed, aborting early" if !@verifier.call(retriable_connection)
|
99
99
|
end
|
100
100
|
end
|
101
101
|
|
102
102
|
def upper_id(next_id, stride)
|
103
103
|
sql = "select id from `#{ @migration.origin_name }` where id >= #{ next_id } order by id limit 1 offset #{ stride - 1}"
|
104
|
-
top = @connection.select_value(sql,
|
104
|
+
top = @connection.select_value(sql, should_retry: true, log_prefix: LOG_PREFIX)
|
105
105
|
|
106
106
|
[top ? top.to_i : @limit, @limit].min
|
107
107
|
end
|
data/lib/lhm/cleanup/current.rb
CHANGED
@@ -4,6 +4,9 @@ require 'lhm/sql_retry'
|
|
4
4
|
module Lhm
|
5
5
|
module Cleanup
|
6
6
|
class Current
|
7
|
+
|
8
|
+
LOG_PREFIX = "Current"
|
9
|
+
|
7
10
|
def initialize(run, origin_table_name, connection, options={})
|
8
11
|
@run = run
|
9
12
|
@table_name = TableName.new(origin_table_name)
|
@@ -54,7 +57,7 @@ module Lhm
|
|
54
57
|
|
55
58
|
def execute_ddls
|
56
59
|
ddls.each do |ddl|
|
57
|
-
@connection.execute(ddl,
|
60
|
+
@connection.execute(ddl, should_retry: true, log_prefix: LOG_PREFIX)
|
58
61
|
end
|
59
62
|
Lhm.logger.info("Dropped triggers on #{@lhm_triggers_for_origin.join(', ')}")
|
60
63
|
Lhm.logger.info("Dropped tables #{@lhm_triggers_for_origin.join(', ')}")
|
data/lib/lhm/connection.rb
CHANGED
@@ -1,43 +1,90 @@
|
|
1
1
|
require 'delegate'
|
2
|
+
require 'forwardable'
|
3
|
+
require 'lhm/sql_retry'
|
2
4
|
|
3
5
|
module Lhm
|
4
|
-
class Connection < SimpleDelegator
|
5
|
-
|
6
6
|
# Lhm::Connection inherits from SingleDelegator. It will forward any unknown method calls to the ActiveRecord
|
7
7
|
# connection.
|
8
|
-
|
9
|
-
|
8
|
+
class Connection < SimpleDelegator
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
# Will delegate the following function to @sql_retry object, while leaving them accessible from the Lhm::Connection
|
12
|
+
# object
|
13
|
+
def_delegators :@sql_retry, :reconnect_with_consistent_host, :reconnect_with_consistent_host=, :retry_config=
|
14
|
+
|
15
|
+
alias ar_connection __getobj__
|
10
16
|
|
11
|
-
def initialize(connection:,
|
12
|
-
@default_log_prefix = default_log_prefix
|
13
|
-
@retry_options = retry_options || default_retry_config
|
17
|
+
def initialize(connection:, options: {})
|
14
18
|
@sql_retry = Lhm::SqlRetry.new(
|
15
19
|
connection,
|
16
|
-
retry_options,
|
20
|
+
retry_options: options[:retriable] || {},
|
21
|
+
reconnect_with_consistent_host: options[:reconnect_with_consistent_host] || false
|
17
22
|
)
|
18
23
|
|
19
24
|
# Creates delegation for the ActiveRecord Connection
|
20
25
|
super(connection)
|
21
26
|
end
|
22
27
|
|
23
|
-
def
|
24
|
-
|
28
|
+
def ar_connection=(connection)
|
29
|
+
raise Lhm::Error.new("Lhm::Connection requires an active record connection to operate") if connection.nil?
|
30
|
+
|
31
|
+
@sql_retry.connection = connection
|
32
|
+
# Sets connection as the delegated object
|
33
|
+
__setobj__(connection)
|
25
34
|
end
|
26
35
|
|
27
|
-
|
28
|
-
|
36
|
+
# ActiveRecord::Base overridden methods to incorporate custom retry logic
|
37
|
+
# All other methods will be delegated
|
38
|
+
def execute(query, should_retry: false, log_prefix: nil)
|
39
|
+
if should_retry
|
40
|
+
exec_with_retries(:execute, query, log_prefix)
|
41
|
+
else
|
42
|
+
exec(:execute, query)
|
43
|
+
end
|
29
44
|
end
|
30
45
|
|
31
|
-
def
|
32
|
-
|
46
|
+
def update(query, should_retry: false, log_prefix: nil)
|
47
|
+
if should_retry
|
48
|
+
exec_with_retries(:update, query, log_prefix)
|
49
|
+
else
|
50
|
+
exec(:update, query)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def select_value(query, should_retry: false, log_prefix: nil)
|
55
|
+
if should_retry
|
56
|
+
exec_with_retries(:select_value, query, log_prefix)
|
57
|
+
else
|
58
|
+
exec(:select_value, query)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def select_values(query, should_retry: false, log_prefix: nil)
|
63
|
+
if should_retry
|
64
|
+
exec_with_retries(:select_values, query, log_prefix)
|
65
|
+
else
|
66
|
+
exec(:select_values, query)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def select_one(query, should_retry: false, log_prefix: nil)
|
71
|
+
if should_retry
|
72
|
+
exec_with_retries(:select_one, query, log_prefix)
|
73
|
+
else
|
74
|
+
exec(:select_one, query)
|
75
|
+
end
|
33
76
|
end
|
34
77
|
|
35
78
|
private
|
36
79
|
|
37
|
-
def
|
38
|
-
|
39
|
-
|
40
|
-
|
80
|
+
def exec(method, sql)
|
81
|
+
ar_connection.public_send(method, Lhm::ProxySQLHelper.tagged(sql))
|
82
|
+
end
|
83
|
+
|
84
|
+
def exec_with_retries(method, sql, log_prefix=nil)
|
85
|
+
effective_log_prefix = log_prefix || file
|
86
|
+
@sql_retry.with_retries(log_prefix: effective_log_prefix) do |conn|
|
87
|
+
conn.public_send(method, Lhm::ProxySQLHelper.tagged(sql))
|
41
88
|
end
|
42
89
|
end
|
43
90
|
|
@@ -51,7 +98,7 @@ module Lhm
|
|
51
98
|
|
52
99
|
def relevant_caller
|
53
100
|
lhm_stack = caller.select { |x| x.include?("/lhm") }
|
54
|
-
first_candidate_index = lhm_stack.find_index {|line| !line.include?(__FILE__)}
|
101
|
+
first_candidate_index = lhm_stack.find_index { |line| !line.include?(__FILE__) }
|
55
102
|
|
56
103
|
# Find the file that called the `#execute` (fallbacks to current file)
|
57
104
|
return lhm_stack.first unless first_candidate_index
|
data/lib/lhm/entangler.rb
CHANGED
@@ -13,14 +13,15 @@ module Lhm
|
|
13
13
|
|
14
14
|
attr_reader :connection
|
15
15
|
|
16
|
+
LOG_PREFIX = "Entangler"
|
17
|
+
|
16
18
|
# Creates entanglement between two tables. All creates, updates and deletes
|
17
19
|
# to origin will be repeated on the destination table.
|
18
|
-
def initialize(migration, connection = nil
|
20
|
+
def initialize(migration, connection = nil)
|
19
21
|
@intersection = migration.intersection
|
20
22
|
@origin = migration.origin
|
21
23
|
@destination = migration.destination
|
22
24
|
@connection = connection
|
23
|
-
@retry_options = options[:retriable] || {}
|
24
25
|
end
|
25
26
|
|
26
27
|
def entangle
|
@@ -86,14 +87,14 @@ module Lhm
|
|
86
87
|
|
87
88
|
def before
|
88
89
|
entangle.each do |stmt|
|
89
|
-
@connection.execute(stmt,
|
90
|
+
@connection.execute(stmt, should_retry: true, log_prefix: LOG_PREFIX)
|
90
91
|
end
|
91
92
|
Lhm.logger.info("Created triggers on #{@origin.name}")
|
92
93
|
end
|
93
94
|
|
94
95
|
def after
|
95
96
|
untangle.each do |stmt|
|
96
|
-
@connection.execute(stmt,
|
97
|
+
@connection.execute(stmt, should_retry: true, log_prefix: LOG_PREFIX)
|
97
98
|
end
|
98
99
|
Lhm.logger.info("Dropped triggers on #{@origin.name}")
|
99
100
|
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
|
|
@@ -49,7 +49,7 @@ module Lhm
|
|
49
49
|
normalize_options(options)
|
50
50
|
set_session_lock_wait_timeouts
|
51
51
|
migration = @migrator.run
|
52
|
-
entangler = Entangler.new(migration, @connection
|
52
|
+
entangler = Entangler.new(migration, @connection)
|
53
53
|
|
54
54
|
entangler.run do
|
55
55
|
options[:verifier] ||= Proc.new { |conn| triggers_still_exist?(conn, entangler) }
|
@@ -90,6 +90,8 @@ module Lhm
|
|
90
90
|
options[:throttler] = Lhm.throttler
|
91
91
|
end
|
92
92
|
|
93
|
+
Lhm.connection.retry_config = options[:retriable] || {}
|
94
|
+
|
93
95
|
rescue => e
|
94
96
|
Lhm.logger.error "LHM run failed with exception=#{e.class} message=#{e.message}"
|
95
97
|
raise
|
data/lib/lhm/locked_switcher.rb
CHANGED
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,26 +16,123 @@ 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
|
-
|
33
|
+
self.retry_config = retry_options
|
34
|
+
self.reconnect_with_consistent_host = reconnect_with_consistent_host
|
21
35
|
end
|
22
36
|
|
23
|
-
|
24
|
-
|
25
|
-
@log_prefix =
|
26
|
-
|
27
|
-
|
37
|
+
# Complete explanation of algorithm: https://github.com/Shopify/lhm/pull/112
|
38
|
+
def with_retries(log_prefix: nil)
|
39
|
+
@log_prefix = log_prefix || "" # No prefix. Just logs
|
40
|
+
|
41
|
+
Retriable.retriable(@retry_config) do
|
42
|
+
# Using begin -> rescue -> end for Ruby 2.4 compatibility
|
43
|
+
begin
|
44
|
+
if @reconnect_with_consistent_host
|
45
|
+
raise Lhm::Error.new("Could not reconnected to initial MySQL host. Aborting to avoid data-loss") unless same_host_as_initial?
|
46
|
+
end
|
47
|
+
|
48
|
+
yield(@connection)
|
49
|
+
rescue StandardError => e
|
50
|
+
# Not all errors should trigger a reconnect. Some errors such be raised and abort the LHM (such as reconnecting to the wrong host).
|
51
|
+
# The error will be raised the connection is still active (i.e. no need to reconnect) or if the connection is
|
52
|
+
# dead (i.e. not active) and @reconnect_with_host is false (i.e. instructed not to reconnect)
|
53
|
+
raise e if @connection.active? || (!@connection.active? && !@reconnect_with_consistent_host)
|
54
|
+
reconnect_with_host_check! if @reconnect_with_consistent_host
|
55
|
+
end
|
28
56
|
end
|
29
57
|
end
|
30
58
|
|
31
|
-
|
59
|
+
# Both attributes will have defined setters
|
60
|
+
attr_reader :retry_config, :reconnect_with_consistent_host
|
61
|
+
attr_accessor :connection
|
62
|
+
|
63
|
+
def retry_config=(retry_options)
|
64
|
+
@retry_config = default_retry_config.dup.merge!(retry_options)
|
65
|
+
end
|
66
|
+
|
67
|
+
def reconnect_with_consistent_host=(reconnect)
|
68
|
+
if (@reconnect_with_consistent_host = reconnect)
|
69
|
+
@initial_hostname = hostname
|
70
|
+
@initial_server_id = server_id
|
71
|
+
end
|
72
|
+
end
|
32
73
|
|
33
74
|
private
|
34
75
|
|
76
|
+
def hostname
|
77
|
+
mysql_single_value(MYSQL_VAR_NAMES[:hostname])
|
78
|
+
end
|
79
|
+
|
80
|
+
def server_id
|
81
|
+
mysql_single_value(MYSQL_VAR_NAMES[:server_id])
|
82
|
+
end
|
83
|
+
|
84
|
+
def cloudsql?
|
85
|
+
mysql_single_value(MYSQL_VAR_NAMES[:version_comment]).include?(CLOUDSQL_VERSION_COMMENT)
|
86
|
+
end
|
87
|
+
|
88
|
+
def mysql_single_value(name)
|
89
|
+
query = Lhm::ProxySQLHelper.tagged("SELECT #{name} LIMIT 1")
|
90
|
+
|
91
|
+
@connection.execute(query).to_a.first.tap do |record|
|
92
|
+
return record&.first
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def same_host_as_initial?
|
97
|
+
return @initial_server_id == server_id if cloudsql?
|
98
|
+
@initial_hostname == hostname
|
99
|
+
end
|
100
|
+
|
35
101
|
def log_with_prefix(message, level = :info)
|
36
102
|
message.prepend("[#{@log_prefix}] ") if @log_prefix
|
37
|
-
Lhm.logger.
|
103
|
+
Lhm.logger.public_send(level, message)
|
104
|
+
end
|
105
|
+
|
106
|
+
def reconnect_with_host_check!
|
107
|
+
log_with_prefix("Lost connection to MySQL, will retry to connect to same host")
|
108
|
+
begin
|
109
|
+
Retriable.retriable(host_retry_config) do
|
110
|
+
# tries to reconnect. On failure will trigger a retry
|
111
|
+
@connection.reconnect!
|
112
|
+
|
113
|
+
if same_host_as_initial?
|
114
|
+
# This is not an actual error, but controlled way to get the parent `Retriable.retriable` to retry
|
115
|
+
# the statement that failed (since the Retriable gem only retries on errors).
|
116
|
+
raise ReconnectToHostSuccessful.new("LHM successfully reconnected to initial host: #{@initial_hostname} (server_id: #{@initial_server_id})")
|
117
|
+
else
|
118
|
+
# New Master --> abort LHM (reconnecting will not change anything)
|
119
|
+
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}).")
|
120
|
+
end
|
121
|
+
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
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def reconnect_successful?(e)
|
135
|
+
e.is_a?(ReconnectToHostSuccessful)
|
38
136
|
end
|
39
137
|
|
40
138
|
# For a full list of configuration options see https://github.com/kamui/retriable
|
@@ -49,6 +147,11 @@ module Lhm
|
|
49
147
|
/Lost connection to MySQL server during query/,
|
50
148
|
/Max connect timeout reached/,
|
51
149
|
/Unknown MySQL server host/,
|
150
|
+
/connection is locked to hostgroup/,
|
151
|
+
/The MySQL server is running with the --read-only option so it cannot execute this statement/,
|
152
|
+
],
|
153
|
+
ReconnectToHostSuccessful => [
|
154
|
+
/#{RECONNECT_SUCCESSFUL_MESSAGE}/
|
52
155
|
]
|
53
156
|
},
|
54
157
|
multiplier: 1, # each successive interval grows by this factor
|
@@ -57,8 +160,29 @@ module Lhm
|
|
57
160
|
rand_factor: 0, # percentage to randomize the next retry interval time
|
58
161
|
max_elapsed_time: Float::INFINITY, # max total time in seconds that code is allowed to keep being retried
|
59
162
|
on_retry: Proc.new do |exception, try_number, total_elapsed_time, next_interval|
|
60
|
-
|
61
|
-
|
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
|
+
on_retry: Proc.new do |exception, try_number, total_elapsed_time, next_interval|
|
185
|
+
log_with_prefix("#{exception.class}: '#{exception.message}' - #{try_number} tries in #{total_elapsed_time} seconds and #{next_interval} seconds until the next try.", :error)
|
62
186
|
end
|
63
187
|
}.freeze
|
64
188
|
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,7 @@ 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'
|
11
12
|
require 'lhm/connection'
|
12
13
|
require 'lhm/test_support'
|
13
14
|
require 'lhm/railtie' if defined?(Rails::Railtie)
|
@@ -27,7 +28,7 @@ module Lhm
|
|
27
28
|
extend Throttler
|
28
29
|
extend self
|
29
30
|
|
30
|
-
DEFAULT_LOGGER_OPTIONS =
|
31
|
+
DEFAULT_LOGGER_OPTIONS = { level: Logger::INFO, file: STDOUT }
|
31
32
|
|
32
33
|
# Alters a table with the changes described in the block
|
33
34
|
#
|
@@ -45,15 +46,19 @@ module Lhm
|
|
45
46
|
# Use atomic switch to rename tables (defaults to: true)
|
46
47
|
# If using a version of mysql affected by atomic switch bug, LHM forces user
|
47
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)
|
48
51
|
# @yield [Migrator] Yielded Migrator object records the changes
|
49
52
|
# @return [Boolean] Returns true if the migration finishes
|
50
53
|
# @raise [Error] Raises Lhm::Error in case of a error and aborts the migration
|
51
54
|
def change_table(table_name, options = {}, &block)
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
57
62
|
end
|
58
63
|
|
59
64
|
# Cleanup tables and triggers
|
@@ -82,16 +87,19 @@ module Lhm
|
|
82
87
|
Lhm::Cleanup::Current.new(run, table_name, connection, options).execute
|
83
88
|
end
|
84
89
|
|
90
|
+
# Setups DB connection
|
91
|
+
#
|
92
|
+
# @param [ActiveRecord::Base] connection ActiveRecord Connection
|
85
93
|
def setup(connection)
|
86
|
-
@@connection =
|
94
|
+
@@connection = Connection.new(connection: connection)
|
87
95
|
end
|
88
96
|
|
97
|
+
# Returns DB connection (or initializes it if not created yet)
|
89
98
|
def connection
|
90
|
-
@@connection ||=
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
end
|
99
|
+
@@connection ||= begin
|
100
|
+
raise 'Please call Lhm.setup' unless defined?(ActiveRecord)
|
101
|
+
@@connection = Connection.new(connection: ActiveRecord::Base.connection)
|
102
|
+
end
|
95
103
|
end
|
96
104
|
|
97
105
|
def self.logger=(new_logger)
|
@@ -133,4 +141,16 @@ module Lhm
|
|
133
141
|
false
|
134
142
|
end
|
135
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
|
136
156
|
end
|
@@ -1,3 +1,6 @@
|
|
1
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
|
+
|
2
5
|
CREATE USER IF NOT EXISTS 'replication'@'%' IDENTIFIED BY 'password';
|
3
6
|
GRANT REPLICATION SLAVE ON *.* TO' replication'@'%' IDENTIFIED BY 'password';
|