lhm-shopify 4.3.0 → 4.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f0c79dc83baf664e8cd4ad2e11f8e72b3927edbb2af0beb5626961339e7f75c7
4
- data.tar.gz: 1a58b01ab3ab768c37f497e24329d0e4e3d85dc64af7b1a338c5447b742f4a4b
3
+ metadata.gz: 9b9ff81249ed6289557c131a6b6183dd77b494af67b71cb4845e69628092801e
4
+ data.tar.gz: ca4a71a59985f62685056833ad288a63ff33b0ca233a7a41571cb7f0963b096e
5
5
  SHA512:
6
- metadata.gz: e349310577754cebfe7fa4d067727946c19fb0b291f1f2305e87d26a7d30371fd37c80bcdb58039936c90c75618157904f3141722e5f3f83c9b0e05afb76e447
7
- data.tar.gz: 63fe5f4a016b32ab3d299e5cd9368f226a575bd725040931b8dd28db7e8a35cef7fbd78f0ec2713108dd508a974c7b783734a02e4d724a3b40e00afb7502693e
6
+ metadata.gz: ae9eedad45c7425785150927537a604084c60215c1bdfa796e7bc018fdddbaff2e431af97f5611d15ad5064ad9640f4af38c24f44712eb3567a66bba9f128038
7
+ data.tar.gz: 67135252c34441946ff069a1c6283545656a60b77306f8e6c019d319f0a164fd17dd898a21fcbfeba5490a52598812a804de52ad5fa498b6eee77a7731bc49ae
data/Appraisals CHANGED
@@ -11,3 +11,7 @@ end
11
11
  appraise "activerecord-7.1" do
12
12
  gem "activerecord", "7.1.1"
13
13
  end
14
+
15
+ appraise "activerecord-head" do
16
+ gem "activerecord", git: "https://github.com/rails/rails.git", branch: "main"
17
+ end
data/CHANGELOG.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Unreleased
2
2
 
3
+ # 4.4.0 (Aug, 2024)
4
+ * Add support for retrying chunks when running into max_binlog_cache_size exceeded error
5
+
3
6
  # 4.3.0 (Aug, 2024)
4
7
  * Drop support for Ruby 3.0, as it reached its EOL
5
8
  * Add support for next Rails version
data/Gemfile.lock CHANGED
@@ -1,28 +1,29 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- lhm-shopify (4.3.0)
4
+ lhm-shopify (4.4.0)
5
5
  retriable (>= 3.0.0)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activemodel (7.1.3.4)
11
- activesupport (= 7.1.3.4)
12
- activerecord (7.1.3.4)
13
- activemodel (= 7.1.3.4)
14
- activesupport (= 7.1.3.4)
10
+ activemodel (7.2.0)
11
+ activesupport (= 7.2.0)
12
+ activerecord (7.2.0)
13
+ activemodel (= 7.2.0)
14
+ activesupport (= 7.2.0)
15
15
  timeout (>= 0.4.0)
16
- activesupport (7.1.3.4)
16
+ activesupport (7.2.0)
17
17
  base64
18
18
  bigdecimal
19
- concurrent-ruby (~> 1.0, >= 1.0.2)
19
+ concurrent-ruby (~> 1.0, >= 1.3.1)
20
20
  connection_pool (>= 2.2.5)
21
21
  drb
22
22
  i18n (>= 1.6, < 2)
23
+ logger (>= 1.4.2)
23
24
  minitest (>= 5.1)
24
- mutex_m
25
- tzinfo (~> 2.0)
25
+ securerandom (>= 0.3)
26
+ tzinfo (~> 2.0, >= 2.0.5)
26
27
  after_do (0.4.0)
27
28
  appraisal (2.5.0)
28
29
  bundler
@@ -31,20 +32,21 @@ GEM
31
32
  base64 (0.2.0)
32
33
  bigdecimal (3.1.8)
33
34
  byebug (11.1.3)
34
- concurrent-ruby (1.3.3)
35
+ concurrent-ruby (1.3.4)
35
36
  connection_pool (2.4.1)
