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,317 @@
1
+ require File.expand_path(File.dirname(__FILE__)) + '/../unit_helper'
2
+
3
+ require 'lhm/throttler/slave_lag'
4
+
5
+ describe Lhm::Throttler do
6
+ include UnitHelper
7
+
8
+ describe '#format_hosts' do
9
+ describe 'with only localhost hosts' do
10
+ it 'returns no hosts' do
11
+ assert_equal([], Lhm::Throttler.format_hosts(['localhost:1234', '127.0.0.1:5678']))
12
+ end
13
+ end
14
+
15
+ describe 'with only remote hosts' do
16
+ it 'returns remote hosts' do
17
+ assert_equal(['server.example.com', 'anotherserver.example.com'], Lhm::Throttler.format_hosts(['server.example.com:1234', 'anotherserver.example.com']))
18
+ end
19
+ end
20
+
21
+ describe 'with only nil hosts' do
22
+ it 'returns no hosts' do
23
+ assert_equal([], Lhm::Throttler.format_hosts([nil]))
24
+ end
25
+ end
26
+
27
+ describe 'with some nil hosts' do
28
+ it 'returns the remaining hosts' do
29
+ assert_equal(['server.example.com'], Lhm::Throttler.format_hosts([nil, 'server.example.com:1234']))
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ describe Lhm::Throttler::Slave do
36
+ include UnitHelper
37
+
38
+ before :each do
39
+ @logs = StringIO.new
40
+ Lhm.logger = Logger.new(@logs)
41
+
42
+ @dummy_mysql_client_config = lambda { {'username' => 'user', 'password' => 'pw', 'database' => 'db'} }
43
+ end
44
+
45
+ describe "#client" do
46
+ before do
47
+ class TestMysql2Client
48
+ def initialize(config)
49
+ raise Mysql2::Error.new("connection error")
50
+ end
51
+ end
52
+ end
53
+
54
+ describe 'on connection error' do
55
+ it 'logs and returns nil' do
56
+ assert_nil(Lhm::Throttler::Slave.new('slave', @dummy_mysql_client_config).connection)
57
+
58
+ log_messages = @logs.string.lines
59
+ assert_equal(2, log_messages.length)
60
+ assert log_messages[0].include? "Connecting to slave on database: db"
61
+ assert log_messages[1].include? "Error connecting to slave: Unknown MySQL server host 'slave'"
62
+ end
63
+ end
64
+
65
+ describe 'with proper config' do
66
+ it "creates a new Mysql2::Client" do
67
+ expected_config = {username: 'user', password: 'pw', database: 'db', host: 'slave'}
68
+ Mysql2::Client.stubs(:new).with(expected_config).returns(mock())
69
+
70
+ assert Lhm::Throttler::Slave.new('slave', @dummy_mysql_client_config).connection
71
+ end
72
+ end
73
+
74
+ describe 'with active record config' do
75
+ it 'logs and creates client' do
76
+ active_record_config = {username: 'user', password: 'pw', database: 'db'}
77
+ ActiveRecord::Base.stubs(:connection_pool).returns(stub(spec: stub(config: active_record_config)))
78
+
79
+ Mysql2::Client.stubs(:new).returns(mock())
80
+
81
+ assert Lhm::Throttler::Slave.new('slave').connection
82
+
83
+ log_messages = @logs.string.lines
84
+ assert_equal(1, log_messages.length)
85
+ assert log_messages[0].include? "Connecting to slave on database: db"
86
+ end
87
+ end
88
+ end
89
+
90
+ describe "#connection" do
91
+ before do
92
+ class Connection
93
+ def self.query(query)
94
+ if query == Lhm::Throttler::Slave::SQL_SELECT_MAX_SLAVE_LAG
95
+ [{'Seconds_Behind_Master' => 20}]
96
+ elsif query == Lhm::Throttler::Slave::SQL_SELECT_SLAVE_HOSTS
97
+ [{'host' => '1.1.1.1:80'}]
98
+ end
99
+ end
100
+ end
101
+
102
+ @slave = Lhm::Throttler::Slave.new('slave', @dummy_mysql_client_config)
103
+ @slave.instance_variable_set(:@connection, Connection)
104
+
105
+ class StoppedConnection
106
+ def self.query(query)
107
+ [{'Seconds_Behind_Master' => nil}]
108
+ end
109
+ end
110
+
111
+ @stopped_slave = Lhm::Throttler::Slave.new('stopped_slave', @dummy_mysql_client_config)
112
+ @stopped_slave.instance_variable_set(:@connection, StoppedConnection)
113
+ end
114
+
115
+ describe "#lag" do
116
+ it "returns the slave lag" do
117
+ assert_equal(20, @slave.lag)
118
+ end
119
+ end
120
+
121
+ describe "#lag with a stopped slave" do
122
+ it "returns 0 slave lag" do
123
+ assert_equal(0, @stopped_slave.lag)
124
+ end
125
+ end
126
+
127
+ describe "#slave_hosts" do
128
+ it "returns the hosts" do
129
+ assert_equal(['1.1.1.1'], @slave.slave_hosts)
130
+ end
131
+ end
132
+
133
+ describe "#lag on connection error" do
134
+ it "logs and returns 0 slave lag" do
135
+ client = mock()
136
+ client.stubs(:query).raises(Mysql2::Error, "Can't connect to MySQL server")
137
+ Lhm::Throttler::Slave.any_instance.stubs(:client).returns(client)
138
+ Lhm::Throttler::Slave.any_instance.stubs(:config).returns([])
139
+
140
+ slave = Lhm::Throttler::Slave.new('slave', @dummy_mysql_client_config)
141
+ assert_send([Lhm.logger, :info, "Unable to connect and/or query slave: error"])
142
+ assert_equal(0, slave.lag)
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ describe Lhm::Throttler::SlaveLag do
149
+ include UnitHelper
150
+
151
+ before :each do
152
+ @throttler = Lhm::Throttler::SlaveLag.new
153
+ end
154
+
155
+ describe '#throttle_seconds' do
156
+ describe 'with no slave lag' do
157
+ before do
158
+ def @throttler.max_current_slave_lag
159
+ 0
160
+ end
161
+ end
162
+
163
+ it 'does not alter the currently set timeout' do
164
+ timeout = @throttler.timeout_seconds
165
+ assert_equal(timeout, @throttler.send(:throttle_seconds))
166
+ end
167
+ end
168
+
169
+ describe 'with a large slave lag' do
170
+ before do
171
+ def @throttler.max_current_slave_lag
172
+ 100
173
+ end
174
+ end
175
+
176
+ it 'doubles the currently set timeout' do
177
+ timeout = @throttler.timeout_seconds
178
+ assert_equal(timeout * 2, @throttler.send(:throttle_seconds))
179
+ end
180
+
181
+ it 'does not increase the timeout past the maximum' do
182
+ @throttler.timeout_seconds = Lhm::Throttler::SlaveLag::MAX_TIMEOUT
183
+ assert_equal(Lhm::Throttler::SlaveLag::MAX_TIMEOUT, @throttler.send(:throttle_seconds))
184
+ end
185
+ end
186
+
187
+ describe 'with no slave lag after it has previously been increased' do
188
+ before do
189
+ def @throttler.max_current_slave_lag
190
+ 0
191
+ end
192
+ end
193
+
194
+ it 'halves the currently set timeout' do
195
+ @throttler.timeout_seconds *= 2 * 2
196
+ timeout = @throttler.timeout_seconds
197
+ assert_equal(timeout / 2, @throttler.send(:throttle_seconds))
198
+ end
199
+
200
+ it 'does not decrease the timeout past the minimum on repeated runs' do
201
+ @throttler.timeout_seconds = Lhm::Throttler::SlaveLag::INITIAL_TIMEOUT * 2
202
+ assert_equal(Lhm::Throttler::SlaveLag::INITIAL_TIMEOUT, @throttler.send(:throttle_seconds))
203
+ assert_equal(Lhm::Throttler::SlaveLag::INITIAL_TIMEOUT, @throttler.send(:throttle_seconds))
204
+ end
205
+ end
206
+ end
207
+
208
+ describe '#max_current_slave_lag' do
209
+ describe 'with multiple slaves' do
210
+ it 'returns the largest amount of lag' do
211
+ slave1 = mock()
212
+ slave2 = mock()
213
+ slave1.stubs(:lag).returns(5)
214
+ slave2.stubs(:lag).returns(0)
215
+ Lhm::Throttler::SlaveLag.any_instance.stubs(:slaves).returns([slave1, slave2])
216
+ assert_equal 5, @throttler.send(:max_current_slave_lag)
217
+ end
218
+ end
219
+
220
+ describe 'with MySQL stopped on the slave' do
221
+ it 'assumes 0 slave lag' do
222
+ client = mock()
223
+ client.stubs(:query).raises(Mysql2::Error, "Can't connect to MySQL server")
224
+ Lhm::Throttler::Slave.any_instance.stubs(:client).returns(client)
225
+
226
+ Lhm::Throttler::Slave.any_instance.stubs(:prepare_connection_config).returns([])
227
+ Lhm::Throttler::Slave.any_instance.stubs(:slave_hosts).returns(['1.1.1.2'])
228
+ @throttler.stubs(:master_slave_hosts).returns(['1.1.1.1'])
229
+
230
+ assert_equal 0, @throttler.send(:max_current_slave_lag)
231
+ end
232
+ end
233
+ end
234
+
235
+ describe '#get_slaves' do
236
+ describe 'with no slaves' do
237
+ before do
238
+ def @throttler.master_slave_hosts
239
+ []
240
+ end
241
+ end
242
+
243
+ it 'returns no slaves' do
244
+ assert_equal([], @throttler.send(:get_slaves))
245
+ end
246
+ end
247
+
248
+ describe 'with multiple slaves' do
249
+ before do
250
+ class TestSlave
251
+ attr_reader :host, :connection
252
+
253
+ def initialize(host, _)
254
+ @host = host
255
+ @connection = 'conn' if @host
256
+ end
257
+
258
+ def slave_hosts
259
+ if @host == '1.1.1.1'
260
+ ['1.1.1.2', '1.1.1.3']
261
+ else
262
+ [nil]
263
+ end
264
+ end
265
+ end
266
+
267
+ @create_slave = lambda { |host, config|
268
+ TestSlave.new(host, config)
269
+ }
270
+ end
271
+
272
+ describe 'without the :check_only option' do
273
+ before do
274
+ def @throttler.master_slave_hosts
275
+ ['1.1.1.1', '1.1.1.4']
276
+ end
277
+ end
278
+
279
+ it 'returns the slave instances' do
280
+ Lhm::Throttler::Slave.stubs(:new).returns(@create_slave) do
281
+ assert_equal(["1.1.1.4", "1.1.1.1", "1.1.1.3", "1.1.1.2"], @throttler.send(:get_slaves).map(&:host))
282
+ end
283
+ end
284
+ end
285
+
286
+ describe 'with the :check_only option' do
287
+ describe 'with a callable argument' do
288
+ before do
289
+ check_only = lambda {{'host' => '1.1.1.3'}}
290
+ @throttler = Lhm::Throttler::SlaveLag.new :check_only => check_only
291
+ end
292
+
293
+ it 'returns only that single slave' do
294
+ Lhm::Throttler::Slave.stubs(:new).returns(@create_slave) do
295
+ assert_equal ['1.1.1.3'], @throttler.send(:get_slaves).map(&:host)
296
+ end
297
+ end
298
+ end
299
+
300
+ describe 'with a non-callable argument' do
301
+ before do
302
+ @throttler = Lhm::Throttler::SlaveLag.new :check_only => 'I cannot be called'
303
+ def @throttler.master_slave_hosts
304
+ ['1.1.1.1', '1.1.1.4']
305
+ end
306
+ end
307
+
308
+ it 'returns all the slave instances' do
309
+ Lhm::Throttler::Slave.stubs(:new).returns(@create_slave) do
310
+ assert_equal(["1.1.1.4", "1.1.1.1", "1.1.1.3", "1.1.1.2"], @throttler.send(:get_slaves).map(&:host))
311
+ end
312
+ end
313
+ end
314
+ end
315
+ end
316
+ end
317
+ end
@@ -0,0 +1,64 @@
1
+ require File.expand_path(File.dirname(__FILE__)) + '/../unit_helper'
2
+
3
+ require 'lhm/throttler/threads_running'
4
+
5
+ describe Lhm::Throttler::ThreadsRunning do
6
+ include UnitHelper
7
+
8
+ before :each do
9
+ @throttler = Lhm::Throttler::ThreadsRunning.new
10
+ end
11
+
12
+ describe '#throttle_seconds' do
13
+ describe 'with no mysql activity' do
14
+ before do
15
+ def @throttler.threads_running
16
+ 0
17
+ end
18
+ end
19
+
20
+ it 'does not alter the currently set timeout' do
21
+ timeout = @throttler.timeout_seconds
22
+ assert_equal(timeout, @throttler.send(:throttle_seconds))
23
+ end
24
+ end
25
+
26
+ describe 'with an overloaded mysql' do
27
+ before do
28
+ def @throttler.threads_running
29
+ 100
30
+ end
31
+ end
32
+
33
+ it 'doubles the currently set timeout' do
34
+ timeout = @throttler.timeout_seconds
35
+ assert_equal(timeout * 2, @throttler.send(:throttle_seconds))
36
+ end
37
+
38
+ it 'does not increase the timeout past the maximum' do
39
+ @throttler.timeout_seconds = @throttler.max_timeout_seconds
40
+ assert_equal(@throttler.max_timeout_seconds, @throttler.send(:throttle_seconds))
41
+ end
42
+ end
43
+
44
+ describe 'with an idle mysql after it has previously been busy' do
45
+ before do
46
+ def @throttler.threads_running
47
+ 0
48
+ end
49
+ end
50
+
51
+ it 'halves the currently set timeout' do
52
+ @throttler.timeout_seconds *= 2 * 2
53
+ timeout = @throttler.timeout_seconds
54
+ assert_equal(timeout / 2, @throttler.send(:throttle_seconds))
55
+ end
56
+
57
+ it 'does not decrease the timeout past the minimum on repeated runs' do
58
+ @throttler.timeout_seconds = @throttler.initial_timeout_seconds * 2
59
+ assert_equal(@throttler.initial_timeout_seconds, @throttler.send(:throttle_seconds))
60
+ assert_equal(@throttler.initial_timeout_seconds, @throttler.send(:throttle_seconds))
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,124 @@
1
+ require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
2
+
3
+ require 'lhm/throttler'
4
+
5
+ describe Lhm::Throttler do
6
+ include UnitHelper
7
+
8
+ before :each do
9
+ @mock = Class.new do
10
+ extend Lhm::Throttler
11
+ end
12
+
13
+ @conn = Class.new do
14
+ def execute
15
+ end
16
+ end
17
+ end
18
+
19
+ describe '#setup_throttler' do
20
+ describe 'when passing a time_throttler key' do
21
+ before do
22
+ @mock.setup_throttler(:time_throttler, delay: 2)
23
+ end
24
+
25
+ it 'instantiates the time throttle' do
26
+ @mock.throttler.class.must_equal Lhm::Throttler::Time
27
+ end
28
+
29
+ it 'returns 2 seconds as time' do
30
+ @mock.throttler.timeout_seconds.must_equal 2
31
+ end
32
+ end
33
+
34
+ describe 'when passing a slave_lag_throttler key' do
35
+ before do
36
+ @mock.setup_throttler(:slave_lag_throttler, allowed_lag: 20)
37
+ end
38
+
39
+ it 'instantiates the slave_lag throttle' do
40
+ @mock.throttler.class.must_equal Lhm::Throttler::SlaveLag
41
+ end
42
+
43
+ it 'returns 20 seconds as allowed_lag' do
44
+ @mock.throttler.allowed_lag.must_equal 20
45
+ end
46
+ end
47
+
48
+ describe 'when passing a time_throttler instance' do
49
+
50
+ before do
51
+ @instance = Class.new(Lhm::Throttler::Time) do
52
+ def timeout_seconds
53
+ 0
54
+ end
55
+ end.new
56
+
57
+ @mock.setup_throttler(@instance)
58
+ end
59
+
60
+ it 'returns the instace given' do
61
+ @mock.throttler.must_equal @instance
62
+ end
63
+
64
+ it 'returns 0 seconds as time' do
65
+ @mock.throttler.timeout_seconds.must_equal 0
66
+ end
67
+ end
68
+
69
+ describe 'when passing a slave_lag_throttler instance' do
70
+
71
+ before do
72
+ @instance = Lhm::Throttler::SlaveLag.new
73
+ def @instance.timeout_seconds
74
+ 0
75
+ end
76
+
77
+ @mock.setup_throttler(@instance)
78
+ end
79
+
80
+ it 'returns the instace given' do
81
+ @mock.throttler.must_equal @instance
82
+ end
83
+
84
+ it 'returns 0 seconds as time' do
85
+ @mock.throttler.timeout_seconds.must_equal 0
86
+ end
87
+ end
88
+
89
+ describe 'when passing a time_throttler class' do
90
+
91
+ before do
92
+ @klass = Class.new(Lhm::Throttler::Time)
93
+ @mock.setup_throttler(@klass)
94
+ end
95
+
96
+ it 'has the same class as given' do
97
+ @mock.throttler.class.must_equal @klass
98
+ end
99
+ end
100
+
101
+ describe 'when passing a slave_lag_throttler class' do
102
+
103
+ before do
104
+ @klass = Class.new(Lhm::Throttler::SlaveLag)
105
+ @mock.setup_throttler(@klass)
106
+ end
107
+
108
+ it 'has the same class as given' do
109
+ @mock.throttler.class.must_equal @klass
110
+ end
111
+ end
112
+ end
113
+
114
+ describe '#throttler' do
115
+
116
+ it 'returns the default Time based' do
117
+ @mock.throttler.class.must_equal Lhm::Throttler::Time
118
+ end
119
+
120
+ it 'should default to 100 milliseconds' do
121
+ @mock.throttler.timeout_seconds.must_equal 0.1
122
+ end
123
+ end
124
+ end