lhm-teak 3.6.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.
Files changed (112) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +43 -0
  3. data/.gitignore +12 -0
  4. data/.rubocop.yml +183 -0
  5. data/.travis.yml +21 -0
  6. data/Appraisals +24 -0
  7. data/CHANGELOG.md +254 -0
  8. data/Gemfile +5 -0
  9. data/Gemfile.lock +67 -0
  10. data/LICENSE +27 -0
  11. data/README.md +335 -0
  12. data/Rakefile +33 -0
  13. data/dev.yml +45 -0
  14. data/docker-compose.yml +60 -0
  15. data/gemfiles/activerecord_5.2.gemfile +9 -0
  16. data/gemfiles/activerecord_5.2.gemfile.lock +66 -0
  17. data/gemfiles/activerecord_6.0.gemfile +7 -0
  18. data/gemfiles/activerecord_6.0.gemfile.lock +68 -0
  19. data/gemfiles/activerecord_6.1.gemfile +7 -0
  20. data/gemfiles/activerecord_6.1.gemfile.lock +67 -0
  21. data/gemfiles/activerecord_7.0.0.alpha2.gemfile +7 -0
  22. data/gemfiles/activerecord_7.0.0.alpha2.gemfile.lock +65 -0
  23. data/lhm.gemspec +38 -0
  24. data/lib/lhm/atomic_switcher.rb +46 -0
  25. data/lib/lhm/chunk_finder.rb +62 -0
  26. data/lib/lhm/chunk_insert.rb +61 -0
  27. data/lib/lhm/chunker.rb +95 -0
  28. data/lib/lhm/cleanup/current.rb +71 -0
  29. data/lib/lhm/command.rb +48 -0
  30. data/lib/lhm/connection.rb +108 -0
  31. data/lib/lhm/entangler.rb +112 -0
  32. data/lib/lhm/intersection.rb +51 -0
  33. data/lib/lhm/invoker.rb +100 -0
  34. data/lib/lhm/locked_switcher.rb +76 -0
  35. data/lib/lhm/migration.rb +51 -0
  36. data/lib/lhm/migrator.rb +244 -0
  37. data/lib/lhm/printer.rb +63 -0
  38. data/lib/lhm/proxysql_helper.rb +10 -0
  39. data/lib/lhm/railtie.rb +9 -0
  40. data/lib/lhm/sql_helper.rb +77 -0
  41. data/lib/lhm/sql_retry.rb +180 -0
  42. data/lib/lhm/table.rb +121 -0
  43. data/lib/lhm/table_name.rb +23 -0
  44. data/lib/lhm/test_support.rb +35 -0
  45. data/lib/lhm/throttler/slave_lag.rb +162 -0
  46. data/lib/lhm/throttler/threads_running.rb +53 -0
  47. data/lib/lhm/throttler/time.rb +29 -0
  48. data/lib/lhm/throttler.rb +36 -0
  49. data/lib/lhm/timestamp.rb +11 -0
  50. data/lib/lhm/version.rb +6 -0
  51. data/lib/lhm-shopify.rb +1 -0
  52. data/lib/lhm.rb +156 -0
  53. data/scripts/helpers/wait-for-dbs.sh +21 -0
  54. data/scripts/mysql/reader/create_replication.sql +10 -0
  55. data/scripts/mysql/writer/create_test_db.sql +1 -0
  56. data/scripts/mysql/writer/create_users.sql +6 -0
  57. data/scripts/proxysql/proxysql.cnf +117 -0
  58. data/shipit.rubygems.yml +0 -0
  59. data/spec/.lhm.example +4 -0
  60. data/spec/README.md +58 -0
  61. data/spec/fixtures/bigint_table.ddl +4 -0
  62. data/spec/fixtures/composite_primary_key.ddl +6 -0
  63. data/spec/fixtures/composite_primary_key_dest.ddl +6 -0
  64. data/spec/fixtures/custom_primary_key.ddl +6 -0
  65. data/spec/fixtures/custom_primary_key_dest.ddl +6 -0
  66. data/spec/fixtures/destination.ddl +6 -0
  67. data/spec/fixtures/lines.ddl +7 -0
  68. data/spec/fixtures/origin.ddl +6 -0
  69. data/spec/fixtures/permissions.ddl +5 -0
  70. data/spec/fixtures/small_table.ddl +4 -0
  71. data/spec/fixtures/tracks.ddl +5 -0
  72. data/spec/fixtures/users.ddl +14 -0
  73. data/spec/fixtures/wo_id_int_column.ddl +6 -0
  74. data/spec/integration/atomic_switcher_spec.rb +129 -0
  75. data/spec/integration/chunk_insert_spec.rb +30 -0
  76. data/spec/integration/chunker_spec.rb +269 -0
  77. data/spec/integration/cleanup_spec.rb +147 -0
  78. data/spec/integration/database.yml +25 -0
  79. data/spec/integration/entangler_spec.rb +68 -0
  80. data/spec/integration/integration_helper.rb +252 -0
  81. data/spec/integration/invoker_spec.rb +33 -0
  82. data/spec/integration/lhm_spec.rb +659 -0
  83. data/spec/integration/lock_wait_timeout_spec.rb +30 -0
  84. data/spec/integration/locked_switcher_spec.rb +50 -0
  85. data/spec/integration/proxysql_spec.rb +34 -0
  86. data/spec/integration/sql_retry/db_connection_helper.rb +52 -0
  87. data/spec/integration/sql_retry/lock_wait_spec.rb +127 -0
  88. data/spec/integration/sql_retry/lock_wait_timeout_test_helper.rb +114 -0
  89. data/spec/integration/sql_retry/proxysql_helper.rb +22 -0
  90. data/spec/integration/sql_retry/retry_with_proxysql_spec.rb +109 -0
  91. data/spec/integration/table_spec.rb +83 -0
  92. data/spec/integration/toxiproxy_helper.rb +40 -0
  93. data/spec/test_helper.rb +69 -0
  94. data/spec/unit/atomic_switcher_spec.rb +29 -0
  95. data/spec/unit/chunk_finder_spec.rb +73 -0
  96. data/spec/unit/chunk_insert_spec.rb +67 -0
  97. data/spec/unit/chunker_spec.rb +176 -0
  98. data/spec/unit/connection_spec.rb +111 -0
  99. data/spec/unit/entangler_spec.rb +187 -0
  100. data/spec/unit/intersection_spec.rb +51 -0
  101. data/spec/unit/lhm_spec.rb +46 -0
  102. data/spec/unit/locked_switcher_spec.rb +46 -0
  103. data/spec/unit/migrator_spec.rb +144 -0
  104. data/spec/unit/printer_spec.rb +85 -0
  105. data/spec/unit/sql_helper_spec.rb +28 -0
  106. data/spec/unit/table_name_spec.rb +39 -0
  107. data/spec/unit/table_spec.rb +47 -0
  108. data/spec/unit/throttler/slave_lag_spec.rb +322 -0
  109. data/spec/unit/throttler/threads_running_spec.rb +64 -0
  110. data/spec/unit/throttler_spec.rb +124 -0
  111. data/spec/unit/unit_helper.rb +26 -0
  112. metadata +366 -0