36
37
  docile (1.4.1)
37
38
  drb (2.2.1)
38
39
  i18n (1.14.5)
39
40
  concurrent-ruby (~> 1.0)
40
- minitest (5.24.1)
41
+ logger (1.6.0)
42
+ minitest (5.25.1)
41
43
  mocha (2.4.5)
42
44
  ruby2_keywords (>= 0.0.5)
43
- mutex_m (0.2.0)
44
45
  mysql2 (0.5.6)
45
46
  rake (13.2.1)
46
47
  retriable (3.1.2)
47
48
  ruby2_keywords (0.0.5)
49
+ securerandom (0.3.1)
48
50
  simplecov (0.22.0)
49
51
  docile (~> 1.1)
50
52
  simplecov-html (~> 0.11)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- lhm-shopify (4.3.0)
4
+ lhm-shopify (4.4.0)
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 (4.3.0)
4
+ lhm-shopify (4.4.0)
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 (4.3.0)
4
+ lhm-shopify (4.4.0)
5
5
  retriable (>= 3.0.0)
6
6
 
7
7
  GEM
@@ -0,0 +1,88 @@
1
+ GIT
2
+ remote: https://github.com/rails/rails.git
3
+ revision: f4a9b7618fc32f0d3b2c0ff03a3f34f4964cc553
4
+ branch: main
5
+ specs:
6
+ activemodel (8.0.0.alpha)
7
+ activesupport (= 8.0.0.alpha)
8
+ activerecord (8.0.0.alpha)
9
+ activemodel (= 8.0.0.alpha)
10
+ activesupport (= 8.0.0.alpha)
11
+ timeout (>= 0.4.0)
12
+ activesupport (8.0.0.alpha)
13
+ base64
14
+ bigdecimal
15
+ concurrent-ruby (~> 1.0, >= 1.3.1)
16
+ connection_pool (>= 2.2.5)
17
+ drb
18
+ i18n (>= 1.6, < 2)
19
+ logger (>= 1.4.2)
20
+ minitest (>= 5.1)
21
+ securerandom (>= 0.3)
22
+ tzinfo (~> 2.0, >= 2.0.5)
23
+
24
+ PATH
25
+ remote: ..
26
+ specs:
27
+ lhm-shopify (4.4.0)
28
+ retriable (>= 3.0.0)
29
+
30
+ GEM
31
+ remote: https://rubygems.org/
32
+ specs:
33
+ after_do (0.4.0)
34
+ appraisal (2.5.0)
35
+ bundler
36
+ rake
37
+ thor (>= 0.14.0)
38
+ base64 (0.2.0)
39
+ bigdecimal (3.1.8)
40
+ byebug (11.1.3)
41
+ concurrent-ruby (1.3.3)
42
+ connection_pool (2.4.1)
43
+ docile (1.4.1)
44
+ drb (2.2.1)
45
+ i18n (1.14.5)
46
+ concurrent-ruby (~> 1.0)
47
+ logger (1.6.0)
48
+ minitest (5.24.1)
49
+ mocha (2.4.5)
50
+ ruby2_keywords (>= 0.0.5)
51
+ mysql2 (0.5.6)
52
+ rake (13.2.1)
53
+ retriable (3.1.2)
54
+ ruby2_keywords (0.0.5)
55
+ securerandom (0.3.1)
56
+ simplecov (0.22.0)
57
+ docile (~> 1.1)
58
+ simplecov-html (~> 0.11)
59
+ simplecov_json_formatter (~> 0.1)
60
+ simplecov-html (0.12.3)
61
+ simplecov_json_formatter (0.1.4)
62
+ thor (1.3.1)
63
+ timeout (0.4.1)
64
+ toxiproxy (2.0.2)
65
+ trilogy (2.8.1)
66
+ tzinfo (2.0.6)
67
+ concurrent-ruby (~> 1.0)
68
+
69
+ PLATFORMS
70
+ arm64-darwin-22
71
+ x86_64-linux
72
+
73
+ DEPENDENCIES
74
+ activerecord!
75
+ after_do
76
+ appraisal
77
+ byebug
78
+ lhm-shopify!
79
+ minitest
80
+ mocha
81
+ mysql2
82
+ rake
83
+ simplecov
84
+ toxiproxy
85
+ trilogy
86
+
87
+ BUNDLED WITH
88
+ 2.2.22
data/lib/lhm/chunker.rb CHANGED
@@ -37,6 +37,10 @@ module Lhm
37
37
  )
