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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8b5fbf9905c14cd938075e3f6b930de0b52cdbc7c048f8ed572c654824681d99
4
- data.tar.gz: c950dd9650c4e04412a91a9d21b6bbff3b3f092ecbbce1474a77518716737d62
3
+ metadata.gz: c3893b743c675e62933e58a4f56c0ac2dba1b1081afabc779dc1d899406e7c42
4
+ data.tar.gz: 569175938b8069e0036f881400b9427180283ec61c29ca5d509a7090a7f8a9ba
5
5
  SHA512:
6
- metadata.gz: e92c7cc7cefa85e18a9c5b3d17966574324c8fe8e067f4ac4203241d18e8dae836c82554f92685e90978af26451422852c654fed8f0b855032dee0ac3d0f130d
7
- data.tar.gz: 8755a2814d7d92d015d35ae3636993e39a66b5d27d1980a002eec96a2e33012350310d5f903b5f3b225d6b4bf60295841ee4e35919b927914a0cc700683d357b
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- lhm-shopify (3.5.1)
4
+ lhm-shopify (3.5.5)
5
5
  retriable (>= 3.0.0)
6
6
 
7
7
  GEM
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 doing the initial setup:
148
+ the feature can be enabled. This is done by adding this flag when providing options to the migration:
149
149
  ```ruby
150
- Lhm.setup(connection, options: {reconnect_with_consistent_host: true})
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
- end
19
+ end
20
20
 
21
21
  Rake::TestTask.new('dev') do |t|
22
22
  t.libs << 'lib'
23
23
  t.libs << 'spec'
24
- t.test_files = FileList[
25
- 'spec/test_helper.rb',
26
- 'spec/integration/lhm_spec.rb'
27
- # Add file to test individually
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.0
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 TEST="$@"
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- lhm-shopify (3.5.1)
4
+ lhm-shopify (3.5.5)
5
5
  retriable (>= 3.0.0)
6
6
 
7
7
  GEM
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- lhm-shopify (3.5.1)
4
+ lhm-shopify (3.5.5)
5
5
  retriable (>= 3.0.0)
6
6
 
7
7
  GEM
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- lhm-shopify (3.5.1)
4
+ lhm-shopify (3.5.5)
5
5
  retriable (>= 3.0.0)
6
6
 
7
7
  GEM
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- lhm-shopify (3.5.1)
4
+ lhm-shopify (3.5.5)
5
5
  retriable (>= 3.0.0)
6
6
 
7
7
  GEM
@@ -16,12 +16,13 @@ module Lhm
16
16
 
17
17
  attr_reader :connection
18
18
 
19
- def initialize(migration, connection = nil, options={})
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, retry_options: @retry_options)
43
+ @connection.execute(atomic_switch, should_retry: true, log_prefix: LOG_PREFIX)
43
44
  end
44
45
  end
45
46
  end
@@ -3,16 +3,19 @@ require 'lhm/proxysql_helper'
3
3
 
4
4
  module Lhm
5
5
  class ChunkInsert
6
- def initialize(migration, connection, lowest, highest, options = {})
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 = options[:retriable] || {}
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, retry_options: @retry_options)
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
- options: {
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, retry_options: @retry_options).each do |level, code, message|
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(@retry_options) do |retriable_connection|
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, retry_options: @retry_options)
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
@@ -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, retry_options: @retry_config)
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(', ')}")
@@ -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
- alias connection __getobj__
10
- alias connection= __setobj__
8
+ class Connection < SimpleDelegator
9
+ extend Forwardable
11
10
 
12
- def initialize(connection:, default_log_prefix: nil, options: {}, retry_config: {})
13
- @default_log_prefix = default_log_prefix
14
- @retry_options = retry_config || default_retry_config
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: retry_config,
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 options=(options)
26
- # If any other flags are added. Add the "processing" here
27
- @sql_retry.reconnect_with_consistent_host = options[:reconnect_with_consistent_host] || false
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
- def execute(query, should_retry: false, retry_options: {})
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, retry_options)
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, retry_options: {})
46
+ def update(query, should_retry: false, log_prefix: nil)
39
47
  if should_retry
40
- exec_with_retries(:update, query, retry_options)
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, retry_options: {})
54
+ def select_value(query, should_retry: false, log_prefix: nil)
47
55
  if should_retry
48
- exec_with_retries(:select_value, query, retry_options)
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, retry_options: {})
62
+ def select_values(query, should_retry: false, log_prefix: nil)
55
63
  if should_retry
56
- exec_with_retries(:select_values, query, retry_options)
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, retry_options: {})
70
+ def select_one(query, should_retry: false, log_prefix: nil)
63
71
  if should_retry
64
- exec_with_retries(:select_one, query, retry_options)
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
- connection.public_send(method, Lhm::ProxySQLHelper.tagged(sql))
81
+ ar_connection.public_send(method, Lhm::ProxySQLHelper.tagged(sql))
74
82
  end
75
83
 
76
- def exec_with_retries(method, sql, retry_options = {})
77
- retry_options[:log_prefix] ||= file
78
- @sql_retry.with_retries(retry_options) do |conn|
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, options = {})
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, retry_options: @retry_options)
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, retry_options: @retry_options)
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, options)
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
@@ -22,6 +22,8 @@ module Lhm
22
22
 
23
23
  attr_reader :connection
24
24
 
25
+ LOG_PREFIX = "LockedSwitcher"
26
+
25
27
  def initialize(migration, connection = nil)
26
28
  @migration = migration
27
29
  @connection = connection
@@ -4,7 +4,7 @@ module Lhm
4
4
  ANNOTATION = "/*maintenance:lhm*/"
5
5
 
6
6
  def tagged(sql)
7
- "#{ANNOTATION}#{sql}"
7
+ "#{sql} #{ANNOTATION}"
8
8
  end
9
9
  end
10
10
  end