lhm-shopify 3.3.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +34 -0
  3. data/.gitignore +17 -0
  4. data/.rubocop.yml +183 -0
  5. data/.travis.yml +21 -0
  6. data/CHANGELOG.md +216 -0
  7. data/Gemfile +5 -0
  8. data/LICENSE +27 -0
  9. data/README.md +284 -0
  10. data/Rakefile +22 -0
  11. data/bin/.gitkeep +0 -0
  12. data/dbdeployer/config.json +32 -0
  13. data/dbdeployer/install.sh +64 -0
  14. data/dev.yml +20 -0
  15. data/gemfiles/ar-2.3_mysql.gemfile +6 -0
  16. data/gemfiles/ar-3.2_mysql.gemfile +5 -0
  17. data/gemfiles/ar-3.2_mysql2.gemfile +5 -0
  18. data/gemfiles/ar-4.0_mysql2.gemfile +5 -0
  19. data/gemfiles/ar-4.1_mysql2.gemfile +5 -0
  20. data/gemfiles/ar-4.2_mysql2.gemfile +5 -0
  21. data/gemfiles/ar-5.0_mysql2.gemfile +5 -0
  22. data/lhm.gemspec +34 -0
  23. data/lib/lhm.rb +131 -0
  24. data/lib/lhm/atomic_switcher.rb +52 -0
  25. data/lib/lhm/chunk_finder.rb +32 -0
  26. data/lib/lhm/chunk_insert.rb +51 -0
  27. data/lib/lhm/chunker.rb +87 -0
  28. data/lib/lhm/cleanup/current.rb +74 -0
  29. data/lib/lhm/command.rb +48 -0
  30. data/lib/lhm/entangler.rb +117 -0
  31. data/lib/lhm/intersection.rb +51 -0
  32. data/lib/lhm/invoker.rb +98 -0
  33. data/lib/lhm/locked_switcher.rb +74 -0
  34. data/lib/lhm/migration.rb +43 -0
  35. data/lib/lhm/migrator.rb +237 -0
  36. data/lib/lhm/printer.rb +59 -0
  37. data/lib/lhm/railtie.rb +9 -0
  38. data/lib/lhm/sql_helper.rb +77 -0
  39. data/lib/lhm/sql_retry.rb +61 -0
  40. data/lib/lhm/table.rb +121 -0
  41. data/lib/lhm/table_name.rb +23 -0
  42. data/lib/lhm/test_support.rb +35 -0
  43. data/lib/lhm/throttler.rb +36 -0
  44. data/lib/lhm/throttler/slave_lag.rb +145 -0
  45. data/lib/lhm/throttler/threads_running.rb +53 -0
  46. data/lib/lhm/throttler/time.rb +29 -0
  47. data/lib/lhm/timestamp.rb +11 -0
  48. data/lib/lhm/version.rb +6 -0
  49. data/shipit.rubygems.yml +0 -0
  50. data/spec/.lhm.example +4 -0
  51. data/spec/README.md +58 -0
  52. data/spec/fixtures/bigint_table.ddl +4 -0
  53. data/spec/fixtures/composite_primary_key.ddl +7 -0
  54. data/spec/fixtures/custom_primary_key.ddl +6 -0
  55. data/spec/fixtures/destination.ddl +6 -0
  56. data/spec/fixtures/lines.ddl +7 -0
  57. data/spec/fixtures/origin.ddl +6 -0
  58. data/spec/fixtures/permissions.ddl +5 -0
  59. data/spec/fixtures/small_table.ddl +4 -0
  60. data/spec/fixtures/tracks.ddl +5 -0
  61. data/spec/fixtures/users.ddl +14 -0
  62. data/spec/fixtures/wo_id_int_column.ddl +6 -0
  63. data/spec/integration/atomic_switcher_spec.rb +93 -0
  64. data/spec/integration/chunk_insert_spec.rb +29 -0
  65. data/spec/integration/chunker_spec.rb +185 -0
  66. data/spec/integration/cleanup_spec.rb +136 -0
  67. data/spec/integration/entangler_spec.rb +66 -0
  68. data/spec/integration/integration_helper.rb +237 -0
  69. data/spec/integration/invoker_spec.rb +33 -0
  70. data/spec/integration/lhm_spec.rb +585 -0
  71. data/spec/integration/lock_wait_timeout_spec.rb +30 -0
  72. data/spec/integration/locked_switcher_spec.rb +50 -0
  73. data/spec/integration/sql_retry/lock_wait_spec.rb +125 -0
  74. data/spec/integration/sql_retry/lock_wait_timeout_test_helper.rb +101 -0
  75. data/spec/integration/table_spec.rb +91 -0
  76. data/spec/test_helper.rb +32 -0
  77. data/spec/unit/atomic_switcher_spec.rb +31 -0
  78. data/spec/unit/chunk_finder_spec.rb +73 -0
  79. data/spec/unit/chunk_insert_spec.rb +44 -0
  80. data/spec/unit/chunker_spec.rb +166 -0
  81. data/spec/unit/entangler_spec.rb +124 -0
  82. data/spec/unit/intersection_spec.rb +51 -0
  83. data/spec/unit/lhm_spec.rb +29 -0
  84. data/spec/unit/locked_switcher_spec.rb +51 -0
  85. data/spec/unit/migrator_spec.rb +146 -0
  86. data/spec/unit/printer_spec.rb +97 -0
  87. data/spec/unit/sql_helper_spec.rb +32 -0
  88. data/spec/unit/table_name_spec.rb +39 -0
  89. data/spec/unit/table_spec.rb +47 -0
  90. data/spec/unit/throttler/slave_lag_spec.rb +317 -0
  91. data/spec/unit/throttler/threads_running_spec.rb +64 -0
  92. data/spec/unit/throttler_spec.rb +124 -0
  93. data/spec/unit/unit_helper.rb +13 -0
  94. metadata +239 -0