38
38
  end
39
39
 
40
+ def handle_max_binlog_exceeded_error
41
+ @throttler.backoff_stride
42
+ end
43
+
40
44
  def execute
41
45
  @start_time = Time.now
42
46
 
@@ -47,7 +51,18 @@ module Lhm
47
51
  top = upper_id(@next_to_insert, stride)
48
52
  verify_can_run
49
53
 
50
- affected_rows = ChunkInsert.new(@migration, @connection, bottom, top, @retry_options).insert_and_return_count_of_rows_created
54
+ begin
55
+ affected_rows = ChunkInsert.new(@migration, @connection, bottom, top, @retry_options).insert_and_return_count_of_rows_created
56
+ rescue ActiveRecord::StatementInvalid => e
57
+ if e.message.downcase.include?("transaction required more than 'max_binlog_cache_size' bytes of storage")
58
+ Lhm.logger.info("Encountered max_binlog_cache_size error, attempting to reduce stride size")
59
+ handle_max_binlog_exceeded_error
60
+ next
61
+ else
62
+ raise e
63
+ end
64
+ end
65
+
51
66
  expected_rows = top - bottom + 1
52
67
 
53
68
  # Only log the chunker progress every 5 minutes instead of every iteration
@@ -110,6 +125,5 @@ module Lhm
110
125
  return if @chunk_finder.table_empty?
111
126
  @chunk_finder.validate
112
127
  end
113
-
114
128
  end
115
129
  end
@@ -5,6 +5,8 @@ module Lhm
5
5
 
6
6
  DEFAULT_TIMEOUT = 0.1
7
7
  DEFAULT_STRIDE = 2_000
8
+ DEFAULT_BACKOFF_REDUCTION_FACTOR = 0.2 # 20%
9
+ MIN_STRIDE_SIZE = 1
8
10
 
9
11
  attr_accessor :timeout_seconds
10
12
  attr_accessor :stride
@@ -12,6 +14,37 @@ module Lhm
12
14
  def initialize(options = {})
13
15
  @timeout_seconds = options[:delay] || DEFAULT_TIMEOUT
14
16
  @stride = options[:stride] || DEFAULT_STRIDE
17
+ @backoff_reduction_factor = options[:backoff_reduction_factor] || DEFAULT_BACKOFF_REDUCTION_FACTOR
18
+ @min_stride_size = options[:min_stride_size] || MIN_STRIDE_SIZE
19
+
20
+ if @backoff_reduction_factor >= 1 || @backoff_reduction_factor <= 0
21
+ raise ArgumentError, 'backoff_reduction_factor must be between greater than 0, and less than 1'
22
+ end
23
+
24
+ if @min_stride_size < 1
25
+ raise ArgumentError, 'min_stride_size must be an integer greater than 0'
26
+ end
27
+
28
+ if !@min_stride_size.is_a?(Integer)
29
+ raise ArgumentError, 'min_stride_size must be an integer'
30
+ end
31
+
32
+ if @min_stride_size > @stride
33
+ raise ArgumentError, 'min_stride_size must be less than or equal to stride'
34
+ end
35
+ end
36
+
37
+ def backoff_stride
38
+ new_stride = (@stride * (1 - @backoff_reduction_factor)).to_i
39
+
40
+ if new_stride == @stride
41
+ raise RuntimeError, "Cannot backoff any further"
42
+ end
43
+
44
+ if new_stride < @min_stride_size
45
+ raise RuntimeError, "Cannot reduce stride below #{@min_stride_size}"
46
+ end
47
+ @stride = new_stride
15
48
  end
