lhm-shopify 3.3.5

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 (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