lhm-shopify 3.3.5 → 3.4.2

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: 0c187c9f0e9bd183d5eb227f83ba9cce6a57ee5b03e4732312470a737f2d8a13
4
- data.tar.gz: 54797e75f27f2f174ffd0eb07cd6d1fad57a9d16542e5baa0a56cb1130fcd88a
3
+ metadata.gz: cfb1802db81b2673ea5e52acec84b4e457a8d5debb74f5391fcb282bf1fc8484
4
+ data.tar.gz: 88abd0ae0484efa69777b0284c253788996b2148c0cb61fc035c0ccd7d49627c
5
5
  SHA512:
6
- metadata.gz: 9f11850e292435adb700695686887945d729ec8ec3b2d4b658fd1d6dd297f198e17711b26f2f1533cca0a58cdcb5f45a87bdbb1741f0cd988b57dfb47bd90d5b
7
- data.tar.gz: ba2e873e51070ac3658644f90c7f54a1680707e8aa48db2d844fdcbfd2de83a5a96abbc0521eae9e55894d5e93ee399f9327df4c56f6b95945e381894e93ff6a
6
+ metadata.gz: 4b98a803678e1ab543997d4e6f9031b02511324293c2d97b3f94938ae6b701e55c122633ab6044920363c56d9746e35f9b131433eb65108437c4ffeb8ca2e5ea
7
+ data.tar.gz: '018c3a179dfdf90f9b5fd4bd3f67636644fc47eda7b0b825aee00330db0f83a59cf6855a5e18105493a89ac0a372ac26fd2c356148282449ca2de84fc56c5179'
data/CHANGELOG.md CHANGED
@@ -1,4 +1,16 @@
1
- # 3.3.5 (Jul 5, 2021)
1
+ # 3.4.1 (Sep 22, 2021)
2
+
3
+ * Add better logging to the LHM components (https://github.com/Shopify/lhm/pull/108)
4
+
5
+ # 3.4.0 (Jul 19, 2021)
6
+
7
+ * Log or raise on unexpected duplicated entry warnings during INSERT IGNORE (https://github.com/Shopify/lhm/pull/100)
8
+
9
+ # 3.3.6 (Jul 7, 2021)
10
+
11
+ * Add lhm-shopify.rb to require lhm
12
+
13
+ # 3.3.5 (Jul 5, 2021)
2
14
 
3
15
  * Add comment and collate copying to rename_column
4
16
  * Publish to rubygems
data/lib/lhm/chunker.rb CHANGED
@@ -20,6 +20,7 @@ module Lhm
20
20
  @connection = connection
21
21
  @chunk_finder = ChunkFinder.new(migration, connection, options)
22
22
  @options = options
23
+ @raise_on_warnings = options.fetch(:raise_on_warnings, false)
23
24
  @verifier = options[:verifier]
24
25
  if @throttler = options[:throttler]
25
26
  @throttler.connection = @connection if @throttler.respond_to?(:connection=)
@@ -36,6 +37,8 @@ module Lhm
36
37
  end
37
38
 
38
39
  def execute
40
+ @start_time = Time.now
41
+
39
42
  return if @chunk_finder.table_empty?
40
43
  @next_to_insert = @start
41
44
  while @next_to_insert <= @limit || (@start == @limit)
@@ -44,11 +47,26 @@ module Lhm
44
47
  verify_can_run
45
48
 
46
49
  affected_rows = ChunkInsert.new(@migration, @connection, bottom, top, @options).insert_and_return_count_of_rows_created
50
+ expected_rows = top - bottom + 1
51
+
52
+ # Only log the chunker progress every 5 minutes instead of every iteration
53
+ current_time = Time.now
54
+ if current_time - @start_time > (5 * 60)
55
+ Lhm.logger.info("Inserted #{affected_rows} rows into the destination table from #{bottom} to #{top}")
56
+ @start_time = current_time
57
+ end
58
+
59
+ if affected_rows < expected_rows
60
+ raise_on_non_pk_duplicate_warning
61
+ end
62
+
47
63
  if @throttler && affected_rows > 0
48
64
  @throttler.run
49
65
  end
50
- @printer.notify(bottom, @limit)
66
+
51
67
  @next_to_insert = top + 1
68
+ @printer.notify(bottom, @limit)
69
+
52
70
  break if @start == @limit
53
71
  end
54
72
  @printer.end
@@ -59,6 +77,16 @@ module Lhm
59
77
 
60
78
  private
61
79
 
80
+ def raise_on_non_pk_duplicate_warning
81
+ @connection.query("show warnings").each do |level, code, message|
82
+ unless message.match?(/Duplicate entry .+ for key 'PRIMARY'/)
83
+ m = "Unexpected warning found for inserted row: #{message}"
84
+ Lhm.logger.warn(m)
85
+ raise Error.new(m) if @raise_on_warnings
86
+ end
87
+ end
88
+ end
89
+
62
90
  def bottom
63
91
  @next_to_insert
64
92
  end
@@ -83,5 +111,6 @@ module Lhm
83
111
  return if @chunk_finder.table_empty?
84
112
  @chunk_finder.validate
85
113
  end
114
+
86
115
  end
87
116
  end
@@ -63,11 +63,12 @@ module Lhm
63
63
  retriable_connection.execute(ddl)
64
64
  end
65
65
  end
66
+ Lhm.logger.info("Dropped triggers on #{@lhm_triggers_for_origin.join(', ')}")
67
+ Lhm.logger.info("Dropped tables #{@lhm_triggers_for_origin.join(', ')}")
66
68
  end
67
69
 
68
70
  def report_ddls
69
- puts "The following DDLs would be executed:"
70
- ddls.each { |ddl| puts ddl }
71
+ Lhm.logger.info("The following DDLs would be executed: #{ddls}")
71
72
  end
72
73
  end
73
74
  end
data/lib/lhm/entangler.rb CHANGED
@@ -94,6 +94,7 @@ module Lhm
94
94
  retriable_connection.execute(stmt)
95
95
  end
96
96
  end
97
+ Lhm.logger.info("Created triggers on #{@origin.name}")
97
98
  end
98
99
 
99
100
  def after
@@ -102,6 +103,7 @@ module Lhm
102
103
  retriable_connection.execute(stmt)
103
104
  end
104
105
  end
106
+ Lhm.logger.info("Dropped triggers on #{@origin.name}")
105
107
  end
106
108
 
107
109
  def revert
data/lib/lhm/migrator.rb CHANGED
@@ -214,6 +214,8 @@ module Lhm
214
214
  replacement = %{CREATE TABLE `#{ @origin.destination_name }`}
215
215
  stmt = @origin.ddl.gsub(original, replacement)
216
216
  @connection.execute(tagged(stmt))
217
+
218
+ Lhm.logger.info("Created destination table #{@origin.destination_name}")
217
219
  end
218
220
 
219
221
  def destination_read
data/lib/lhm/printer.rb CHANGED
@@ -12,26 +12,30 @@ module Lhm
12
12
  end
13
13
  end
14
14
 
15
- class Percentage < Base
15
+ class Percentage
16
16
  def initialize
17
- super
18
17
  @max_length = 0
19
18
  end
20
19
 
21
20
  def notify(lowest, highest)
22
21
  return if !highest || highest == 0
22
+
23
+ # The argument lowest represents the next_to_insert row id, and highest represents the
24
+ # maximum id upto which chunker has to copy the data.
25
+ # If all the rows are inserted upto highest, then lowest passed here from chunker was
26
+ # highest + 1, which leads to the printer printing the progress > 100%.
27
+ return if lowest >= highest
28
+
23
29
  message = "%.2f%% (#{lowest}/#{highest}) complete" % (lowest.to_f / highest * 100.0)
24
30
  write(message)
25
31
  end
26
32
 
27
33
  def end
28
34
  write('100% complete')
29
- @output.write "\n"
30
35
  end
31
36
 
32
37
  def exception(e)
33
- write("failed: #{e}")
34
- @output.write "\n"
38
+ Lhm.logger.error("failed: #{e}")
35
39
  end
36
40
 
37
41
  private
@@ -42,7 +46,7 @@ module Lhm
42
46
  extra = 0
43
47
  end
44
48
 
45
- @output.write "\r#{message}" + (' ' * extra)
49
+ Lhm.logger.info(message)
46
50
  end
47
51
  end
48
52
 
data/lib/lhm/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # Schmidt
3
3
 
4
4
  module Lhm
5
- VERSION = '3.3.5'
5
+ VERSION = '3.4.2'
6
6
  end
@@ -0,0 +1 @@
1
+ require "lhm"
data/lib/lhm.rb CHANGED
@@ -114,17 +114,21 @@ module Lhm
114
114
  triggers.each do |trigger|
115
115
  connection.execute("drop trigger if exists #{trigger}")
116
116
  end
117
+ logger.info("Dropped triggers #{triggers.join(', ')}")
118
+
117
119
  tables.each do |table|
118
120
  connection.execute("drop table if exists #{table}")
119
121
  end
122
+ logger.info("Dropped tables #{tables.join(', ')}")
123
+
120
124
  true
121
125
  elsif tables.empty? && triggers.empty?
122
- puts 'Everything is clean. Nothing to do.'
126
+ logger.info('Everything is clean. Nothing to do.')
123
127
  true
124
128
  else
125
- puts "Would drop LHM backup tables: #{tables.join(', ')}."
126
- puts "Would drop LHM triggers: #{triggers.join(', ')}."
127
- puts 'Run with Lhm.cleanup(true) to drop all LHM triggers and tables, or Lhm.cleanup_current_run(true, table_name) to clean up a specific LHM.'
129
+ logger.info("Would drop LHM backup tables: #{tables.join(', ')}.")
130
+ logger.info("Would drop LHM triggers: #{triggers.join(', ')}.")
131
+ logger.info('Run with Lhm.cleanup(true) to drop all LHM triggers and tables, or Lhm.cleanup_current_run(true, table_name) to clean up a specific LHM.')
128
132
  false
129
133
  end
130
134
  end
@@ -1,7 +1,6 @@
1
-
2
1
  CREATE TABLE `composite_primary_key` (
3
2
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
4
3
  `shop_id` bigint(20) NOT NULL,
5
- PRIMARY KEY (`shop_id`,`id`),
4
+ CONSTRAINT `pk_composite` PRIMARY KEY (`shop_id`,`id`),
6
5
  INDEX `index_key_id` (`id`)
7
6
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8
@@ -0,0 +1,6 @@
1
+ CREATE TABLE `composite_primary_key_dest` (
2
+ `id` bigint(20) NOT NULL AUTO_INCREMENT,
3
+ `shop_id` bigint(20) NOT NULL,
4
+ CONSTRAINT `pk_composite` PRIMARY KEY (`shop_id`,`id`),
5
+ INDEX `index_key_id` (`id`)
6
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
@@ -0,0 +1,6 @@
1
+ CREATE TABLE `custom_primary_key_dest` (
2
+ `id` int(11) NOT NULL AUTO_INCREMENT,
3
+ `pk` varchar(255),
4
+ PRIMARY KEY (`pk`),
5
+ UNIQUE KEY `index_custom_primary_key_on_id` (`id`)
6
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
@@ -58,7 +58,7 @@ describe Lhm::AtomicSwitcher do
58
58
  switcher.send :define_singleton_method, :atomic_switch do
59
59
  'SELECT * FROM nonexistent'
60
60
  end
61
- -> { switcher.run }.must_raise(ActiveRecord::StatementInvalid)
61
+ value(-> { switcher.run }).must_raise(ActiveRecord::StatementInvalid)
62
62
  end
63
63
 
64
64
  it "should raise when destination doesn't exist" do
@@ -75,8 +75,8 @@ describe Lhm::AtomicSwitcher do
75
75
  switcher.run
76
76
 
77
77
  slave do
78
- data_source_exists?(@origin).must_equal true
79
- table_read(@migration.archive_name).columns.keys.must_include 'origin'
78
+ value(data_source_exists?(@origin)).must_equal true
79
+ value(table_read(@migration.archive_name).columns.keys).must_include 'origin'
80
80
  end
81
81
  end
82
82
 
@@ -85,8 +85,8 @@ describe Lhm::AtomicSwitcher do
85
85
  switcher.run
86
86
 
87
87
  slave do
88
- data_source_exists?(@destination).must_equal false
89
- table_read(@origin.name).columns.keys.must_include 'destination'
88
+ value(data_source_exists?(@destination)).must_equal false
89
+ value(table_read(@origin.name).columns.keys).must_include 'destination'
90
90
  end
91
91
  end
92
92
  end
@@ -22,7 +22,7 @@ describe Lhm::ChunkInsert do
22
22
  @instance.insert_and_return_count_of_rows_created
23
23
 
24
24
  slave do
25
- count_all(@destination.name).must_equal(1)
25
+ value(count_all(@destination.name)).must_equal(1)
26
26
  end
27
27
  end
28
28
  end
@@ -15,34 +15,120 @@ describe Lhm::Chunker do
15
15
  @origin = table_create(:origin)
16
16
  @destination = table_create(:destination)
17
17
  @migration = Lhm::Migration.new(@origin, @destination)
18
+ @logs = StringIO.new
19
+ Lhm.logger = Logger.new(@logs)
20
+ end
21
+
22
+ def log_messages
23
+ @logs.string.split("\n")
18
24
  end
19
25
 
20
26
  it 'should copy 1 row from origin to destination even if the id of the single row does not start at 1' do
21
27
  execute("insert into origin set id = 1001 ")
22
- printer = Lhm::Printer::Base.new
23
28
 
24
- def printer.notify(*) ;end
25
- def printer.end(*) [] ;end
29
+ Lhm::Chunker.new(@migration, connection, {throttler: throttler, printer: printer} ).run
30
+
31
+ slave do
32
+ value(count_all(@destination.name)).must_equal(1)
33
+ end
34
+
35
+ end
26
36
 
27
- Lhm::Chunker.new(@migration, connection, {:throttler => Lhm::Throttler::Time.new(:stride => 100), :printer => printer} ).run
37
+ it 'should copy and ignore duplicate primary key' do
38
+ execute("insert into origin set id = 1001 ")
39
+ execute("insert into origin set id = 1002 ")
40
+ execute("insert into destination set id = 1002 ")
41
+
42
+ Lhm::Chunker.new(@migration, connection, {throttler: throttler, printer: printer} ).run
28
43
 
29
44
  slave do
30
- count_all(@destination.name).must_equal(1)
45
+ value(count_all(@destination.name)).must_equal(2)
46
+ end
47
+ end
48
+
49
+ it 'should copy and ignore duplicate composite primary key' do
50
+ origin = table_create(:composite_primary_key)
51
+ destination = table_create(:composite_primary_key_dest)
52
+ migration = Lhm::Migration.new(origin, destination)
53
+
54
+ execute("insert into composite_primary_key set id = 1001, shop_id = 1")
55
+ execute("insert into composite_primary_key set id = 1002, shop_id = 1")
56
+ execute("insert into composite_primary_key_dest set id = 1002, shop_id = 1")
57
+
58
+ Lhm::Chunker.new(migration, connection, {throttler: throttler, printer: printer} ).run
59
+
60
+ slave do
61
+ value(count_all(destination.name)).must_equal(2)
62
+ end
63
+ end
64
+
65
+ it 'should copy and raise on unexpected warnings' do
66
+ origin = table_create(:custom_primary_key)
67
+ destination = table_create(:custom_primary_key_dest)
68
+ migration = Lhm::Migration.new(origin, destination)
69
+
70
+ execute("insert into custom_primary_key set id = 1001, pk = 1")
71
+ execute("insert into custom_primary_key_dest set id = 1001, pk = 2")
72
+
73
+ exception = assert_raises(Lhm::Error) do
74
+ Lhm::Chunker.new(migration, connection, {raise_on_warnings: true, throttler: throttler, printer: printer} ).run
31
75
  end
32
76
 
77
+ assert_match "Unexpected warning found for inserted row: Duplicate entry '1001' for key 'index_custom_primary_key_on_id'", exception.message
78
+ end
79
+
80
+ it 'should copy and warn on unexpected warnings by default' do
81
+ origin = table_create(:custom_primary_key)
82
+ destination = table_create(:custom_primary_key_dest)
83
+ migration = Lhm::Migration.new(origin, destination)
84
+
85
+ execute("insert into custom_primary_key set id = 1001, pk = 1")
86
+ execute("insert into custom_primary_key_dest set id = 1001, pk = 2")
87
+
88
+ Lhm::Chunker.new(migration, connection, {throttler: throttler, printer: printer} ).run
89
+
90
+ assert_equal 2, log_messages.length
91
+ assert log_messages[1].include?("Unexpected warning found for inserted row: Duplicate entry '1001' for key 'index_custom_primary_key_on_id'"), log_messages
92
+ end
93
+
94
+ it 'should log two times for two unexpected warnings' do
95
+ origin = table_create(:custom_primary_key)
96
+ destination = table_create(:custom_primary_key_dest)
97
+ migration = Lhm::Migration.new(origin, destination)
98
+
99
+ execute("insert into custom_primary_key set id = 1001, pk = 1")
100
+ execute("insert into custom_primary_key set id = 1002, pk = 2")
101
+ execute("insert into custom_primary_key_dest set id = 1001, pk = 3")
102
+ execute("insert into custom_primary_key_dest set id = 1002, pk = 4")
103
+
104
+ Lhm::Chunker.new(migration, connection, {throttler: throttler, printer: printer} ).run
105
+
106
+ assert_equal 3, log_messages.length
107
+ assert log_messages[1].include?("Unexpected warning found for inserted row: Duplicate entry '1001' for key 'index_custom_primary_key_on_id'"), log_messages
108
+ assert log_messages[2].include?("Unexpected warning found for inserted row: Duplicate entry '1002' for key 'index_custom_primary_key_on_id'"), log_messages
109
+ end
110
+
111
+ it 'should copy and warn on unexpected warnings' do
112
+ origin = table_create(:custom_primary_key)
113
+ destination = table_create(:custom_primary_key_dest)
114
+ migration = Lhm::Migration.new(origin, destination)
115
+
116
+ execute("insert into custom_primary_key set id = 1001, pk = 1")
117
+ execute("insert into custom_primary_key_dest set id = 1001, pk = 2")
118
+
119
+ Lhm::Chunker.new(migration, connection, {raise_on_warnings: false, throttler: throttler, printer: printer} ).run
120
+
121
+ assert_equal 2, log_messages.length
122
+ assert log_messages[1].include?("Unexpected warning found for inserted row: Duplicate entry '1001' for key 'index_custom_primary_key_on_id'"), log_messages
33
123
  end
34
124
 
35
125
  it 'should create the modified destination, even if the source is empty' do
36
126
  execute("truncate origin ")
37
- printer = Lhm::Printer::Base.new
38
127
 
39
- def printer.notify(*) ;end
40
- def printer.end(*) [] ;end
41
-
42
- Lhm::Chunker.new(@migration, connection, {:throttler => Lhm::Throttler::Time.new(:stride => 100), :printer => printer} ).run
128
+ Lhm::Chunker.new(@migration, connection, {throttler: throttler, printer: printer} ).run
43
129
 
44
130
  slave do
45
- count_all(@destination.name).must_equal(0)
131
+ value(count_all(@destination.name)).must_equal(0)
46
132
  end
47
133
 
48
134
  end
@@ -55,11 +141,11 @@ describe Lhm::Chunker do
55
141
  printer.expect(:end, :return_value, [])
56
142
 
57
143
  Lhm::Chunker.new(
58
- @migration, connection, { :throttler => Lhm::Throttler::Time.new(:stride => 100), :printer => printer }
144
+ @migration, connection, { throttler: throttler, printer: printer }
59
145
  ).run
60
146
 
61
147
  slave do
62
- count_all(@destination.name).must_equal(23)
148
+ value(count_all(@destination.name)).must_equal(23)
63
149
  end
64
150
 
65
151
  printer.verify
@@ -69,17 +155,13 @@ describe Lhm::Chunker do
69
155
  it 'should copy all the records of a table, even if the last chunk starts with the last record of it.' do
70
156
  11.times { |n| execute("insert into origin set id = '#{ n + 1 }'") }
71
157
 
72
- printer = Lhm::Printer::Base.new
73
-
74
- def printer.notify(*) ;end
75
- def printer.end(*) [] ;end
76
158
 
77
159
  Lhm::Chunker.new(
78
- @migration, connection, { :throttler => Lhm::Throttler::Time.new(:stride => 10), :printer => printer }
160
+ @migration, connection, { throttler: Lhm::Throttler::Time.new(stride: 10), printer: printer }
79
161
  ).run
80
162
 
81
163
  slave do
82
- count_all(@destination.name).must_equal(11)
164
+ value(count_all(@destination.name)).must_equal(11)
83
165
  end
84
166
 
85
167
  end
@@ -92,11 +174,11 @@ describe Lhm::Chunker do
92
174
  printer.expect(:end, :return_value, [])
93
175
 
94
176
  Lhm::Chunker.new(
95
- @migration, connection, { :throttler => Lhm::Throttler::SlaveLag.new(:stride => 100), :printer => printer }
177
+ @migration, connection, { throttler: Lhm::Throttler::SlaveLag.new(stride: 100), printer: printer }
96
178
  ).run
97
179
 
98
180
  slave do
99
- count_all(@destination.name).must_equal(23)
181
+ value(count_all(@destination.name)).must_equal(23)
100
182
  end
101
183
 
102
184
  printer.verify
@@ -109,19 +191,19 @@ describe Lhm::Chunker do
109
191
  printer.expects(:notify).with(instance_of(Integer), instance_of(Integer)).twice
110
192
  printer.expects(:end)
111
193
 
112
- throttler = Lhm::Throttler::SlaveLag.new(:stride => 10, :allowed_lag => 0)
194
+ throttler = Lhm::Throttler::SlaveLag.new(stride: 10, allowed_lag: 0)
113
195
  def throttler.max_current_slave_lag
114
196
  1
115
197
  end
116
198
 
117
199
  Lhm::Chunker.new(
118
- @migration, connection, { :throttler => throttler, :printer => printer }
200
+ @migration, connection, { throttler: throttler, printer: printer }
119
201
  ).run
120
202
 
121
203
  assert_equal(Lhm::Throttler::SlaveLag::INITIAL_TIMEOUT * 2 * 2, throttler.timeout_seconds)
122
204
 
123
205
  slave do
124
- count_all(@destination.name).must_equal(15)
206
+ value(count_all(@destination.name)).must_equal(15)
125
207
  end
126
208
  end
127
209
 
@@ -133,7 +215,7 @@ describe Lhm::Chunker do
133
215
  printer.expects(:verify)
134
216
  printer.expects(:end)
135
217
 
136
- throttler = Lhm::Throttler::SlaveLag.new(:stride => 10, :allowed_lag => 0)
218
+ throttler = Lhm::Throttler::SlaveLag.new(stride: 10, allowed_lag: 0)
137
219
 
138
220
  def throttler.slave_hosts
139
221
  ['127.0.0.1']
@@ -149,14 +231,14 @@ describe Lhm::Chunker do
149
231
  end
150
232
 
151
233
  Lhm::Chunker.new(
152
- @migration, connection, { :throttler => throttler, :printer => printer }
234
+ @migration, connection, { throttler: throttler, printer: printer }
153
235
  ).run
154
236
 
155
237
  assert_equal(Lhm::Throttler::SlaveLag::INITIAL_TIMEOUT, throttler.timeout_seconds)
156
238
  assert_equal(0, throttler.send(:max_current_slave_lag))
157
239
 
158
240
  slave do
159
- count_all(@destination.name).must_equal(15)
241
+ value(count_all(@destination.name)).must_equal(15)
160
242
  end
161
243
 
162
244
  printer.verify
@@ -171,14 +253,14 @@ describe Lhm::Chunker do
171
253
 
172
254
  exception = assert_raises do
173
255
  Lhm::Chunker.new(
174
- @migration, connection, { :verifier => failer, :printer => printer, :throttler => Lhm::Throttler::Time.new(:stride => 100) }
256
+ @migration, connection, { verifier: failer, printer: printer, throttler: throttler }
175
257
  ).run
176
258
  end
177
259
 
178
260
  assert_match "Verification failed, aborting early", exception.message
179
261
 
180
262
  slave do
181
- count_all(@destination.name).must_equal(0)
263
+ value(count_all(@destination.name)).must_equal(0)
182
264
  end
183
265
  end
184
266
  end