16
49
 
17
50
  def execute
data/lib/lhm/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # Schmidt
3
3
 
4
4
  module Lhm
5
- VERSION = '4.3.0'
5
+ VERSION = '4.4.0'
6
6
  end
@@ -5,6 +5,7 @@ require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
5
5
  require 'lhm/table'
6
6
  require 'lhm/migration'
7
7
 
8
+
8
9
  describe Lhm::Chunker do
9
10
  include IntegrationHelper
10
11
 
@@ -17,6 +18,7 @@ describe Lhm::Chunker do
17
18
  @migration = Lhm::Migration.new(@origin, @destination)
18
19
  @logs = StringIO.new
19
20
  Lhm.logger = Logger.new(@logs)
21
+ set_max_binlog_size(1024 * 1024 * 1024) # necessary since some tests reduce binlog size (1gb default)
20
22
  end
21
23
 
22
24
  def log_messages
@@ -306,6 +308,70 @@ describe Lhm::Chunker do
306
308
  value(count_all(@destination.name)).must_equal(0)
307
309
  end
308
310
  end
311
+
312
+ it 'should reduce stride size if chunker runs into max_binlog_cache_size error' do
313
+ init_stride = 1000
314
+
315
+ # Create a bunch of users
316
+ n = 0
317
+ 25.times do |i|
318
+ execute "BEGIN"
319
+ init_stride.times do # each batch is 10 * 1000 * i bytes, so each batch of 1000 will range from 10kb - 250kb
320
+ n += 1
321
+ id = n
322
+ username_data = "a" * 10 * i
323
+ execute "insert into origin (id, common) values (#{id}, '#{username_data}')"
324
+ end
325
+ execute "COMMIT"
326
+ end
327
+
328
+ # reduce binlog size to 8kb
329
+ set_max_binlog_size(1024 * 8)
330
+
331
+ throttler = Lhm::Throttler::Time.new(stride: init_stride )
332
+ chunker = Lhm::Chunker.new(
333
+ @migration, connection, { throttler: throttler }
334
+ )
335
+
336
+ # start chunking
337
+ chunker.run
338
+ assert init_stride > throttler.stride
339
+ end
340
+
341
+ it 'should throw an error when stride cannot be reduced beyond min stride size' do
342
+ init_stride = 100
343
+ min_stride_size = 50
344
+
345
+ # Create a bunch of users
346
+ n = 0
347
+ 25.times do |i|
348
+ execute "BEGIN"
349
+ init_stride.times do # each batch is init_stride * 250 bytes, so even at min_stride of 20,
350
+ # batch_size will be greater than 4kb (50 * 250kb = 12.5kb)
351
+ n += 1
352
+ id = n
353
+ username_data = "a" * 250
354
+ execute "insert into origin (id, common) values (#{id}, '#{username_data}')"
355
+ end
356
+ execute "COMMIT"
357
+ end
358
+
359
+ # reduce binlog size to 4kb
360
+ set_max_binlog_size(1024 * 4)
361
+ throttler = Lhm::Throttler::Time.new(stride: init_stride, min_stride_size: min_stride_size, backoff_reduction_factor: 0.9)
362
+
363
+ chunker = Lhm::Chunker.new(
364
+ @migration, connection, { throttler: throttler }
365
+ )
366
+
367
+ # start chunking
368
+ exception = assert_raises do
369
+ chunker.run
370
+ end
371
+
372
+ assert RuntimeError = exception.class
373
+ assert "Cannot reduce stride below #{min_stride_size}" == exception.message
374
+ end
309
375
  end
310
376
 
311
377
  def index_key(table_name, index_name)
@@ -315,4 +381,13 @@ describe Lhm::Chunker do
315
381
  index_name
316
382
  end
317
383
  end
384
+
385
+ def set_global_variable(name, value)
386
+ execute("set global #{name} = #{value}")
387
+ connection.reconnect!
388
+ end
389
+
390
+ def set_max_binlog_size(value)
391
+ set_global_variable('max_binlog_cache_size', value)
392
+ end
318
393
  end