@@ -0,0 +1,136 @@
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
+
6
+ describe Lhm, 'cleanup' do
7
+ include IntegrationHelper
8
+ before(:each) { connect_master! }
9
+
10
+ describe 'changes' do
11
+ before(:each) do
12
+ table_create(:users)
13
+ table_create(:permissions)
14
+ simulate_failed_migration do
15
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
16
+ t.add_column(:logins, "INT(12) DEFAULT '0'")
17
+ t.add_index(:logins)
18
+ end
19
+ end
20
+ simulate_failed_migration do
21
+ Lhm.change_table(:permissions, :atomic_switch => false) do |t|
22
+ t.add_column(:user_id, "INT(12) DEFAULT '0'")
23
+ t.add_index(:user_id)
24
+ end
25
+ end
26
+ end
27
+
28
+ after(:each) do
29
+ Lhm.cleanup(true)
30
+ end
31
+
32
+ describe 'cleanup' do
33
+ it 'should show temporary tables' do
34
+ output = capture_stdout do
35
+ Lhm.cleanup
36
+ end
37
+ output.must_include('Would drop LHM backup tables')
38
+ output.must_match(/lhma_[0-9_]*_users/)
39
+ output.must_match(/lhma_[0-9_]*_permissions/)
40
+ end
41
+
42
+ it 'should show temporary tables within range' do
43
+ table = OpenStruct.new(:name => 'users')
44
+ table_name = Lhm::Migration.new(table, nil, nil, {}, Time.now - 172800).archive_name
45
+ table_rename(:users, table_name)
46
+
47
+ table2 = OpenStruct.new(:name => 'permissions')
48
+ table_name2 = Lhm::Migration.new(table2, nil, nil, {}, Time.now - 172800).archive_name
49
+ table_rename(:permissions, table_name2)
50
+
51
+ output = capture_stdout do
52
+ Lhm.cleanup false, { :until => Time.now - 86400 }
53
+ end
54
+ output.must_include('Would drop LHM backup tables')
55
+ output.must_match(/lhma_[0-9_]*_users/)
56
+ output.must_match(/lhma_[0-9_]*_permissions/)
57
+ end
58
+
59
+ it 'should exclude temporary tables outside range' do
60
+ table = OpenStruct.new(:name => 'users')
61
+ table_name = Lhm::Migration.new(table, nil, nil, {}, Time.now).archive_name
62
+ table_rename(:users, table_name)
63
+
64
+ table2 = OpenStruct.new(:name => 'permissions')
65
+ table_name2 = Lhm::Migration.new(table2, nil, nil, {}, Time.now).archive_name
66
+ table_rename(:permissions, table_name2)
67
+
68
+ output = capture_stdout do
69
+ Lhm.cleanup false, { :until => Time.now - 172800 }
70
+ end
71
+ output.must_include('Would drop LHM backup tables')
72
+ output.wont_match(/lhma_[0-9_]*_users/)
73
+ output.wont_match(/lhma_[0-9_]*_permissions/)
74
+ end
75
+
76
+ it 'should show temporary triggers' do
77
+ output = capture_stdout do
78
+ Lhm.cleanup
79
+ end
80
+ output.must_include('Would drop LHM triggers')
81
+ output.must_include('lhmt_ins_users')
82
+ output.must_include('lhmt_del_users')
83
+ output.must_include('lhmt_upd_users')
84
+ output.must_include('lhmt_ins_permissions')
85
+ output.must_include('lhmt_del_permissions')
86
+ output.must_include('lhmt_upd_permissions')
87
+ end
88
+
89
+ it 'should delete temporary tables' do
90
+ Lhm.cleanup(true).must_equal(true)
91
+ Lhm.cleanup.must_equal(true)
92
+ end
93
+ end
94
+
95
+ describe 'cleanup_current_run' do
96
+ it 'should show lhmn table for the specified table only' do
97
+ table_create(:permissions)
98
+ table_rename(:permissions, 'lhmn_permissions')
99
+ output = capture_stdout do
100
+ Lhm.cleanup_current_run(false, 'permissions')
101
+ end.split("\n")
102
+
103
+ assert_equal "The following DDLs would be executed:", output[0]
104
+ assert_equal "drop trigger if exists lhmt_ins_permissions", output[1]
105
+ assert_equal "drop trigger if exists lhmt_upd_permissions", output[2]
106
+ assert_equal "drop trigger if exists lhmt_del_permissions", output[3]
107
+ assert_match(/rename table lhmn_permissions to lhma_[0-9_]*_permissions_failed/, output[4])
108
+ assert_equal 5, output.length
109
+ end
110
+
111
+ it 'should show temporary triggers for the specified table only' do
112
+ output = capture_stdout do
113
+ Lhm.cleanup_current_run(false, 'permissions')
114
+ end.split("\n")
115
+ assert_equal "The following DDLs would be executed:", output[0]
116
+ assert_equal "drop trigger if exists lhmt_ins_permissions", output[1]
117
+ assert_equal "drop trigger if exists lhmt_upd_permissions", output[2]
118
+ assert_equal "drop trigger if exists lhmt_del_permissions", output[3]
119
+ assert_equal 4, output.length
120
+ end
121
+
122
+ it 'should delete temporary tables and triggers for the specified table only' do
123
+ assert Lhm.cleanup_current_run(true, 'permissions')
124
+
125
+ all_tables = Lhm.connection.select_values('show tables')
126
+ all_triggers = Lhm.connection.select_values('show triggers')
127
+
128
+ refute all_tables.include?('lhmn_permissions')
129
+ assert all_tables.find { |t| t =~ /lhma_(.*)_users/}
130
+
131
+ refute all_triggers.find { |t| t =~ /lhmt_(.*)_permissions/}
132
+ assert all_triggers.find { |t| t =~ /lhmt_(.*)_users/}
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,66 @@
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
+
6
+ require 'lhm/table'
7
+ require 'lhm/migration'
8
+ require 'lhm/entangler'
9
+
10
+ describe Lhm::Entangler do
11
+ include IntegrationHelper
12
+
13
+ before(:each) { connect_master! }
14
+
15
+ describe 'entanglement' do
16
+ before(:each) do
17
+ @origin = table_create('origin')
18
+ @destination = table_create('destination')
19
+ @migration = Lhm::Migration.new(@origin, @destination)
20
+ @entangler = Lhm::Entangler.new(@migration, connection)
21
+ end
22
+
23
+ it 'should replay inserts from origin into destination' do
24
+ @entangler.run do
25
+ execute("insert into origin (common) values ('inserted')")
26
+ end
27
+
28
+ slave do
29
+ count(:destination, 'common', 'inserted').must_equal(1)
30
+ end
31
+ end
32
+
33
+ it 'should replay deletes from origin into destination' do
34
+ execute("insert into origin (common) values ('inserted')")
35
+
36
+ @entangler.run do
37
+ execute("delete from origin where common = 'inserted'")
38
+ end
39
+
40
+ slave do
41
+ count(:destination, 'common', 'inserted').must_equal(0)
42
+ end
43
+ end
44
+
45
+ it 'should replay updates from origin into destination' do
46
+ @entangler.run do
47
+ execute("insert into origin (common) values ('inserted')")
48
+ execute("update origin set common = 'updated'")
49
+ end
50
+
51
+ slave do
52
+ count(:destination, 'common', 'updated').must_equal(1)
53
+ end
54
+ end
55
+
56
+ it 'should remove entanglement' do
57
+ @entangler.run {}
58
+
59
+ execute("insert into origin (common) values ('inserted')")
60
+
61
+ slave do
62
+ count(:destination, 'common', 'inserted').must_equal(0)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,237 @@
1
+ # Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
+ # Schmidt
3
+ require 'test_helper'
4
+ require 'yaml'
5
+ require 'active_support'
6
+
7
+ begin
8
+ $db_config = YAML.load_file(File.expand_path(File.dirname(__FILE__)) + '/database.yml')
9
+ rescue StandardError => e
10
+ puts "Run install.sh to setup database"
11
+ raise e
12
+ end
13
+
14
+ $db_name = 'test'
15
+
16
+ require 'lhm/table'
17
+ require 'lhm/sql_helper'
18
+
19
+ module IntegrationHelper
20
+
21
+ def self.included(base)
22
+ base.after(:each) do
23
+ cleanup_connection = new_mysql_connection
24
+ results = cleanup_connection.query("SELECT table_name FROM information_schema.tables WHERE table_schema = '#{$db_name}';")
25
+ table_names_for_cleanup = results.map { |row| "#{$db_name}." + row.values.first }
26
+ cleanup_connection.query("DROP TABLE IF EXISTS #{table_names_for_cleanup.join(', ')};") if table_names_for_cleanup.length > 0
27
+ end
28
+ end
29
+
30
+ #
31
+ # Connectivity
32
+ #
33
+ def connection
34
+ @connection
35
+ end
36
+
37
+ def connect_master!
38
+ connect!(
39
+ '127.0.0.1',
40
+ $db_config['master']['port'],
41
+ $db_config['master']['user'],
42
+ $db_config['master']['password'],
43
+ $db_config['master']['socket']
44
+ )
45
+ end
46
+
47
+ def connect_slave!
48
+ connect!(
49
+ '127.0.0.1',
50
+ $db_config['slave']['port'],
51
+ $db_config['slave']['user'],
52
+ $db_config['slave']['password'],
53
+ $db_config['slave']['socket']
54
+ )
55
+ end
56
+
57
+ def connect!(hostname, port, user, password, socket)
58
+ adapter = ar_conn(hostname, port, user, password, socket)
59
+ Lhm.setup(adapter)
60
+ unless defined?(@@cleaned_up)
61
+ Lhm.cleanup(true)
62
+ @@cleaned_up = true
63
+ end
64
+ @connection = adapter
65
+ end
66
+
67
+ def ar_conn(host, port, user, password, socket)
68
+ ActiveRecord::Base.establish_connection(
69
+ :adapter => 'mysql2',
70
+ :host => host,
71
+ :username => user,
72
+ :port => port,
73
+ :password => password,
74
+ :socket => socket,
75
+ :database => $db_name
76
+ )
77
+ ActiveRecord::Base.connection
78
+ end
79
+
80
+ def select_one(*args)
81
+ @connection.select_one(*args)
82
+ end
83
+
84
+ def select_value(*args)
85
+ @connection.select_value(*args)
86
+ end
87
+
88
+ def execute(*args)
89
+ retries = 10
90
+ begin
91
+ @connection.execute(*args)
92
+ rescue => e
93
+ if (retries -= 1) > 0 && e.message =~ /Table '.*?' doesn't exist/
94
+ sleep 0.1
95
+ retry
96
+ else
97
+ raise
98
+ end
99
+ end
100
+ end
101
+
102
+ def slave(&block)
103
+ if master_slave_mode?
104
+ connect_slave!
105
+
106
+ # need to wait for the slave to catch up. a better method would be to
107
+ # check the master binlog position and wait for the slave to catch up
108
+ # to that position.
109
+ sleep 1
110
+ else
111
+ connect_master!
112
+ end
113
+
114
+ yield block
115
+
116
+ if master_slave_mode?
117
+ connect_master!
118
+ end
119
+ end
120
+
121
+ # Helps testing behaviour when another client locks the db
122
+ def start_locking_thread(lock_for, queue, locking_query)
123
+ Thread.new do
124
+ conn = Mysql2::Client.new(host: '127.0.0.1', database: $db_name, user: 'root', port: 3306)
125
+ conn.query('BEGIN')
126
+ conn.query(locking_query)
127
+ queue.push(true)
128
+ sleep(lock_for) # Sleep for log so LHM gives up
129
+ conn.query('ROLLBACK')
130
+ end
131
+ end
132
+
133
+ #
134
+ # Test Data
135
+ #
136
+
137
+ def fixture(name)
138
+ File.read($fixtures.join("#{ name }.ddl"))
139
+ end
140
+
141
+ def table_create(fixture_name)
142
+ execute "drop table if exists `#{ fixture_name }`"
143
+ execute fixture(fixture_name)
144
+ table_read(fixture_name)
145
+ end
146
+
147
+ def table_rename(from_name, to_name)
148
+ execute "rename table `#{ from_name }` to `#{ to_name }`"
149
+ end
150
+
151
+ def table_read(fixture_name)
152
+ Lhm::Table.parse(fixture_name, @connection)
153
+ end
154
+
155
+ def data_source_exists?(table)
156
+ connection.data_source_exists?(table.name)
157
+ end
158
+
159
+ def new_mysql_connection(role='master')
160
+ Mysql2::Client.new(
161
+ host: '127.0.0.1',
162
+ database: $db_name,
163
+ username: $db_config[role]['user'],
164
+ password: $db_config[role]['password'],
165
+ port: $db_config[role]['port'],
166
+ socket: $db_config[role]['socket']
167
+ )
168
+ end
169
+
170
+ #
171
+ # Database Helpers
172
+ #
173
+
174
+ def count(table, column, value)
175
+ query = "select count(*) from #{ table } where #{ column } = '#{ value }'"
176
+ select_value(query).to_i
177
+ end
178
+
179
+ def count_all(table)
180
+ query = "select count(*) from `#{ table }`"
181
+ select_value(query).to_i
182
+ end
183
+
184
+ def index_on_columns?(table_name, cols, type = :non_unique)
185
+ key_name = Lhm::SqlHelper.idx_name(table_name, cols)
186
+
187
+ index?(table_name, key_name, type)
188
+ end
189
+
190
+ def index?(table_name, key_name, type = :non_unique)
191
+ non_unique = type == :non_unique ? 1 : 0
192
+
193
+ !!select_one(%Q<
194
+ show indexes in `#{ table_name }`
195
+ where key_name = '#{ key_name }'
196
+ and non_unique = #{ non_unique }
197
+ >)
198
+ end
199
+
200
+ #
201
+ # Environment
202
+ #
203
+
204
+ def master_slave_mode?
205
+ !!ENV['MASTER_SLAVE']
206
+ end
207
+
208
+ #
209
+ # Misc
210
+ #
211
+
212
+ def capture_stdout
213
+ out = StringIO.new
214
+ $stdout = out
215
+ yield
216
+ return out.string
217
+ ensure
218
+ $stdout = ::STDOUT
219
+ end
220
+
221
+ def simulate_failed_migration
222
+ Lhm::Entangler.class_eval do
223
+ alias_method :old_after, :after
224
+ def after
225
+ true
226
+ end
227
+ end
228
+
229
+ yield
230
+ ensure
231
+ Lhm::Entangler.class_eval do
232
+ undef_method :after
233
+ alias_method :after, :old_after
234
+ undef_method :old_after
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,33 @@
1
+ require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
2
+
3
+ require 'lhm/invoker'
4
+
5
+ describe Lhm::Invoker do
6
+ include IntegrationHelper
7
+
8
+ before(:each) do
9
+ connect_master!
10
+ @origin = table_create('users')
11
+ @destination = table_create('destination')
12
+ @invoker = Lhm::Invoker.new(Lhm::Table.parse(:users, @connection), @connection)
13
+ @migration = Lhm::Migration.new(@origin, @destination)
14
+ @entangler = Lhm::Entangler.new(@migration, @connection)
15
+ @entangler.before
16
+ end
17
+
18
+ after(:each) do
19
+ @entangler.after if @invoker.triggers_still_exist?(@invoker.connection, @entangler)
20
+ end
21
+
22
+ describe 'triggers_still_exist?' do
23
+ it 'should return true when triggers still exist' do
24
+ assert @invoker.triggers_still_exist?(@invoker.connection, @entangler)
25
+ end
26
+
27
+ it 'should return false when triggers do not exist' do
28
+ @entangler.after
29
+
30
+ refute @invoker.triggers_still_exist?(@invoker.connection, @entangler)
31
+ end
32
+ end
33
+ end