@@ -0,0 +1,659 @@
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
+ # Schmidt
3
+
4
+ require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
5
+ require 'integration/toxiproxy_helper'
6
+
7
+ describe Lhm do
8
+ include IntegrationHelper
9
+
10
+ before(:each) { connect_master!; Lhm.cleanup(true) }
11
+
12
+ describe 'id column requirement' do
13
+ it 'should migrate the table when id is pk' do
14
+ table_create(:users)
15
+
16
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
17
+ t.add_column(:logins, "int(12) default '0'")
18
+ end
19
+
20
+ slave do
21
+ value(table_read(:users).columns['logins']).must_equal({
22
+ :type => 'int(12)',
23
+ :is_nullable => 'YES',
24
+ :column_default => '0',
25
+ :comment => '',
26
+ :collate => nil,
27
+ })
28
+ end
29
+ end
30
+
31
+ it 'should migrate the table when id is not pk' do
32
+ table_create(:custom_primary_key)
33
+
34
+ Lhm.change_table(:custom_primary_key, :atomic_switch => false) do |t|
35
+ t.add_column(:logins, "int(12) default '0'")
36
+ end
37
+
38
+ slave do
39
+ value(table_read(:custom_primary_key).columns['logins']).must_equal({
40
+ :type => 'int(12)',
41
+ :is_nullable => 'YES',
42
+ :column_default => '0',
43
+ :comment => '',
44
+ :collate => nil,
45
+ })
46
+ end
47
+ end
48
+
49
+ it 'should migrate the table when using a composite primary key if id column exists' do
50
+ table_create(:composite_primary_key)
51
+
52
+ Lhm.change_table(:composite_primary_key, :atomic_switch => false) do |t|
53
+ t.add_column(:logins, "int(12) default '0'")
54
+ end
55
+
56
+ slave do
57
+ value(table_read(:composite_primary_key).columns['logins']).must_equal({
58
+ :type => 'int(12)',
59
+ :is_nullable => 'YES',
60
+ :column_default => '0',
61
+ :comment => '',
62
+ :collate => nil,
63
+ })
64
+ end
65
+ end
66
+ end
67
+
68
+ describe 'changes' do
69
+ before(:each) do
70
+ table_create(:users)
71
+ table_create(:tracks)
72
+ table_create(:permissions)
73
+ end
74
+
75
+ describe 'when changing to a composite primary key' do
76
+ it 'should be able to use ddl statement to create composite keys' do
77
+
78
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
79
+ t.ddl("ALTER TABLE #{t.name} CHANGE id id bigint (20) NOT NULL")
80
+ t.ddl("ALTER TABLE #{t.name} DROP PRIMARY KEY, ADD PRIMARY KEY (username, id)")
81
+ t.ddl("ALTER TABLE #{t.name} ADD INDEX (id)")
82
+ t.ddl("ALTER TABLE #{t.name} CHANGE id id bigint (20) NOT NULL AUTO_INCREMENT")
83
+ end
84
+
85
+ slave do
86
+ value(connection.primary_key('users')).must_equal(['username', 'id'])
87
+ end
88
+ end
89
+
90
+ end
91
+
92
+ describe 'when providing a subset of data to copy' do
93
+
94
+ before do
95
+ execute('insert into tracks set id = 13, public = 0')
96
+ 11.times { |n| execute("insert into tracks set id = #{n + 1}, public = 1") }
97
+ 11.times { |n| execute("insert into permissions set track_id = #{n + 1}") }
98
+
99
+ Lhm.change_table(:permissions, :atomic_switch => false) do |t|
100
+ t.filter('inner join tracks on tracks.`id` = permissions.`track_id` and tracks.`public` = 1')
101
+ end
102
+ end
103
+
104
+ describe 'when no additional data is inserted into the table' do
105
+
106
+ it 'migrates the existing data' do
107
+ slave do
108
+ value(count_all(:permissions)).must_equal(11)
109
+ end
110
+ end
111
+ end
112
+
113
+ describe 'when additional data is inserted' do
114
+
115
+ before do
116
+ execute('insert into tracks set id = 14, public = 0')
117
+ execute('insert into tracks set id = 15, public = 1')
118
+ execute('insert into permissions set track_id = 14')
119
+ execute('insert into permissions set track_id = 15')
120
+ end
121
+
122
+ it 'migrates all data' do
123
+ slave do
124
+ value(count_all(:permissions)).must_equal(13)
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ it 'should add a column' do
131
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
132
+ t.add_column(:logins, "INT(12) DEFAULT '0'")
133
+ end
134
+
135
+ slave do
136
+ value(table_read(:users).columns['logins']).must_equal({
137
+ :type => 'int(12)',
138
+ :is_nullable => 'YES',
139
+ :column_default => '0',
140
+ :comment => '',
141
+ :collate => nil,
142
+ })
143
+ end
144
+ end
145
+
146
+ it 'should copy all rows' do
147
+ 23.times { |n| execute("insert into users set reference = '#{ n }'") }
148
+
149
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
150
+ t.add_column(:logins, "INT(12) DEFAULT '0'")
151
+ end
152
+
153
+ slave do
154
+ value(count_all(:users)).must_equal(23)
155
+ end
156
+ end
157
+
158
+ it 'should remove a column' do
159
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
160
+ t.remove_column(:comment)
161
+ end
162
+
163
+ slave do
164
+ assert_nil table_read(:users).columns['comment']
165
+ end
166
+ end
167
+
168
+ it 'should add an index' do
169
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
170
+ t.add_index([:comment, :created_at])
171
+ end
172
+
173
+ slave do
174
+ value(index_on_columns?(:users, [:comment, :created_at])).must_equal(true)
175
+ end
176
+ end
177
+
178
+ it 'should add an index with a custom name' do
179
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
180
+ t.add_index([:comment, :created_at], :my_index_name)
181
+ end
182
+
183
+ slave do
184
+ value(index?(:users, :my_index_name)).must_equal(true)
185
+ end
186
+ end
187
+
188
+ it 'should add an index on a column with a reserved name' do
189
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
190
+ t.add_index(:group)
191
+ end
192
+
193
+ slave do
194
+ value(index_on_columns?(:users, :group)).must_equal(true)
195
+ end
196
+ end
197
+
198
+ it 'should add a unique index' do
199
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
200
+ t.add_unique_index(:comment)
201
+ end
202
+
203
+ slave do
204
+ value(index_on_columns?(:users, :comment, :unique)).must_equal(true)
205
+ end
206
+ end
207
+
208
+ it 'should remove an index' do
209
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
210
+ t.remove_index([:username, :created_at])
211
+ end
212
+
213
+ slave do
214
+ value(index_on_columns?(:users, [:username, :created_at])).must_equal(false)
215
+ end
216
+ end
217
+
218
+ it 'should remove an index with a custom name' do
219
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
220
+ t.remove_index([:username, :group])
221
+ end
222
+
223
+ slave do
224
+ value(index?(:users, :index_with_a_custom_name)).must_equal(false)
225
+ end
226
+ end
227
+
228
+ it 'should remove an index with a custom name by name' do
229
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
230
+ t.remove_index(:irrelevant_column_name, :index_with_a_custom_name)
231
+ end
232
+
233
+ slave do
234
+ value(index?(:users, :index_with_a_custom_name)).must_equal(false)
235
+ end
236
+ end
237
+
238
+ it 'should apply a ddl statement' do
239
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
240
+ t.ddl('alter table %s add column flag tinyint(1)' % t.name)
241
+ end
242
+
243
+ slave do
244
+ value(table_read(:users).columns['flag']).must_equal({
245
+ :type => 'tinyint(1)',
246
+ :is_nullable => 'YES',
247
+ :column_default => nil,
248
+ :comment => '',
249
+ :collate => nil,
250
+ })
251
+ end
252
+ end
253
+
254
+ it 'should change a column' do
255
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
256
+ t.change_column(:comment, "varchar(20) DEFAULT 'none' NOT NULL")
257
+ end
258
+
259
+ slave do
260
+ value(table_read(:users).columns['comment']).must_equal({
261
+ :type => 'varchar(20)',
262
+ :is_nullable => 'NO',
263
+ :column_default => 'none',
264
+ :comment => '',
265
+ :collate => 'utf8_general_ci',
266
+ })
267
+ end
268
+ end
269
+
270
+ it 'should change the last column in a table' do
271
+ table_create(:small_table)
272
+
273
+ Lhm.change_table(:small_table, :atomic_switch => false) do |t|
274
+ t.change_column(:id, 'int(5)')
275
+ end
276
+
277
+ slave do
278
+ value(table_read(:small_table).columns['id']).must_equal({
279
+ :type => 'int(5)',
280
+ :is_nullable => 'NO',
281
+ :column_default => nil,
282
+ :comment => '',
283
+ :collate => nil,
284
+ })
285
+ end
286
+ end
287
+
288
+ it 'should rename a column' do
289
+ table_create(:users)
290
+
291
+ execute("INSERT INTO users (username) VALUES ('a user')")
292
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
293
+ t.rename_column(:username, :login)
294
+ end
295
+
296
+ slave do
297
+ table_data = table_read(:users)
298
+ assert_nil table_data.columns['username']
299
+ value(table_read(:users).columns['login']).must_equal({
300
+ :type => 'varchar(255)',
301
+ :is_nullable => 'YES',
302
+ :column_default => nil,
303
+ :comment => '',
304
+ :collate => 'utf8_general_ci',
305
+ })
306
+
307
+ result = select_one('SELECT login from users')
308
+ result = result['login'] if result.respond_to?(:has_key?)
309
+ value(result).must_equal('a user')
310
+ end
311
+ end
312
+
313
+ it 'should rename a column with a default' do
314
+ table_create(:users)
315
+
316
+ execute("INSERT INTO users (username) VALUES ('a user')")
317
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
318
+ t.rename_column(:group, :fnord)
319
+ end
320
+
321
+ slave do
322
+ table_data = table_read(:users)
323
+ assert_nil table_data.columns['group']
324
+ value(table_read(:users).columns['fnord']).must_equal({
325
+ :type => 'varchar(255)',
326
+ :is_nullable => 'YES',
327
+ :column_default => 'Superfriends',
328
+ :comment => '',
329
+ :collate => 'utf8_general_ci',
330
+ })
331
+
332
+ result = select_one('SELECT `fnord` from users')
333
+ result = result['fnord'] if result.respond_to?(:has_key?)
334
+ value(result).must_equal('Superfriends')
335
+ end
336
+ end
337
+
338
+ it 'should rename a column with a collate' do
339
+ table_create(:users)
340
+
341
+ execute("ALTER TABLE users MODIFY `username` varchar(255) COLLATE utf8mb4_unicode_ci NULL")
342
+ execute("INSERT INTO users (username) VALUES ('a user')")
343
+
344
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
345
+ t.rename_column(:username, :user_name)
346
+ end
347
+
348
+ slave do
349
+ table_data = table_read(:users)
350
+ assert_nil table_data.columns['username']
351
+ value(table_read(:users).columns['user_name']).must_equal({
352
+ :type => 'varchar(255)',
353
+ :is_nullable => 'YES',
354
+ :column_default => nil,
355
+ :comment => '',
356
+ :collate => 'utf8mb4_unicode_ci',
357
+ })
358
+
359
+ result = select_one('SELECT `user_name` from users')
360
+ result = result['user_name'] if result.respond_to?(:has_key?)
361
+ value(result).must_equal('a user')
362
+ end
363
+ end
364
+
365
+
366
+ it 'should rename a column with a comment' do
367
+ table_create(:users)
368
+
369
+ execute("ALTER TABLE users MODIFY `reference` int(11) DEFAULT NULL COMMENT 'RefComment'")
370
+ execute("INSERT INTO users (username,reference) VALUES ('a user', 10)")
371
+
372
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
373
+ t.rename_column(:reference, :ref)
374
+ end
375
+
376
+ slave do
377
+ table_data = table_read(:users)
378
+ assert_nil table_data.columns['reference']
379
+ value(table_read(:users).columns['ref']).must_equal({
380
+ :type => 'int(11)',
381
+ :is_nullable => 'YES',
382
+ :column_default => nil,
383
+ :comment => 'RefComment',
384
+ :collate => nil,
385
+ })
386
+
387
+ result = select_one('SELECT `ref` from users')
388
+ result = result['ref'] if result.respond_to?(:has_key?)
389
+ value(result).must_equal(10)
390
+ end
391
+ end
392
+
393
+ it 'should rename a column with a default null' do
394
+ table_create(:users)
395
+
396
+ execute("ALTER TABLE users MODIFY `group` varchar(255) NULL DEFAULT NULL")
397
+ execute("INSERT INTO users (username) VALUES ('a user')")
398
+
399
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
400
+ t.rename_column(:group, :fnord)
401
+ end
402
+
403
+ slave do
404
+ table_data = table_read(:users)
405
+ assert_nil table_data.columns['group']
406
+ value(table_read(:users).columns['fnord']).must_equal({
407
+ :type => 'varchar(255)',
408
+ :is_nullable => 'YES',
409
+ :column_default => nil,
410
+ :comment => '',
411
+ :collate => 'utf8_general_ci',
412
+ })
413
+
414
+ result = select_one('SELECT `fnord` from users')
415
+ result = result['fnord'] if result.respond_to?(:has_key?)
416
+ assert_nil(result)
417
+ end
418
+ end
419
+
420
+ it 'should rename a colmn with nullable' do
421
+ table_create(:users)
422
+ execute("INSERT INTO users (username) VALUES ('a user')")
423
+
424
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
425
+ t.rename_column(:username, :user_name)
426
+ end
427
+
428
+ slave do
429
+ table_data = table_read(:users)
430
+ assert_nil table_data.columns['username']
431
+ value(table_read(:users).columns['user_name']).must_equal({
432
+ :type => 'varchar(255)',
433
+ :is_nullable => 'YES',
434
+ :column_default => nil,
435
+ :comment => '',
436
+ :collate => 'utf8_general_ci',
437
+ })
438
+
439
+ result = select_one('SELECT `user_name` from users')
440
+ result = result['user_name'] if result.respond_to?(:has_key?)
441
+ value(result).must_equal('a user')
442
+ end
443
+ end
444
+
445
+ it 'should rename a column with a not null' do
446
+ table_create(:users)
447
+
448
+ execute("ALTER TABLE users MODIFY username varchar(255) NOT NULL")
449
+ execute("INSERT INTO users (username) VALUES ('a user')")
450
+
451
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
452
+ t.rename_column(:username, :user_name)
453
+ end
454
+
455
+ slave do
456
+ table_data = table_read(:users)
457
+ assert_nil table_data.columns['username']
458
+ value(table_read(:users).columns['user_name']).must_equal({
459
+ :type => 'varchar(255)',
460
+ :is_nullable => 'NO',
461
+ :column_default => nil,
462
+ :comment => '',
463
+ :collate => 'utf8_general_ci',
464
+ })
465
+
466
+ result = select_one('SELECT `user_name` from users')
467
+ result = result['user_name'] if result.respond_to?(:has_key?)
468
+ value(result).must_equal('a user')
469
+ end
470
+ end
471
+
472
+ it 'should raise an exception if the triggers do not exist after copying all rows' do
473
+ table_create(:users)
474
+
475
+ execute("INSERT INTO users (username) VALUES ('a user')")
476
+
477
+ Lhm::Invoker.any_instance.stubs(:triggers_still_exist?).returns(false)
478
+
479
+ exception = assert_raises do
480
+ Lhm.change_table(:users) do |t|
481
+ t.rename_column(:group, :fnord)
482
+ end
483
+ end
484
+
485
+ assert_match "Verification failed, aborting early", exception.message
486
+ end
487
+
488
+ it 'should not perform the table rename if the triggers do not exist after copying all rows' do
489
+ table_create(:users)
490
+
491
+ execute("INSERT INTO users (username) VALUES ('a user')")
492
+
493
+ Lhm::Invoker.any_instance.stubs(:triggers_still_exist?).returns(false)
494
+ Lhm::LockedSwitcher.any_instance.expects(:run).never
495
+
496
+ assert_raises do
497
+ Lhm.change_table(:users) do |t|
498
+ t.rename_column(:group, :fnord)
499
+ end
500
+ end
501
+
502
+ slave do
503
+ table_data = table_read(:users)
504
+ assert_nil table_data.columns['fnord']
505
+ value(table_read(:users).columns['group']).must_equal({
506
+ :type => 'varchar(255)',
507
+ :is_nullable => 'YES',
508
+ :column_default => 'Superfriends',
509
+ :comment => '',
510
+ :collate => 'utf8_general_ci',
511
+ })
512
+ end
513
+ end
514
+
515
+ it 'works when mysql reserved words are used' do
516
+ table_create(:lines)
517
+ execute("insert into `lines` set id = 1, `between` = 'foo'")
518
+ execute("insert into `lines` set id = 2, `between` = 'bar'")
519
+
520
+ Lhm.change_table(:lines) do |t|
521
+ t.add_column('by', 'varchar(10)')
522
+ t.remove_column('lines')
523
+ t.add_index('by')
524
+ t.add_unique_index('between')
525
+ t.remove_index('by')
526
+ end
527
+
528
+ slave do
529
+ value(table_read(:lines).columns).must_include 'by'
530
+ value(table_read(:lines).columns).wont_include 'lines'
531
+ value(index_on_columns?(:lines, ['between'], :unique)).must_equal true
532
+ value(index_on_columns?(:lines, ['by'])).must_equal false
533
+ value(count_all(:lines)).must_equal(2)
534
+ end
535
+ end
536
+
537
+ describe 'parallel' do
538
+ it 'should perserve inserts during migration' do
539
+ 50.times { |n| execute("insert into users set reference = '#{ n }'") }
540
+
541
+ insert = Thread.new do
542
+ 10.times do |n|
543
+ connect_master!
544
+ execute("insert into users set reference = '#{ 100 + n }'")
545
+ sleep(0.17)
546
+ end
547
+ end
548
+ sleep 2
549
+
550
+ options = { :stride => 10, :throttle => 97, :atomic_switch => false }
551
+ Lhm.change_table(:users, options) do |t|
552
+ t.add_column(:parallel, "INT(10) DEFAULT '0'")
553
+ end
554
+
555
+ insert.join
556
+
557
+ slave do
558
+ value(count_all(:users)).must_equal(60)
559
+ end
560
+ end
561
+
562
+ it 'should perserve deletes during migration' do
563
+ 50.times { |n| execute("insert into users set reference = '#{ n }'") }
564
+
565
+ delete = Thread.new do
566
+ 10.times do |n|
567
+ execute("delete from users where reference = '#{ n }'")
568
+ sleep(0.17)
569
+ end
570
+ end
571
+ sleep 2
572
+
573
+ options = { :stride => 10, :throttle => 97, :atomic_switch => false }
574
+ Lhm.change_table(:users, options) do |t|
575
+ t.add_column(:parallel, "INT(10) DEFAULT '0'")
576
+ end
577
+
578
+ delete.join
579
+
580
+ slave do
581
+ value(count_all(:users)).must_equal(40)
582
+ end
583
+ end
584
+ end
585
+
586
+ describe 'connection' do
587
+ include ToxiproxyHelper
588
+
589
+ before(:each) do
590
+ @logs = StringIO.new
591
+ Lhm.logger = Logger.new(@logs)
592
+ end
593
+
594
+ it " should not try to reconnect if reconnect_with_consistent_host is not provided" do
595
+ connect_master_with_toxiproxy!
596
+
597
+ table_create(:users)
598
+ 100.times { |n| execute("insert into users set reference = '#{ n }'") }
599
+
600
+ assert_raises ActiveRecord::StatementInvalid do
601
+ Toxiproxy[:mysql_master].down do
602
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
603
+ t.ddl("ALTER TABLE #{t.name} CHANGE id id bigint (20) NOT NULL")
604
+ t.ddl("ALTER TABLE #{t.name} DROP PRIMARY KEY, ADD PRIMARY KEY (username, id)")
605
+ t.ddl("ALTER TABLE #{t.name} ADD INDEX (id)")
606
+ t.ddl("ALTER TABLE #{t.name} CHANGE id id bigint (20) NOT NULL AUTO_INCREMENT")
607
+ end
608
+ end
609
+ end
610
+ end
611
+
612
+ it "should reconnect if reconnect_with_consistent_host is true" do
613
+ connect_master_with_toxiproxy!
614
+ mysql_disabled = false
615
+
616
+ table_create(:users)
617
+ 100.times { |n| execute("insert into users set reference = '#{ n }'") }
618
+
619
+ # Redeclare Lhm::ChunkInsert to use Hook to disable MySQL writer for 3 seconds before first insert
620
+ Lhm::ChunkInsert.class_eval do
621
+ extend AfterDo
622
+
623
+ before(:insert_and_return_count_of_rows_created) do
624
+ unless mysql_disabled
625
+ mysql_disabled = true
626
+ Thread.new do
627
+ Toxiproxy[:mysql_master].down do
628
+ sleep 3
629
+ end
630
+ end
631
+ end
632
+ end
633
+
634
+ # Need to call `#method_added` manually to have the hooks take into effect
635
+ method_added(:insert_and_return_count_of_rows_created)
636
+ end
637
+
638
+ Lhm.change_table(:users, atomic_switch: false, reconnect_with_consistent_host: true) do |t|
639
+ t.ddl("ALTER TABLE #{t.name} CHANGE id id bigint (20) NOT NULL")
640
+ t.ddl("ALTER TABLE #{t.name} DROP PRIMARY KEY, ADD PRIMARY KEY (username, id)")
641
+ t.ddl("ALTER TABLE #{t.name} ADD INDEX (id)")
642
+ t.ddl("ALTER TABLE #{t.name} CHANGE id id bigint (20) NOT NULL AUTO_INCREMENT")
643
+ end
644
+
645
+ log_lines = @logs.string.split("\n")
646
+
647
+ assert log_lines.one?{ |line| line.include?("Lost connection to MySQL, will retry to connect to same host")}
648
+ assert log_lines.one?{ |line| line.include?("LHM successfully reconnected to initial host")}
649
+ assert log_lines.one?{ |line| line.include?("100% complete")}
650
+
651
+ Lhm::ChunkInsert.remove_all_callbacks
652
+
653
+ slave do
654
+ value(count_all(:users)).must_equal(100)
655
+ end
656
+ end
657
+ end
658
+ end
659
+ end
@@ -0,0 +1,30 @@
1
+ require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
2
+
3
+ describe Lhm do
4
+ include IntegrationHelper
5
+
6
+ before(:each) do
7
+ connect_master!
8
+ table_create(:users)
9
+ end
10
+
11
+ it 'set_session_lock_wait_timeouts should set the sessions lock wait timeouts to less than the global values by a delta' do
12
+ connection = Lhm.send(:connection)
13
+ connection.execute('SET GLOBAL innodb_lock_wait_timeout=11')
14
+ connection.execute('SET GLOBAL lock_wait_timeout=11')
15
+ connection.execute('SET SESSION innodb_lock_wait_timeout=1')
16
+ connection.execute('SET SESSION lock_wait_timeout=1')
17
+
18
+ global_innodb_lock_wait_timeout = connection.select_one("SHOW GLOBAL VARIABLES LIKE 'innodb_lock_wait_timeout'")['Value'].to_i
19
+ global_lock_wait_timeout = connection.select_one("SHOW GLOBAL VARIABLES LIKE 'lock_wait_timeout'")['Value'].to_i
20
+
21
+ invoker = Lhm::Invoker.new(Lhm::Table.parse(:users, connection), connection)
22
+ invoker.set_session_lock_wait_timeouts
23
+
24
+ session_innodb_lock_wait_timeout = connection.select_one("SHOW SESSION VARIABLES LIKE 'innodb_lock_wait_timeout'")['Value'].to_i
25
+ session_lock_wait_timeout = connection.select_one("SHOW SESSION VARIABLES LIKE 'lock_wait_timeout'")['Value'].to_i
26
+
27
+ value(session_lock_wait_timeout).must_equal global_lock_wait_timeout + Lhm::Invoker::LOCK_WAIT_TIMEOUT_DELTA
28
+ value(session_innodb_lock_wait_timeout).must_equal global_innodb_lock_wait_timeout + Lhm::Invoker::LOCK_WAIT_TIMEOUT_DELTA
29
+ end
30
+ end