@@ -111,6 +111,66 @@ describe Lhm::Throttler do
111
111
  end
112
112
  end
113
113
 
114
+ describe 'when using backoff functionality' do
115
+ it 'should backoff by default amount' do
116
+ @mock.setup_throttler(:time_throttler, stride: 100)
117
+ @mock.throttler.backoff_stride
118
+ value(@mock.throttler.stride).must_equal 80
119
+ end
120
+
121
+ it 'should backoff by specified amount' do
122
+ @mock.setup_throttler(:time_throttler, backoff_reduction_factor: 0.5, stride: 100)
123
+ @mock.throttler.backoff_stride
124
+ value(@mock.throttler.stride).must_equal 50
125
+ end
126
+
127
+ it 'should throw an error when backoff exceeds limit' do
128
+ @mock.setup_throttler(:time_throttler, backoff_reduction_factor: 0.2, stride: 1000, min_stride_size: 900)
129
+ proc { @mock.throttler.backoff_stride }.must_raise RuntimeError
130
+ end
131
+
132
+ it 'should throw an error when backoff cannot be done anymore' do
133
+ @mock.setup_throttler(:time_throttler, backoff_reduction_factor: 0.2, stride: 1, min_stride_size: 1)
134
+ proc { @mock.throttler.backoff_stride }.must_raise RuntimeError
135
+ end
136
+
137
+ it 'should throw an error when backoff reduction factor is not less than one' do
138
+ assert_raises ArgumentError do
139
+ @mock.setup_throttler(:time_throttler, backoff_reduction_factor: 1)
140
+ end
141
+ end
142
+
143
+ it 'should throw an error when backoff reduction factor is not greater than zero' do
144
+ assert_raises ArgumentError do
145
+ @mock.setup_throttler(:time_throttler, backoff_reduction_factor: 0)
146
+ end
147
+ end
148
+
149
+ it 'should throw an error when backoff reduction factor is negative' do
150
+ assert_raises ArgumentError do
151
+ @mock.setup_throttler(:time_throttler, backoff_reduction_factor: -0.5)
152
+ end
153
+ end
154
+
155
+ it 'should throw an error when min_stride_size is not an integer' do
156
+ assert_raises ArgumentError do
157
+ @mock.setup_throttler(:time_throttler, min_stride_size: 0.5)
158
+ end
159
+ end
160
+
161
+ it 'should throw an error when min_stride_size is not greater than 1' do
162
+ assert_raises ArgumentError do
163
+ @mock.setup_throttler(:time_throttler, min_stride_size: -12)
164
+ end
165
+ end
166
+
167
+ it 'should throw an error when min_stride_size is greater than inital stride size' do
168
+ assert_raises ArgumentError do
169
+ @mock.setup_throttler(:time_throttler, min_stride_size: 1000, stride: 500)
170
+ end
171
+ end
172
+ end
173
+
114
174
  describe '#throttler' do
115
175
 
116
176
  it 'returns the default Time based' do
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.3.0
4
+ version: 4.4.0
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: 2024-08-05 00:00:00.000000000 Z
15
+ date: 2024-08-20 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: retriable
@@ -213,6 +213,7 @@ files:
213
213
  - gemfiles/activerecord_7.1.gemfile
214
214
  - gemfiles/activerecord_7.1.gemfile.lock
215
215
  - gemfiles/activerecord_head.gemfile
216
+ - gemfiles/activerecord_head.gemfile.lock
216
217
  - lhm.gemspec
217
218
  - lib/lhm-shopify.rb
218
219
  - lib/lhm.rb
@@ -325,7 +326,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
325
326
  - !ruby/object:Gem::Version
326
327
  version: '0'
327
328
  requirements: []
328
- rubygems_version: 3.5.16
329
+ rubygems_version: 3.5.17
329
330
  signing_key:
330
331
  specification_version: 4
331
332
  summary: online schema changer for mysql