lhm-shopify 4.0.0 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +20 -18
- data/Appraisals +5 -11
- data/CHANGELOG.md +9 -0
- data/Gemfile.lock +22 -7
- data/README.md +7 -7
- data/dev.yml +4 -1
- data/docker-compose-mysql-5.7.yml +1 -0
- data/docker-compose-mysql-8.0.yml +63 -0
- data/docker-compose.yml +3 -3
- data/gemfiles/activerecord_6.1.gemfile +1 -0
- data/gemfiles/activerecord_6.1.gemfile.lock +8 -2
- data/gemfiles/activerecord_7.0.gemfile +1 -0
- data/gemfiles/activerecord_7.0.gemfile.lock +7 -1
- data/gemfiles/{activerecord_6.0.gemfile → activerecord_7.1.gemfile} +1 -1
- data/gemfiles/{activerecord_7.1.0.beta1.gemfile.lock → activerecord_7.1.gemfile.lock} +10 -8
- data/lhm.gemspec +1 -0
- data/lib/lhm/atomic_switcher.rb +3 -3
- data/lib/lhm/chunker.rb +8 -7
- data/lib/lhm/connection.rb +9 -1
- data/lib/lhm/sql_retry.rb +36 -18
- data/lib/lhm/table.rb +3 -4
- data/lib/lhm/throttler/replica_lag.rb +17 -13
- data/lib/lhm/version.rb +1 -1
- data/scripts/helpers/wait-for-dbs.sh +3 -3
- data/scripts/mysql/writer/create_users.sql +1 -1
- data/spec/integration/atomic_switcher_spec.rb +4 -8
- data/spec/integration/chunker_spec.rb +23 -9
- data/spec/integration/database.yml +3 -3
- data/spec/integration/integration_helper.rb +11 -3
- data/spec/integration/lhm_spec.rb +29 -13
- data/spec/integration/proxysql_spec.rb +10 -10
- data/spec/integration/sql_retry/db_connection_helper.rb +2 -4
- data/spec/integration/sql_retry/lock_wait_spec.rb +7 -8
- data/spec/integration/sql_retry/lock_wait_timeout_test_helper.rb +18 -10
- data/spec/integration/sql_retry/proxysql_helper.rb +1 -1
- data/spec/integration/sql_retry/retry_with_proxysql_spec.rb +1 -2
- data/spec/integration/table_spec.rb +1 -1
- data/spec/test_helper.rb +27 -3
- data/spec/unit/atomic_switcher_spec.rb +2 -2
- data/spec/unit/chunker_spec.rb +43 -43
- data/spec/unit/connection_spec.rb +2 -2
- data/spec/unit/entangler_spec.rb +14 -24
- data/spec/unit/throttler/replica_lag_spec.rb +6 -14
- metadata +21 -8
- data/.travis.yml +0 -21
- data/gemfiles/activerecord_6.0.gemfile.lock +0 -71
- data/gemfiles/activerecord_7.1.0.beta1.gemfile +0 -7
@@ -14,13 +14,21 @@ class LockWaitTimeoutTestHelper
|
|
14
14
|
|
15
15
|
@lock_duration = lock_duration
|
16
16
|
|
17
|
-
# While implementing this, I discovered that MySQL seems to have an off-by-one
|
18
|
-
# bug with the innodb_lock_wait_timeout. If you ask it to wait 2 seconds, it will wait 3.
|
19
|
-
# In order to avoid surprisingly the user, let's account for that here, but also
|
20
|
-
# guard against a case where we go below 1, the minimum value.
|
21
|
-
raise ArgumentError, "innodb_lock_wait_timeout must be greater than or equal to 2" unless innodb_lock_wait_timeout >= 2
|
22
17
|
raise ArgumentError, "innodb_lock_wait_timeout must be an integer" if innodb_lock_wait_timeout.class != Integer
|
23
|
-
|
18
|
+
|
19
|
+
result = DATABASE.query(@main_conn, "SELECT VERSION()")
|
20
|
+
mysql_version = result.to_a.dig(0, "VERSION()").split("-", 2)[0]
|
21
|
+
|
22
|
+
if mysql_version.start_with?("8")
|
23
|
+
@innodb_lock_wait_timeout = innodb_lock_wait_timeout
|
24
|
+
else
|
25
|
+
# While implementing this, I discovered that MySQL seems to have an off-by-one
|
26
|
+
# bug with the innodb_lock_wait_timeout. If you ask it to wait 2 seconds, it will wait 3.
|
27
|
+
# In order to avoid surprisingly the user, let's account for that here, but also
|
28
|
+
# guard against a case where we go below 1, the minimum value.
|
29
|
+
raise ArgumentError, "innodb_lock_wait_timeout must be greater than or equal to 2" unless innodb_lock_wait_timeout >= 2
|
30
|
+
@innodb_lock_wait_timeout = innodb_lock_wait_timeout - 1
|
31
|
+
end
|
24
32
|
|
25
33
|
@threads = []
|
26
34
|
@queue = Queue.new
|
@@ -51,7 +59,7 @@ class LockWaitTimeoutTestHelper
|
|
51
59
|
end
|
52
60
|
|
53
61
|
def record_count(connection = main_conn)
|
54
|
-
response = connection
|
62
|
+
response = mysql_exec(connection, "SELECT COUNT(id) FROM #{test_table_name}")
|
55
63
|
response.first.values.first
|
56
64
|
end
|
57
65
|
|
@@ -79,7 +87,7 @@ class LockWaitTimeoutTestHelper
|
|
79
87
|
attr_reader :main_conn, :lock_duration, :innodb_lock_wait_timeout
|
80
88
|
|
81
89
|
def new_mysql_connection
|
82
|
-
|
90
|
+
DATABASE.client.new(
|
83
91
|
host: '127.0.0.1',
|
84
92
|
username: db_config['master']['user'],
|
85
93
|
password: db_config['master']['password'],
|
@@ -103,8 +111,8 @@ class LockWaitTimeoutTestHelper
|
|
103
111
|
private
|
104
112
|
|
105
113
|
def mysql_exec(connection, statement)
|
106
|
-
if connection.class ==
|
107
|
-
|
114
|
+
if connection.class == DATABASE.client
|
115
|
+
DATABASE.query(connection, statement)
|
108
116
|
elsif connection.class.to_s.include?("ActiveRecord")
|
109
117
|
connection.execute(statement)
|
110
118
|
else
|
@@ -2,7 +2,7 @@ class ProxySQLHelper
|
|
2
2
|
class << self
|
3
3
|
# Flips the destination hostgroup for /maintenance:lhm/ from 0 (i.e. writer) to 1 (i.e. reader)
|
4
4
|
def with_lhm_hostgroup_flip
|
5
|
-
conn =
|
5
|
+
conn = DATABASE.client.new(
|
6
6
|
host: '127.0.0.1',
|
7
7
|
username: "remote-admin",
|
8
8
|
password: "password",
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'minitest/autorun'
|
2
|
-
require 'mysql2'
|
3
2
|
require 'lhm'
|
4
3
|
require 'toxiproxy'
|
5
4
|
|
@@ -50,7 +49,7 @@ describe Lhm::SqlRetry, "ProxiSQL tests for LHM retry" do
|
|
50
49
|
end
|
51
50
|
end
|
52
51
|
|
53
|
-
assert_equal @connection.
|
52
|
+
assert_equal 2000, @connection.select_one("SELECT * FROM #{DBConnectionHelper.test_table_name} WHERE id=2000")["id"]
|
54
53
|
|
55
54
|
logs = @logger.string.split("\n")
|
56
55
|
|
@@ -23,7 +23,7 @@ describe Lhm::Table do
|
|
23
23
|
end
|
24
24
|
|
25
25
|
it 'should parse columns' do
|
26
|
-
value(@table.columns['id'][:type]).must_match(/(bigint|int)\(\d+\)
|
26
|
+
value(@table.columns['id'][:type]).must_match(/(bigint|int)(\(\d+\))?/)
|
27
27
|
end
|
28
28
|
|
29
29
|
it 'should return true for method that should be renamed' do
|
data/spec/test_helper.rb
CHANGED
@@ -14,6 +14,7 @@ require 'after_do'
|
|
14
14
|
require 'byebug'
|
15
15
|
require 'pathname'
|
16
16
|
require 'lhm'
|
17
|
+
require 'active_record'
|
17
18
|
|
18
19
|
$project = Pathname.new(File.dirname(__FILE__) + '/..').cleanpath
|
19
20
|
$spec = $project.join('spec')
|
@@ -21,8 +22,31 @@ $fixtures = $spec.join('fixtures')
|
|
21
22
|
|
22
23
|
$db_name = 'test'
|
23
24
|
|
24
|
-
|
25
|
-
|
25
|
+
Database = Struct.new(:adapter, :client, :error_class, :timeout_error) do
|
26
|
+
def query(connection, sql)
|
27
|
+
results = connection.query(sql)
|
28
|
+
results = results.each_hash if adapter == "trilogy"
|
29
|
+
results
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
DATABASE =
|
34
|
+
case ENV['DATABASE_ADAPTER']
|
35
|
+
when 'trilogy'
|
36
|
+
require 'trilogy'
|
37
|
+
|
38
|
+
if ActiveRecord.version < ::Gem::Version.new('7.1.0')
|
39
|
+
require 'activerecord-trilogy-adapter'
|
40
|
+
require 'trilogy_adapter/connection'
|
41
|
+
|
42
|
+
ActiveRecord::Base.public_send :extend, TrilogyAdapter::Connection
|
43
|
+
end
|
44
|
+
|
45
|
+
Database.new('trilogy', Trilogy, Trilogy::BaseError, Trilogy::TimeoutError)
|
46
|
+
else
|
47
|
+
require 'mysql2'
|
48
|
+
Database.new('mysql2', Mysql2::Client, Mysql2::Error, Mysql2::Error::TimeoutError)
|
49
|
+
end
|
26
50
|
|
27
51
|
logger = Logger.new STDOUT
|
28
52
|
logger.level = Logger::WARN
|
@@ -53,7 +77,7 @@ end
|
|
53
77
|
|
54
78
|
def init_test_db
|
55
79
|
db_config = YAML.load_file(File.expand_path(File.dirname(__FILE__)) + '/integration/database.yml')
|
56
|
-
conn =
|
80
|
+
conn = DATABASE.client.new(
|
57
81
|
:host => '127.0.0.1',
|
58
82
|
:username => db_config['master']['user'],
|
59
83
|
:password => db_config['master']['password'],
|
@@ -21,8 +21,8 @@ describe Lhm::AtomicSwitcher do
|
|
21
21
|
describe 'atomic switch' do
|
22
22
|
it 'should perform a single atomic rename' do
|
23
23
|
value(@switcher.atomic_switch).must_equal(
|
24
|
-
"
|
25
|
-
'`destination`
|
24
|
+
"RENAME TABLE `origin` TO `#{ @migration.archive_name }`, " \
|
25
|
+
'`destination` TO `origin`'
|
26
26
|
)
|
27
27
|
end
|
28
28
|
end
|
data/spec/unit/chunker_spec.rb
CHANGED
@@ -20,7 +20,7 @@ describe Lhm::Chunker do
|
|
20
20
|
@destination = Lhm::Table.new('bar')
|
21
21
|
@migration = Lhm::Migration.new(@origin, @destination)
|
22
22
|
@connection = mock()
|
23
|
-
@connection.stubs(:
|
23
|
+
@connection.stubs(:select_value).returns("dummy")
|
24
24
|
# This is a poor man's stub
|
25
25
|
@throttler = Object.new
|
26
26
|
def @throttler.run
|
@@ -42,11 +42,11 @@ describe Lhm::Chunker do
|
|
42
42
|
5
|
43
43
|
end
|
44
44
|
|
45
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 4/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(7)
|
46
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 8 order by id limit 1 offset 4/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(21)
|
47
|
-
@connection.expects(:update).with(regexp_matches(/between 1 and 7/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
48
|
-
@connection.expects(:update).with(regexp_matches(/between 8 and 10/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
49
|
-
@connection.expects(:
|
45
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 4/i), EXPECTED_RETRY_FLAGS_CHUNKER).returns(7)
|
46
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 8 order by id limit 1 offset 4/i), EXPECTED_RETRY_FLAGS_CHUNKER).returns(21)
|
47
|
+
@connection.expects(:update).with(regexp_matches(/between 1 and 7/i), EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
48
|
+
@connection.expects(:update).with(regexp_matches(/between 8 and 10/i), EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
49
|
+
@connection.expects(:select_all).twice.with(regexp_matches(/show warnings/i), EXPECTED_RETRY_FLAGS_CHUNKER).returns([])
|
50
50
|
|
51
51
|
@chunker.run
|
52
52
|
end
|
@@ -57,17 +57,17 @@ describe Lhm::Chunker do
|
|
57
57
|
2
|
58
58
|
end
|
59
59
|
|
60
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(2)
|
61
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 3 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(4)
|
62
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 5 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(6)
|
63
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 7 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(8)
|
64
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 9 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(10)
|
60
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/i), EXPECTED_RETRY_FLAGS_CHUNKER).returns(2)
|
61
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 3 order by id limit 1 offset 1/i), EXPECTED_RETRY_FLAGS_CHUNKER).returns(4)
|
62
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 5 order by id limit 1 offset 1/i), EXPECTED_RETRY_FLAGS_CHUNKER).returns(6)
|
63
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 7 order by id limit 1 offset 1/i), EXPECTED_RETRY_FLAGS_CHUNKER).returns(8)
|
64
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 9 order by id limit 1 offset 1/i), EXPECTED_RETRY_FLAGS_CHUNKER).returns(10)
|
65
65
|
|
66
|
-
@connection.expects(:update).with(regexp_matches(/between 1 and 2/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
67
|
-
@connection.expects(:update).with(regexp_matches(/between 3 and 4/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
68
|
-
@connection.expects(:update).with(regexp_matches(/between 5 and 6/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
69
|
-
@connection.expects(:update).with(regexp_matches(/between 7 and 8/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
70
|
-
@connection.expects(:update).with(regexp_matches(/between 9 and 10/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
66
|
+
@connection.expects(:update).with(regexp_matches(/between 1 and 2/i), EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
67
|
+
@connection.expects(:update).with(regexp_matches(/between 3 and 4/i), EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
68
|
+
@connection.expects(:update).with(regexp_matches(/between 5 and 6/i), EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
69
|
+
@connection.expects(:update).with(regexp_matches(/between 7 and 8/i), EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
70
|
+
@connection.expects(:update).with(regexp_matches(/between 9 and 10/i), EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
71
71
|
|
72
72
|
@chunker.run
|
73
73
|
end
|
@@ -84,17 +84,17 @@ describe Lhm::Chunker do
|
|
84
84
|
end
|
85
85
|
end
|
86
86
|
|
87
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(2)
|
88
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 3 order by id limit 1 offset 2/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(5)
|
89
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 6 order by id limit 1 offset 2/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(8)
|
90
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 9 order by id limit 1 offset 2/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(nil)
|
87
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/i), EXPECTED_RETRY_FLAGS_CHUNKER).returns(2)
|
88
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 3 order by id limit 1 offset 2/i), EXPECTED_RETRY_FLAGS_CHUNKER).returns(5)
|
89
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 6 order by id limit 1 offset 2/i), EXPECTED_RETRY_FLAGS_CHUNKER).returns(8)
|
90
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 9 order by id limit 1 offset 2/i), EXPECTED_RETRY_FLAGS_CHUNKER).returns(nil)
|
91
91
|
|
92
|
-
@connection.expects(:update).with(regexp_matches(/between 1 and 2/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
93
|
-
@connection.expects(:update).with(regexp_matches(/between 3 and 5/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
94
|
-
@connection.expects(:update).with(regexp_matches(/between 6 and 8/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
95
|
-
@connection.expects(:update).with(regexp_matches(/between 9 and 10/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
92
|
+
@connection.expects(:update).with(regexp_matches(/between 1 and 2/i), EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
93
|
+
@connection.expects(:update).with(regexp_matches(/between 3 and 5/i), EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
94
|
+
@connection.expects(:update).with(regexp_matches(/between 6 and 8/i), EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
95
|
+
@connection.expects(:update).with(regexp_matches(/between 9 and 10/i), EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
96
96
|
|
97
|
-
@connection.expects(:
|
97
|
+
@connection.expects(:select_all).twice.with(regexp_matches(/show warnings/i), EXPECTED_RETRY_FLAGS_CHUNKER).returns([])
|
98
98
|
|
99
99
|
@chunker.run
|
100
100
|
end
|
@@ -104,8 +104,8 @@ describe Lhm::Chunker do
|
|
104
104
|
:start => 1,
|
105
105
|
:limit => 1)
|
106
106
|
|
107
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 0/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(nil)
|
108
|
-
@connection.expects(:update).with(regexp_matches(/between 1 and 1/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(1)
|
107
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 0/i), EXPECTED_RETRY_FLAGS_CHUNKER).returns(nil)
|
108
|
+
@connection.expects(:update).with(regexp_matches(/between 1 and 1/i), EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(1)
|
109
109
|
|
110
110
|
@chunker.run
|
111
111
|
end
|
@@ -118,17 +118,17 @@ describe Lhm::Chunker do
|
|
118
118
|
2
|
119
119
|
end
|
120
120
|
|
121
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 2 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(3)
|
122
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 4 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(5)
|
123
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 6 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(7)
|
124
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 8 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(9)
|
125
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 10 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(nil)
|
121
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 2 order by id limit 1 offset 1/i), EXPECTED_RETRY_FLAGS_CHUNKER).returns(3)
|
122
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 4 order by id limit 1 offset 1/i), EXPECTED_RETRY_FLAGS_CHUNKER).returns(5)
|
123
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 6 order by id limit 1 offset 1/i), EXPECTED_RETRY_FLAGS_CHUNKER).returns(7)
|
124
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 8 order by id limit 1 offset 1/i), EXPECTED_RETRY_FLAGS_CHUNKER).returns(9)
|
125
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 10 order by id limit 1 offset 1/i), EXPECTED_RETRY_FLAGS_CHUNKER).returns(nil)
|
126
126
|
|
127
|
-
@connection.expects(:update).with(regexp_matches(/between 2 and 3/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
128
|
-
@connection.expects(:update).with(regexp_matches(/between 4 and 5/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
129
|
-
@connection.expects(:update).with(regexp_matches(/between 6 and 7/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
130
|
-
@connection.expects(:update).with(regexp_matches(/between 8 and 9/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
131
|
-
@connection.expects(:update).with(regexp_matches(/between 10 and 10/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(1)
|
127
|
+
@connection.expects(:update).with(regexp_matches(/between 2 and 3/i), EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
128
|
+
@connection.expects(:update).with(regexp_matches(/between 4 and 5/i), EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
129
|
+
@connection.expects(:update).with(regexp_matches(/between 6 and 7/i), EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
130
|
+
@connection.expects(:update).with(regexp_matches(/between 8 and 9/i), EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
131
|
+
@connection.expects(:update).with(regexp_matches(/between 10 and 10/i), EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(1)
|
132
132
|
|
133
133
|
@chunker.run
|
134
134
|
end
|
@@ -142,9 +142,9 @@ describe Lhm::Chunker do
|
|
142
142
|
2
|
143
143
|
end
|
144
144
|
|
145
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(2)
|
146
|
-
@connection.expects(:update).with(regexp_matches(/where \(foo.created_at > '2013-07-10' or foo.baz = 'quux'\) and `foo`/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(1)
|
147
|
-
@connection.expects(:
|
145
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/i), EXPECTED_RETRY_FLAGS_CHUNKER).returns(2)
|
146
|
+
@connection.expects(:update).with(regexp_matches(/where \(foo.created_at > '2013-07-10' or foo.baz = 'quux'\) and `foo`/i), EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(1)
|
147
|
+
@connection.expects(:select_all).with(regexp_matches(/show warnings/i), EXPECTED_RETRY_FLAGS_CHUNKER).returns([])
|
148
148
|
|
149
149
|
def @migration.conditions
|
150
150
|
"where foo.created_at > '2013-07-10' or foo.baz = 'quux'"
|
@@ -162,9 +162,9 @@ describe Lhm::Chunker do
|
|
162
162
|
2
|
163
163
|
end
|
164
164
|
|
165
|
-
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(2)
|
166
|
-
@connection.expects(:update).with(regexp_matches(/inner join bar on foo.id = bar.foo_id and/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(1)
|
167
|
-
@connection.expects(:
|
165
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/i), EXPECTED_RETRY_FLAGS_CHUNKER).returns(2)
|
166
|
+
@connection.expects(:update).with(regexp_matches(/inner join bar on foo.id = bar.foo_id and/i), EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(1)
|
167
|
+
@connection.expects(:select_all).with(regexp_matches(/show warnings/i), EXPECTED_RETRY_FLAGS_CHUNKER).returns([])
|
168
168
|
|
169
169
|
def @migration.conditions
|
170
170
|
'inner join bar on foo.id = bar.foo_id'
|
@@ -93,7 +93,7 @@ describe Lhm::Connection do
|
|
93
93
|
it "Queries should be tagged with ProxySQL tag if reconnect_with_consistent_host is enabled" do
|
94
94
|
ar_connection = mock()
|
95
95
|
ar_connection.expects(:public_send).with(:select_value, "SHOW TABLES #{Lhm::ProxySQLHelper::ANNOTATION}").returns("dummy")
|
96
|
-
ar_connection.stubs(:
|
96
|
+
ar_connection.stubs(:select_value).times(4).returns("dummy")
|
97
97
|
ar_connection.stubs(:active?).returns(true)
|
98
98
|
|
99
99
|
connection = Lhm::Connection.new(connection: ar_connection, options: {
|
@@ -108,4 +108,4 @@ describe Lhm::Connection do
|
|
108
108
|
|
109
109
|
assert_equal val, "dummy"
|
110
110
|
end
|
111
|
-
end
|
111
|
+
end
|
data/spec/unit/entangler_spec.rb
CHANGED
@@ -63,10 +63,9 @@ describe Lhm::Entangler do
|
|
63
63
|
it 'should retry trigger creation when it hits a lock wait timeout' do
|
64
64
|
tries = 1
|
65
65
|
ar_connection = mock()
|
66
|
+
ar_connection.stubs(:select_value).returns("dummy")
|
66
67
|
ar_connection.stubs(:execute)
|
67
|
-
|
68
|
-
.then
|
69
|
-
.raises(Mysql2::Error, 'Lock wait timeout exceeded; try restarting transaction')
|
68
|
+
.raises(ActiveRecord::StatementInvalid, 'Lock wait timeout exceeded; try restarting transaction')
|
70
69
|
ar_connection.stubs(:active?).returns(true)
|
71
70
|
|
72
71
|
connection = Lhm::Connection.new(connection: ar_connection, options: {
|
@@ -79,15 +78,14 @@ describe Lhm::Entangler do
|
|
79
78
|
|
80
79
|
@entangler = Lhm::Entangler.new(@migration, connection)
|
81
80
|
|
82
|
-
assert_raises(
|
81
|
+
assert_raises(ActiveRecord::StatementInvalid) { @entangler.before }
|
83
82
|
end
|
84
83
|
|
85
84
|
it 'should not retry trigger creation with other mysql errors' do
|
86
85
|
ar_connection = mock()
|
86
|
+
ar_connection.stubs(:select_value).returns("dummy")
|
87
87
|
ar_connection.stubs(:execute)
|
88
|
-
.
|
89
|
-
.then
|
90
|
-
.raises(Mysql2::Error, 'The MySQL server is running with the --read-only option so it cannot execute this statement.')
|
88
|
+
.raises(DATABASE.error_class, 'The MySQL server is running with the --read-only option so it cannot execute this statement.')
|
91
89
|
ar_connection.stubs(:active?).returns(true)
|
92
90
|
connection = Lhm::Connection.new(connection: ar_connection, options: {
|
93
91
|
reconnect_with_consistent_host: true,
|
@@ -97,15 +95,14 @@ describe Lhm::Entangler do
|
|
97
95
|
})
|
98
96
|
|
99
97
|
@entangler = Lhm::Entangler.new(@migration, connection)
|
100
|
-
assert_raises(
|
98
|
+
assert_raises(DATABASE.error_class) { @entangler.before }
|
101
99
|
end
|
102
100
|
|
103
101
|
it 'should succesfully finish after retrying' do
|
104
102
|
ar_connection = mock()
|
103
|
+
ar_connection.stubs(:select_value).returns("dummy")
|
105
104
|
ar_connection.stubs(:execute)
|
106
|
-
.
|
107
|
-
.then
|
108
|
-
.raises(Mysql2::Error, 'Lock wait timeout exceeded; try restarting transaction')
|
105
|
+
.raises(ActiveRecord::StatementInvalid, 'Lock wait timeout exceeded; try restarting transaction')
|
109
106
|
.then
|
110
107
|
.returns([["dummy"]])
|
111
108
|
ar_connection.stubs(:active?).returns(true)
|
@@ -124,22 +121,15 @@ describe Lhm::Entangler do
|
|
124
121
|
|
125
122
|
it 'should retry as many times as specified by configuration' do
|
126
123
|
ar_connection = mock()
|
124
|
+
ar_connection.stubs(:select_value).returns("dummy")
|
127
125
|
ar_connection.stubs(:execute)
|
128
|
-
.
|
129
|
-
.then
|
130
|
-
.raises(Mysql2::Error, 'Lock wait timeout exceeded; try restarting transaction')
|
131
|
-
.then
|
132
|
-
.returns([["dummy"]]) # reconnect 1
|
133
|
-
.then
|
134
|
-
.raises(Mysql2::Error, 'Lock wait timeout exceeded; try restarting transaction')
|
135
|
-
.then
|
136
|
-
.returns([["dummy"]]) # reconnect 2
|
126
|
+
.raises(DATABASE.error_class, 'Lock wait timeout exceeded; try restarting transaction')
|
137
127
|
.then
|
138
|
-
.raises(
|
128
|
+
.raises(DATABASE.error_class, 'Lock wait timeout exceeded; try restarting transaction')
|
139
129
|
.then
|
140
|
-
.
|
130
|
+
.raises(DATABASE.error_class, 'Lock wait timeout exceeded; try restarting transaction')
|
141
131
|
.then
|
142
|
-
.raises(
|
132
|
+
.raises(DATABASE.error_class, 'Lock wait timeout exceeded; try restarting transaction') # final error
|
143
133
|
ar_connection.stubs(:active?).returns(true)
|
144
134
|
|
145
135
|
connection = Lhm::Connection.new(connection: ar_connection, options: {
|
@@ -152,7 +142,7 @@ describe Lhm::Entangler do
|
|
152
142
|
|
153
143
|
@entangler = Lhm::Entangler.new(@migration, connection)
|
154
144
|
|
155
|
-
assert_raises(
|
145
|
+
assert_raises(DATABASE.error_class) { @entangler.before }
|
156
146
|
end
|
157
147
|
|
158
148
|
describe 'super long table names' do
|
@@ -43,14 +43,6 @@ describe Lhm::Throttler::Replica do
|
|
43
43
|
end
|
44
44
|
|
45
45
|
describe "#client" do
|
46
|
-
before do
|
47
|
-
class TestMysql2Client
|
48
|
-
def initialize(config)
|
49
|
-
raise Mysql2::Error.new("connection error")
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
46
|
describe 'on connection error' do
|
55
47
|
it 'logs and returns nil' do
|
56
48
|
assert_nil(Lhm::Throttler::Replica.new('replica', @dummy_mysql_client_config).connection)
|
@@ -58,14 +50,14 @@ describe Lhm::Throttler::Replica do
|
|
58
50
|
log_messages = @logs.string.lines
|
59
51
|
assert_equal(2, log_messages.length)
|
60
52
|
assert log_messages[0].include? "Connecting to replica on database: db"
|
61
|
-
assert log_messages[1].include? "Error connecting to replica
|
53
|
+
assert log_messages[1].include? "Error connecting to replica"
|
62
54
|
end
|
63
55
|
end
|
64
56
|
|
65
57
|
describe 'with proper config' do
|
66
|
-
it "creates a new
|
58
|
+
it "creates a new database client" do
|
67
59
|
expected_config = { username: 'user', password: 'pw', database: 'db', host: 'replica' }
|
68
|
-
|
60
|
+
DATABASE.client.stubs(:new).with(expected_config).returns(mock())
|
69
61
|
|
70
62
|
assert Lhm::Throttler::Replica.new('replica', @dummy_mysql_client_config).connection
|
71
63
|
end
|
@@ -80,7 +72,7 @@ describe Lhm::Throttler::Replica do
|
|
80
72
|
ActiveRecord::Base.stubs(:connection_pool).returns(stub(spec: stub(config: active_record_config)))
|
81
73
|
end
|
82
74
|
|
83
|
-
|
75
|
+
DATABASE.client.stubs(:new).returns(mock())
|
84
76
|
|
85
77
|
assert Lhm::Throttler::Replica.new('replica').connection
|
86
78
|
|
@@ -137,7 +129,7 @@ describe Lhm::Throttler::Replica do
|
|
137
129
|
describe "#lag on connection error" do
|
138
130
|
it "logs and returns 0 replica lag" do
|
139
131
|
client = mock()
|
140
|
-
client.stubs(:query).raises(
|
132
|
+
client.stubs(:query).raises(DATABASE.error_class, "Can't connect to MySQL server")
|
141
133
|
Lhm::Throttler::Replica.any_instance.stubs(:client).returns(client)
|
142
134
|
Lhm::Throttler::Replica.any_instance.stubs(:config).returns([])
|
143
135
|
|
@@ -224,7 +216,7 @@ describe Lhm::Throttler::ReplicaLag do
|
|
224
216
|
describe 'with MySQL stopped on the replica' do
|
225
217
|
it 'assumes 0 replica lag' do
|
226
218
|
client = mock()
|
227
|
-
client.stubs(:query).raises(
|
219
|
+
client.stubs(:query).raises(DATABASE.error_class, "Can't connect to MySQL server")
|
228
220
|
Lhm::Throttler::Replica.any_instance.stubs(:client).returns(client)
|
229
221
|
|
230
222
|
Lhm::Throttler::Replica.any_instance.stubs(:prepare_connection_config).returns([])
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lhm-shopify
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.
|
4
|
+
version: 4.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- SoundCloud
|
@@ -12,7 +12,7 @@ authors:
|
|
12
12
|
autorequire:
|
13
13
|
bindir: bin
|
14
14
|
cert_chain: []
|
15
|
-
date: 2023-
|
15
|
+
date: 2023-11-22 00:00:00.000000000 Z
|
16
16
|
dependencies:
|
17
17
|
- !ruby/object:Gem::Dependency
|
18
18
|
name: retriable
|
@@ -112,6 +112,20 @@ dependencies:
|
|
112
112
|
- - ">="
|
113
113
|
- !ruby/object:Gem::Version
|
114
114
|
version: '0'
|
115
|
+
- !ruby/object:Gem::Dependency
|
116
|
+
name: trilogy
|
117
|
+
requirement: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
type: :development
|
123
|
+
prerelease: false
|
124
|
+
version_requirements: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '0'
|
115
129
|
- !ruby/object:Gem::Dependency
|
116
130
|
name: simplecov
|
117
131
|
requirement: !ruby/object:Gem::Requirement
|
@@ -179,7 +193,6 @@ files:
|
|
179
193
|
- ".github/workflows/test.yml"
|
180
194
|
- ".gitignore"
|
181
195
|
- ".rubocop.yml"
|
182
|
-
- ".travis.yml"
|
183
196
|
- Appraisals
|
184
197
|
- CHANGELOG.md
|
185
198
|
- Gemfile
|
@@ -188,15 +201,15 @@ files:
|
|
188
201
|
- README.md
|
189
202
|
- Rakefile
|
190
203
|
- dev.yml
|
204
|
+
- docker-compose-mysql-5.7.yml
|
205
|
+
- docker-compose-mysql-8.0.yml
|
191
206
|
- docker-compose.yml
|
192
|
-
- gemfiles/activerecord_6.0.gemfile
|
193
|
-
- gemfiles/activerecord_6.0.gemfile.lock
|
194
207
|
- gemfiles/activerecord_6.1.gemfile
|
195
208
|
- gemfiles/activerecord_6.1.gemfile.lock
|
196
209
|
- gemfiles/activerecord_7.0.gemfile
|
197
210
|
- gemfiles/activerecord_7.0.gemfile.lock
|
198
|
-
- gemfiles/activerecord_7.1.
|
199
|
-
- gemfiles/activerecord_7.1.
|
211
|
+
- gemfiles/activerecord_7.1.gemfile
|
212
|
+
- gemfiles/activerecord_7.1.gemfile.lock
|
200
213
|
- lhm.gemspec
|
201
214
|
- lib/lhm-shopify.rb
|
202
215
|
- lib/lhm.rb
|
@@ -307,7 +320,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
307
320
|
- !ruby/object:Gem::Version
|
308
321
|
version: '0'
|
309
322
|
requirements: []
|
310
|
-
rubygems_version: 3.4.
|
323
|
+
rubygems_version: 3.4.21
|
311
324
|
signing_key:
|
312
325
|
specification_version: 4
|
313
326
|
summary: online schema changer for mysql
|
data/.travis.yml
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
language: ruby
|
2
|
-
before_script:
|
3
|
-
- "mysql -e 'create database lhm;'"
|
4
|
-
rvm:
|
5
|
-
- 2.0.0
|
6
|
-
- 2.1
|
7
|
-
- 2.2
|
8
|
-
sudo: false
|
9
|
-
gemfile:
|
10
|
-
- gemfiles/ar-2.3_mysql.gemfile
|
11
|
-
- gemfiles/ar-3.2_mysql.gemfile
|
12
|
-
- gemfiles/ar-3.2_mysql2.gemfile
|
13
|
-
- gemfiles/ar-4.0_mysql2.gemfile
|
14
|
-
- gemfiles/ar-4.1_mysql2.gemfile
|
15
|
-
- gemfiles/ar-4.2_mysql2.gemfile
|
16
|
-
matrix:
|
17
|
-
exclude:
|
18
|
-
- rvm: 2.2
|
19
|
-
gemfile: gemfiles/ar-3.2_mysql.gemfile
|
20
|
-
- rvm: 2.2
|
21
|
-
gemfile: gemfiles/ar-2.3_mysql.gemfile
|
@@ -1,71 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: ..
|
3
|
-
specs:
|
4
|
-
lhm-shopify (4.0.0)
|
5
|
-
retriable (>= 3.0.0)
|
6
|
-
|
7
|
-
GEM
|
8
|
-
remote: https://rubygems.org/
|
9
|
-
specs:
|
10
|
-
activemodel (6.0.3)
|
11
|
-
activesupport (= 6.0.3)
|
12
|
-
activerecord (6.0.3)
|
13
|
-
activemodel (= 6.0.3)
|
14
|
-
activesupport (= 6.0.3)
|
15
|
-
activesupport (6.0.3)
|
16
|
-
concurrent-ruby (~> 1.0, >= 1.0.2)
|
17
|
-
i18n (>= 0.7, < 2)
|
18
|
-
minitest (~> 5.1)
|
19
|
-
tzinfo (~> 1.1)
|
20
|
-
zeitwerk (~> 2.2, >= 2.2.2)
|
21
|
-
after_do (0.4.0)
|
22
|
-
appraisal (2.5.0)
|
23
|
-
bundler
|
24
|
-
rake
|
25
|
-
thor (>= 0.14.0)
|
26
|
-
byebug (11.1.3)
|
27
|
-
concurrent-ruby (1.2.2)
|
28
|
-
docile (1.4.0)
|
29
|
-
i18n (1.14.1)
|
30
|
-
concurrent-ruby (~> 1.0)
|
31
|
-
minitest (5.20.0)
|
32
|
-
mocha (2.1.0)
|
33
|
-
ruby2_keywords (>= 0.0.5)
|
34
|
-
mysql2 (0.5.5)
|
35
|
-
rake (13.0.6)
|
36
|
-
retriable (3.1.2)
|
37
|
-
ruby2_keywords (0.0.5)
|
38
|
-
simplecov (0.22.0)
|
39
|
-
docile (~> 1.1)
|
40
|
-
simplecov-html (~> 0.11)
|
41
|
-
simplecov_json_formatter (~> 0.1)
|
42
|
-
simplecov-html (0.12.3)
|
43
|
-
simplecov_json_formatter (0.1.4)
|
44
|
-
thor (1.2.2)
|
45
|
-
thread_safe (0.3.6)
|
46
|
-
toxiproxy (2.0.2)
|
47
|
-
tzinfo (1.2.11)
|
48
|
-
thread_safe (~> 0.1)
|
49
|
-
zeitwerk (2.6.11)
|
50
|
-
|
51
|
-
PLATFORMS
|
52
|
-
arm64-darwin-21
|
53
|
-
arm64-darwin-22
|
54
|
-
x86_64-darwin-20
|
55
|
-
x86_64-linux
|
56
|
-
|
57
|
-
DEPENDENCIES
|
58
|
-
activerecord (= 6.0.3)
|
59
|
-
after_do
|
60
|
-
appraisal
|
61
|
-
byebug
|
62
|
-
lhm-shopify!
|
63
|
-
minitest
|
64
|
-
mocha
|
65
|
-
mysql2
|
66
|
-
rake
|
67
|
-
simplecov
|
68
|
-
toxiproxy
|
69
|
-
|
70
|
-
BUNDLED WITH
|
71
|
-
2.2.22
|