lhm-shopify 3.5.1 → 3.5.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/Gemfile.lock +1 -1
- data/README.md +14 -3
- data/Rakefile +6 -6
- data/dev.yml +5 -2
- data/gemfiles/activerecord_5.2.gemfile.lock +1 -1
- data/gemfiles/activerecord_6.0.gemfile.lock +1 -1
- data/gemfiles/activerecord_6.1.gemfile.lock +1 -1
- data/gemfiles/activerecord_7.0.0.alpha2.gemfile.lock +1 -1
- data/lib/lhm/atomic_switcher.rb +4 -3
- data/lib/lhm/chunk_insert.rb +6 -3
- data/lib/lhm/chunker.rb +6 -6
- data/lib/lhm/cleanup/current.rb +4 -1
- data/lib/lhm/connection.rb +33 -25
- 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 +1 -1
- data/lib/lhm/sql_retry.rb +56 -60
- data/lib/lhm/version.rb +1 -1
- data/lib/lhm.rb +30 -24
- data/spec/integration/atomic_switcher_spec.rb +28 -17
- data/spec/integration/chunker_spec.rb +7 -5
- data/spec/integration/integration_helper.rb +4 -6
- data/spec/integration/lhm_spec.rb +3 -4
- data/spec/integration/proxysql_spec.rb +1 -1
- data/spec/integration/sql_retry/lock_wait_spec.rb +2 -2
- data/spec/integration/sql_retry/retry_with_proxysql_spec.rb +8 -7
- data/spec/test_helper.rb +3 -0
- data/spec/unit/chunker_spec.rb +44 -43
- data/spec/unit/connection_spec.rb +37 -12
- data/spec/unit/entangler_spec.rb +31 -9
- data/spec/unit/lhm_spec.rb +17 -0
- data/spec/unit/throttler/slave_lag_spec.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c3893b743c675e62933e58a4f56c0ac2dba1b1081afabc779dc1d899406e7c42
|
4
|
+
data.tar.gz: 569175938b8069e0036f881400b9427180283ec61c29ca5d509a7090a7f8a9ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ba3ebf953f97cd793c52cb83fee77dd4436877ca01a51ee6d718c7937916e391a32c659b9f403b62f65cf0a326dc755e6221e3311760738c9c6fb442746e1617
|
7
|
+
data.tar.gz: 2e617329a895d00a2541539526e941fc94dfda8285083d46d4179587a31786cbbb1062bd8dbdfffcc4d046801944ac96aa700c32618a4db9f4346345e9105c7c
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,29 @@
|
|
1
|
+
# 3.5.5 (Jan, 2022)
|
2
|
+
* Fix error where from Config shadowing which would cause LHM to abort on reconnect (https://github.com/Shopify/lhm/pull/128)
|
3
|
+
|
4
|
+
# 3.5.4 (Dec, 2021)
|
5
|
+
* Refactored the way options are handled internally. Code is now much clearer to understand
|
6
|
+
* Removed optional connection_options from `Lhm.setup` and `Lhm.connection`
|
7
|
+
* Option `reconnect_with_consistent_host` will now be provided with `options` for `Lhm.change_table`
|
8
|
+
|
9
|
+
# 3.5.3 (Dec, 2021)
|
10
|
+
* Adds ProxySQL comments at the end of query to accommodate for internal tool's requirements
|
11
|
+
|
12
|
+
# 3.5.2 (Dec, 2021)
|
13
|
+
* Fixed error on undefined connection, when calling `Lhm.connection` without calling `Lhm.setup` first
|
14
|
+
* Changed `Lhm.connection.connection` to `lhm.connection.ar_connection` for increased clarity and readability
|
15
|
+
|
16
|
+
# 3.5.1 (Dec , 2021)
|
17
|
+
* Add better logging to the LHM components (https://github.com/Shopify/lhm/pull/112)
|
18
|
+
* Slave lag throttler now supports ActiveRecord > 6.0
|
19
|
+
* [Dev] Add `Appraisals` to test against multiple version
|
20
|
+
|
21
|
+
# 3.5.0 (Dec , 2021)
|
22
|
+
* Duplicate of 3.4.2 (unfortunate mistake)
|
23
|
+
|
24
|
+
# 3.4.2 (Sept, 2021)
|
25
|
+
* Fixed Chunker's undefined name error (https://github.com/Shopify/lhm/pull/110)
|
26
|
+
|
1
27
|
# 3.4.1 (Sep 22, 2021)
|
2
28
|
|
3
29
|
* Add better logging to the LHM components (https://github.com/Shopify/lhm/pull/108)
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -113,7 +113,7 @@ tables must be cleaned up.
|
|
113
113
|
LHM can recover from connection loss. However, when used in conjunction with ProxySQL, there are multiple ways that
|
114
114
|
connection loss could induce data loss (if triggered by a failover). Therefore it will perform additional checks to
|
115
115
|
ensure that the MySQL host stays consistent across the schema migrations if the feature is enabled.
|
116
|
-
This is done by tagging every query with `/*maintenance:lhm*/`, which will be recognized by ProxySQL.
|
116
|
+
This is done by tagging every query with `/*maintenance:lhm*/`, which will be recognized by ProxySQL.
|
117
117
|
However, to get this feature working, a new ProxySQL query rule must be added.
|
118
118
|
```cnf
|
119
119
|
{
|
@@ -145,9 +145,11 @@ forwarded to the right target.
|
|
145
145
|
```
|
146
146
|
|
147
147
|
Once these changes are added to the ProxySQL configuration (either through `.cnf` or dynamically through the admin interface),
|
148
|
-
the feature can be enabled. This is done by adding this flag when
|
148
|
+
the feature can be enabled. This is done by adding this flag when providing options to the migration:
|
149
149
|
```ruby
|
150
|
-
Lhm.
|
150
|
+
Lhm.change_table(..., options: {reconnect_with_consistent_host: true}) do |t|
|
151
|
+
...
|
152
|
+
end
|
151
153
|
```
|
152
154
|
**Note**: This feature is disabled by default
|
153
155
|
|
@@ -301,6 +303,15 @@ COV=1 bundle exec rake unit && bundle exec rake integration
|
|
301
303
|
open coverage/index.html
|
302
304
|
```
|
303
305
|
|
306
|
+
### Merging for a new version
|
307
|
+
When creating a PR for a new version, make sure that th version has been bumped in `lib/lhm/version.rb`. Then run the following code snippet to ensure the everything is consistent, otherwise
|
308
|
+
the gem will not publish.
|
309
|
+
```bash
|
310
|
+
bundle install
|
311
|
+
bundle update
|
312
|
+
bundle exec appraisals install
|
313
|
+
```
|
314
|
+
|
304
315
|
### Docker Compose
|
305
316
|
The integration tests rely on a replication configuration for MySQL which is being proxied by an instance of ProxySQL.
|
306
317
|
It is important that every container is running to execute the integration test suite.
|
data/Rakefile
CHANGED
@@ -16,16 +16,16 @@ Rake::TestTask.new('integration') do |t|
|
|
16
16
|
t.libs << 'spec'
|
17
17
|
t.test_files = FileList['spec/integration/**/*_spec.rb']
|
18
18
|
t.verbose = true
|
19
|
-
|
19
|
+
end
|
20
20
|
|
21
21
|
Rake::TestTask.new('dev') do |t|
|
22
22
|
t.libs << 'lib'
|
23
23
|
t.libs << 'spec'
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
24
|
+
|
25
|
+
files = FileList.new('spec/test_helper.rb')
|
26
|
+
files.add(ENV["SINGLE_TEST"]) if ENV["SINGLE_TEST"]
|
27
|
+
t.test_files = files
|
28
|
+
|
29
29
|
t.verbose = true
|
30
30
|
end
|
31
31
|
|
data/dev.yml
CHANGED
@@ -5,7 +5,7 @@ up:
|
|
5
5
|
or: [mysql@5.7]
|
6
6
|
conflicts: [shopify/shopify/mysql-client, mysql-connector-c, mysql, mysql-client]
|
7
7
|
- wget
|
8
|
-
- ruby: 2.7.
|
8
|
+
- ruby: 2.7.5
|
9
9
|
- bundler
|
10
10
|
- custom:
|
11
11
|
name: Get Appraisal gems
|
@@ -31,7 +31,7 @@ commands:
|
|
31
31
|
if [[ $# -eq 0 ]]; then
|
32
32
|
bundle exec rake unit && bundle exec rake integration
|
33
33
|
else
|
34
|
-
bundle exec rake dev
|
34
|
+
SINGLE_TEST="$@" bundle exec rake dev
|
35
35
|
fi
|
36
36
|
appraisals: bundle exec appraisal rake specs
|
37
37
|
cov: rm -rf coverage; COV=1 bundle exec rake unit && bundle exec rake integration; open coverage/index.html
|
@@ -40,3 +40,6 @@ commands:
|
|
40
40
|
run: docker-compose logs -f
|
41
41
|
clear:
|
42
42
|
run: docker-compose rm -v -s -f && docker-compose up -d && ./scripts/helpers/wait-for-dbs.sh
|
43
|
+
pre-publish:
|
44
|
+
# Ensures all Gemfile.lock are sync with the new version in `lhm/version.rb` and runs appraisals
|
45
|
+
run: bundle install && bundle exec appraisal install && bundle exec appraisal rake specs
|
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, should_retry: true,
|
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
@@ -3,16 +3,19 @@ require 'lhm/proxysql_helper'
|
|
3
3
|
|
4
4
|
module Lhm
|
5
5
|
class ChunkInsert
|
6
|
-
|
6
|
+
|
7
|
+
LOG_PREFIX = "ChunkInsert"
|
8
|
+
|
9
|
+
def initialize(migration, connection, lowest, highest, retry_options = {})
|
7
10
|
@migration = migration
|
8
11
|
@connection = connection
|
9
12
|
@lowest = lowest
|
10
13
|
@highest = highest
|
11
|
-
@retry_options =
|
14
|
+
@retry_options = retry_options
|
12
15
|
end
|
13
16
|
|
14
17
|
def insert_and_return_count_of_rows_created
|
15
|
-
@connection.update(sql, should_retry: true,
|
18
|
+
@connection.update(sql, should_retry: true, log_prefix: LOG_PREFIX)
|
16
19
|
end
|
17
20
|
|
18
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", should_retry: true,
|
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, should_retry: true,
|
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, should_retry: true,
|
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,20 +1,23 @@
|
|
1
1
|
require 'delegate'
|
2
|
+
require 'forwardable'
|
2
3
|
require 'lhm/sql_retry'
|
3
4
|
|
4
5
|
module Lhm
|
5
|
-
class Connection < SimpleDelegator
|
6
|
-
|
7
6
|
# Lhm::Connection inherits from SingleDelegator. It will forward any unknown method calls to the ActiveRecord
|
8
7
|
# connection.
|
9
|
-
|
10
|
-
|
8
|
+
class Connection < SimpleDelegator
|
9
|
+
extend Forwardable
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
|
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__
|
16
|
+
|
17
|
+
def initialize(connection:, options: {})
|
15
18
|
@sql_retry = Lhm::SqlRetry.new(
|
16
19
|
connection,
|
17
|
-
options:
|
20
|
+
retry_options: options[:retriable] || {},
|
18
21
|
reconnect_with_consistent_host: options[:reconnect_with_consistent_host] || false
|
19
22
|
)
|
20
23
|
|
@@ -22,46 +25,51 @@ module Lhm
|
|
22
25
|
super(connection)
|
23
26
|
end
|
24
27
|
|
25
|
-
def
|
26
|
-
|
27
|
-
|
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)
|
28
34
|
end
|
29
35
|
|
30
|
-
|
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)
|
31
39
|
if should_retry
|
32
|
-
exec_with_retries(:execute, query,
|
40
|
+
exec_with_retries(:execute, query, log_prefix)
|
33
41
|
else
|
34
42
|
exec(:execute, query)
|
35
43
|
end
|
36
44
|
end
|
37
45
|
|
38
|
-
def update(query, should_retry: false,
|
46
|
+
def update(query, should_retry: false, log_prefix: nil)
|
39
47
|
if should_retry
|
40
|
-
exec_with_retries(:update, query,
|
48
|
+
exec_with_retries(:update, query, log_prefix)
|
41
49
|
else
|
42
50
|
exec(:update, query)
|
43
51
|
end
|
44
52
|
end
|
45
53
|
|
46
|
-
def select_value(query, should_retry: false,
|
54
|
+
def select_value(query, should_retry: false, log_prefix: nil)
|
47
55
|
if should_retry
|
48
|
-
exec_with_retries(:select_value, query,
|
56
|
+
exec_with_retries(:select_value, query, log_prefix)
|
49
57
|
else
|
50
58
|
exec(:select_value, query)
|
51
59
|
end
|
52
60
|
end
|
53
61
|
|
54
|
-
def select_values(query, should_retry: false,
|
62
|
+
def select_values(query, should_retry: false, log_prefix: nil)
|
55
63
|
if should_retry
|
56
|
-
exec_with_retries(:select_values, query,
|
64
|
+
exec_with_retries(:select_values, query, log_prefix)
|
57
65
|
else
|
58
66
|
exec(:select_values, query)
|
59
67
|
end
|
60
68
|
end
|
61
69
|
|
62
|
-
def select_one(query, should_retry: false,
|
70
|
+
def select_one(query, should_retry: false, log_prefix: nil)
|
63
71
|
if should_retry
|
64
|
-
exec_with_retries(:select_one, query,
|
72
|
+
exec_with_retries(:select_one, query, log_prefix)
|
65
73
|
else
|
66
74
|
exec(:select_one, query)
|
67
75
|
end
|
@@ -70,12 +78,12 @@ module Lhm
|
|
70
78
|
private
|
71
79
|
|
72
80
|
def exec(method, sql)
|
73
|
-
|
81
|
+
ar_connection.public_send(method, Lhm::ProxySQLHelper.tagged(sql))
|
74
82
|
end
|
75
83
|
|
76
|
-
def exec_with_retries(method, sql,
|
77
|
-
|
78
|
-
@sql_retry.with_retries(
|
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|
|
79
87
|
conn.public_send(method, Lhm::ProxySQLHelper.tagged(sql))
|
80
88
|
end
|
81
89
|
end
|
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, should_retry: true,
|
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, should_retry: true,
|
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
|
-
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
|
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