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.
- checksums.yaml +7 -0
- data/.github/workflows/test.yml +34 -0
- data/.gitignore +17 -0
- data/.rubocop.yml +183 -0
- data/.travis.yml +21 -0
- data/CHANGELOG.md +216 -0
- data/Gemfile +5 -0
- data/LICENSE +27 -0
- data/README.md +284 -0
- data/Rakefile +22 -0
- data/bin/.gitkeep +0 -0
- data/dbdeployer/config.json +32 -0
- data/dbdeployer/install.sh +64 -0
- data/dev.yml +20 -0
- data/gemfiles/ar-2.3_mysql.gemfile +6 -0
- data/gemfiles/ar-3.2_mysql.gemfile +5 -0
- data/gemfiles/ar-3.2_mysql2.gemfile +5 -0
- data/gemfiles/ar-4.0_mysql2.gemfile +5 -0
- data/gemfiles/ar-4.1_mysql2.gemfile +5 -0
- data/gemfiles/ar-4.2_mysql2.gemfile +5 -0
- data/gemfiles/ar-5.0_mysql2.gemfile +5 -0
- data/lhm.gemspec +34 -0
- data/lib/lhm.rb +131 -0
- data/lib/lhm/atomic_switcher.rb +52 -0
- data/lib/lhm/chunk_finder.rb +32 -0
- data/lib/lhm/chunk_insert.rb +51 -0
- data/lib/lhm/chunker.rb +87 -0
- data/lib/lhm/cleanup/current.rb +74 -0
- data/lib/lhm/command.rb +48 -0
- data/lib/lhm/entangler.rb +117 -0
- data/lib/lhm/intersection.rb +51 -0
- data/lib/lhm/invoker.rb +98 -0
- data/lib/lhm/locked_switcher.rb +74 -0
- data/lib/lhm/migration.rb +43 -0
- data/lib/lhm/migrator.rb +237 -0
- data/lib/lhm/printer.rb +59 -0
- data/lib/lhm/railtie.rb +9 -0
- data/lib/lhm/sql_helper.rb +77 -0
- data/lib/lhm/sql_retry.rb +61 -0
- data/lib/lhm/table.rb +121 -0
- data/lib/lhm/table_name.rb +23 -0
- data/lib/lhm/test_support.rb +35 -0
- data/lib/lhm/throttler.rb +36 -0
- data/lib/lhm/throttler/slave_lag.rb +145 -0
- data/lib/lhm/throttler/threads_running.rb +53 -0
- data/lib/lhm/throttler/time.rb +29 -0
- data/lib/lhm/timestamp.rb +11 -0
- data/lib/lhm/version.rb +6 -0
- data/shipit.rubygems.yml +0 -0
- data/spec/.lhm.example +4 -0
- data/spec/README.md +58 -0
- data/spec/fixtures/bigint_table.ddl +4 -0
- data/spec/fixtures/composite_primary_key.ddl +7 -0
- data/spec/fixtures/custom_primary_key.ddl +6 -0
- data/spec/fixtures/destination.ddl +6 -0
- data/spec/fixtures/lines.ddl +7 -0
- data/spec/fixtures/origin.ddl +6 -0
- data/spec/fixtures/permissions.ddl +5 -0
- data/spec/fixtures/small_table.ddl +4 -0
- data/spec/fixtures/tracks.ddl +5 -0
- data/spec/fixtures/users.ddl +14 -0
- data/spec/fixtures/wo_id_int_column.ddl +6 -0
- data/spec/integration/atomic_switcher_spec.rb +93 -0
- data/spec/integration/chunk_insert_spec.rb +29 -0
- data/spec/integration/chunker_spec.rb +185 -0
- data/spec/integration/cleanup_spec.rb +136 -0
- data/spec/integration/entangler_spec.rb +66 -0
- data/spec/integration/integration_helper.rb +237 -0
- data/spec/integration/invoker_spec.rb +33 -0
- data/spec/integration/lhm_spec.rb +585 -0
- data/spec/integration/lock_wait_timeout_spec.rb +30 -0
- data/spec/integration/locked_switcher_spec.rb +50 -0
- data/spec/integration/sql_retry/lock_wait_spec.rb +125 -0
- data/spec/integration/sql_retry/lock_wait_timeout_test_helper.rb +101 -0
- data/spec/integration/table_spec.rb +91 -0
- data/spec/test_helper.rb +32 -0
- data/spec/unit/atomic_switcher_spec.rb +31 -0
- data/spec/unit/chunk_finder_spec.rb +73 -0
- data/spec/unit/chunk_insert_spec.rb +44 -0
- data/spec/unit/chunker_spec.rb +166 -0
- data/spec/unit/entangler_spec.rb +124 -0
- data/spec/unit/intersection_spec.rb +51 -0
- data/spec/unit/lhm_spec.rb +29 -0
- data/spec/unit/locked_switcher_spec.rb +51 -0
- data/spec/unit/migrator_spec.rb +146 -0
- data/spec/unit/printer_spec.rb +97 -0
- data/spec/unit/sql_helper_spec.rb +32 -0
- data/spec/unit/table_name_spec.rb +39 -0
- data/spec/unit/table_spec.rb +47 -0
- data/spec/unit/throttler/slave_lag_spec.rb +317 -0
- data/spec/unit/throttler/threads_running_spec.rb +64 -0
- data/spec/unit/throttler_spec.rb +124 -0
- data/spec/unit/unit_helper.rb +13 -0
- metadata +239 -0
data/lib/lhm/version.rb
ADDED
data/shipit.rubygems.yml
ADDED
File without changes
|
data/spec/.lhm.example
ADDED
data/spec/README.md
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# Preparing for master slave integration tests
|
2
|
+
|
3
|
+
## Configuration
|
4
|
+
|
5
|
+
create ~/.lhm:
|
6
|
+
|
7
|
+
mysqldir=/usr/local/mysql
|
8
|
+
basedir=~/lhm-cluster
|
9
|
+
master_port=3306
|
10
|
+
slave_port=3307
|
11
|
+
|
12
|
+
mysqldir specifies the location of your mysql install. basedir is the
|
13
|
+
directory master and slave databases will get installed into.
|
14
|
+
|
15
|
+
## Automatic setup
|
16
|
+
|
17
|
+
### Run
|
18
|
+
|
19
|
+
bin/lhm-spec-clobber.sh
|
20
|
+
|
21
|
+
You can set the integration specs up to run against a master slave setup by
|
22
|
+
running the included that. This deletes the configured lhm master slave setup and reinstalls and configures a master slave setup.
|
23
|
+
|
24
|
+
Follow the manual instructions if you want more control over this process.
|
25
|
+
|
26
|
+
## Manual setup
|
27
|
+
|
28
|
+
### set up instances
|
29
|
+
|
30
|
+
bin/lhm-spec-setup-cluster.sh
|
31
|
+
|
32
|
+
### start instances
|
33
|
+
|
34
|
+
basedir=/opt/lhm-luster
|
35
|
+
mysqld --defaults-file="$basedir/master/my.cnf"
|
36
|
+
mysqld --defaults-file="$basedir/slave/my.cnf"
|
37
|
+
|
38
|
+
### run the grants
|
39
|
+
|
40
|
+
bin/lhm-spec-grants.sh
|
41
|
+
|
42
|
+
## run specs
|
43
|
+
|
44
|
+
Setup the dependency gems
|
45
|
+
|
46
|
+
export BUNDLE_GEMFILE=gemfiles/ar-4.2_mysql2.gemfile
|
47
|
+
bundle install
|
48
|
+
|
49
|
+
To run specs in slave mode, set the MASTER_SLAVE=1 when running tests:
|
50
|
+
|
51
|
+
MASTER_SLAVE=1 bundle exec rake specs
|
52
|
+
|
53
|
+
# connecting
|
54
|
+
|
55
|
+
you can connect by running (with the respective ports):
|
56
|
+
|
57
|
+
mysql --protocol=TCP -p3307
|
58
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
CREATE TABLE `users` (
|
2
|
+
`id` int(11) NOT NULL AUTO_INCREMENT,
|
3
|
+
`reference` int(11) DEFAULT NULL,
|
4
|
+
`username` varchar(255) DEFAULT NULL,
|
5
|
+
`group` varchar(255) DEFAULT 'Superfriends',
|
6
|
+
`created_at` datetime DEFAULT NULL,
|
7
|
+
`comment` varchar(20) DEFAULT NULL,
|
8
|
+
`description` text,
|
9
|
+
PRIMARY KEY (`id`),
|
10
|
+
UNIQUE KEY `index_users_on_reference` (`reference`),
|
11
|
+
KEY `index_users_on_username_and_created_at` (`username`,`created_at`),
|
12
|
+
KEY `index_with_a_custom_name` (`username`,`group`)
|
13
|
+
|
14
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
@@ -0,0 +1,93 @@
|
|
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/atomic_switcher'
|
9
|
+
|
10
|
+
describe Lhm::AtomicSwitcher do
|
11
|
+
include IntegrationHelper
|
12
|
+
|
13
|
+
before(:each) { connect_master! }
|
14
|
+
|
15
|
+
describe 'switching' do
|
16
|
+
before(:each) do
|
17
|
+
Thread.abort_on_exception = true
|
18
|
+
@origin = table_create('origin')
|
19
|
+
@destination = table_create('destination')
|
20
|
+
@migration = Lhm::Migration.new(@origin, @destination)
|
21
|
+
@logs = StringIO.new
|
22
|
+
Lhm.logger = Logger.new(@logs)
|
23
|
+
@connection.execute('SET GLOBAL innodb_lock_wait_timeout=3')
|
24
|
+
@connection.execute('SET GLOBAL lock_wait_timeout=3')
|
25
|
+
end
|
26
|
+
|
27
|
+
after(:each) do
|
28
|
+
Thread.abort_on_exception = false
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should retry and log on lock wait timeouts' do
|
32
|
+
connection = mock()
|
33
|
+
connection.stubs(:data_source_exists?).returns(true)
|
34
|
+
connection.stubs(:execute).raises(ActiveRecord::StatementInvalid, 'Lock wait timeout exceeded; try restarting transaction.').then.returns(true)
|
35
|
+
|
36
|
+
switcher = Lhm::AtomicSwitcher.new(@migration, connection, retriable: {base_interval: 0})
|
37
|
+
|
38
|
+
assert switcher.run
|
39
|
+
|
40
|
+
log_messages = @logs.string.split("\n")
|
41
|
+
assert_equal(2, log_messages.length)
|
42
|
+
assert log_messages[0].include? "Starting run of class=Lhm::AtomicSwitcher"
|
43
|
+
assert log_messages[1].include? "[AtomicSwitcher] ActiveRecord::StatementInvalid: 'Lock wait timeout exceeded; try restarting transaction.' - 1 tries"
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should give up on lock wait timeouts after a configured number of tries' do
|
47
|
+
connection = mock()
|
48
|
+
connection.stubs(:data_source_exists?).returns(true)
|
49
|
+
connection.stubs(:execute).twice.raises(ActiveRecord::StatementInvalid, 'Lock wait timeout exceeded; try restarting transaction.')
|
50
|
+
|
51
|
+
switcher = Lhm::AtomicSwitcher.new(@migration, connection, retriable: {tries: 2, base_interval: 0})
|
52
|
+
|
53
|
+
assert_raises(ActiveRecord::StatementInvalid) { switcher.run }
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should raise on non lock wait timeout exceptions' do
|
57
|
+
switcher = Lhm::AtomicSwitcher.new(@migration, connection)
|
58
|
+
switcher.send :define_singleton_method, :atomic_switch do
|
59
|
+
'SELECT * FROM nonexistent'
|
60
|
+
end
|
61
|
+
-> { switcher.run }.must_raise(ActiveRecord::StatementInvalid)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should raise when destination doesn't exist" do
|
65
|
+
connection = mock()
|
66
|
+
connection.stubs(:data_source_exists?).returns(false)
|
67
|
+
|
68
|
+
switcher = Lhm::AtomicSwitcher.new(@migration, connection)
|
69
|
+
|
70
|
+
assert_raises(Lhm::Error) { switcher.run }
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'rename origin to archive' do
|
74
|
+
switcher = Lhm::AtomicSwitcher.new(@migration, connection)
|
75
|
+
switcher.run
|
76
|
+
|
77
|
+
slave do
|
78
|
+
data_source_exists?(@origin).must_equal true
|
79
|
+
table_read(@migration.archive_name).columns.keys.must_include 'origin'
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'rename destination to origin' do
|
84
|
+
switcher = Lhm::AtomicSwitcher.new(@migration, connection)
|
85
|
+
switcher.run
|
86
|
+
|
87
|
+
slave do
|
88
|
+
data_source_exists?(@destination).must_equal false
|
89
|
+
table_read(@origin.name).columns.keys.must_include 'destination'
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
|
2
|
+
require 'lhm/migration'
|
3
|
+
|
4
|
+
describe Lhm::ChunkInsert do
|
5
|
+
include IntegrationHelper
|
6
|
+
|
7
|
+
describe 'insert_and_return_count_of_rows_created' do
|
8
|
+
before(:each) do
|
9
|
+
connect_master!
|
10
|
+
@origin = table_create(:origin)
|
11
|
+
@destination = table_create(:destination)
|
12
|
+
@migration = Lhm::Migration.new(@origin, @destination)
|
13
|
+
execute("insert into origin set id = 1001")
|
14
|
+
@instance = Lhm::ChunkInsert.new(@migration, connection, 1001, 1001)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "returns the count" do
|
18
|
+
assert_equal 1, @instance.insert_and_return_count_of_rows_created
|
19
|
+
end
|
20
|
+
|
21
|
+
it "inserts the record into the slave" do
|
22
|
+
@instance.insert_and_return_count_of_rows_created
|
23
|
+
|
24
|
+
slave do
|
25
|
+
count_all(@destination.name).must_equal(1)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,185 @@
|
|
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 'lhm/table'
|
6
|
+
require 'lhm/migration'
|
7
|
+
|
8
|
+
describe Lhm::Chunker do
|
9
|
+
include IntegrationHelper
|
10
|
+
|
11
|
+
before(:each) { connect_master! }
|
12
|
+
|
13
|
+
describe 'copying' do
|
14
|
+
before(:each) do
|
15
|
+
@origin = table_create(:origin)
|
16
|
+
@destination = table_create(:destination)
|
17
|
+
@migration = Lhm::Migration.new(@origin, @destination)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should copy 1 row from origin to destination even if the id of the single row does not start at 1' do
|
21
|
+
execute("insert into origin set id = 1001 ")
|
22
|
+
printer = Lhm::Printer::Base.new
|
23
|
+
|
24
|
+
def printer.notify(*) ;end
|
25
|
+
def printer.end(*) [] ;end
|
26
|
+
|
27
|
+
Lhm::Chunker.new(@migration, connection, {:throttler => Lhm::Throttler::Time.new(:stride => 100), :printer => printer} ).run
|
28
|
+
|
29
|
+
slave do
|
30
|
+
count_all(@destination.name).must_equal(1)
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should create the modified destination, even if the source is empty' do
|
36
|
+
execute("truncate origin ")
|
37
|
+
printer = Lhm::Printer::Base.new
|
38
|
+
|
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
|
43
|
+
|
44
|
+
slave do
|
45
|
+
count_all(@destination.name).must_equal(0)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should copy 23 rows from origin to destination in one shot, regardless of the value of the id' do
|
51
|
+
23.times { |n| execute("insert into origin set id = '#{ n * n + 23 }'") }
|
52
|
+
|
53
|
+
printer = MiniTest::Mock.new
|
54
|
+
printer.expect(:notify, :return_value, [Integer, Integer])
|
55
|
+
printer.expect(:end, :return_value, [])
|
56
|
+
|
57
|
+
Lhm::Chunker.new(
|
58
|
+
@migration, connection, { :throttler => Lhm::Throttler::Time.new(:stride => 100), :printer => printer }
|
59
|
+
).run
|
60
|
+
|
61
|
+
slave do
|
62
|
+
count_all(@destination.name).must_equal(23)
|
63
|
+
end
|
64
|
+
|
65
|
+
printer.verify
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'should copy all the records of a table, even if the last chunk starts with the last record of it.' do
|
70
|
+
11.times { |n| execute("insert into origin set id = '#{ n + 1 }'") }
|
71
|
+
|
72
|
+
printer = Lhm::Printer::Base.new
|
73
|
+
|
74
|
+
def printer.notify(*) ;end
|
75
|
+
def printer.end(*) [] ;end
|
76
|
+
|
77
|
+
Lhm::Chunker.new(
|
78
|
+
@migration, connection, { :throttler => Lhm::Throttler::Time.new(:stride => 10), :printer => printer }
|
79
|
+
).run
|
80
|
+
|
81
|
+
slave do
|
82
|
+
count_all(@destination.name).must_equal(11)
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'should copy 23 rows from origin to destination in one shot with slave lag based throttler, regardless of the value of the id' do
|
88
|
+
23.times { |n| execute("insert into origin set id = '#{ 100000 + n * n + 23 }'") }
|
89
|
+
|
90
|
+
printer = MiniTest::Mock.new
|
91
|
+
printer.expect(:notify, :return_value, [Integer, Integer])
|
92
|
+
printer.expect(:end, :return_value, [])
|
93
|
+
|
94
|
+
Lhm::Chunker.new(
|
95
|
+
@migration, connection, { :throttler => Lhm::Throttler::SlaveLag.new(:stride => 100), :printer => printer }
|
96
|
+
).run
|
97
|
+
|
98
|
+
slave do
|
99
|
+
count_all(@destination.name).must_equal(23)
|
100
|
+
end
|
101
|
+
|
102
|
+
printer.verify
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'should throttle work stride based on slave lag' do
|
106
|
+
15.times { |n| execute("insert into origin set id = '#{ (n * n) + 1 }'") }
|
107
|
+
|
108
|
+
printer = mock()
|
109
|
+
printer.expects(:notify).with(instance_of(Integer), instance_of(Integer)).twice
|
110
|
+
printer.expects(:end)
|
111
|
+
|
112
|
+
throttler = Lhm::Throttler::SlaveLag.new(:stride => 10, :allowed_lag => 0)
|
113
|
+
def throttler.max_current_slave_lag
|
114
|
+
1
|
115
|
+
end
|
116
|
+
|
117
|
+
Lhm::Chunker.new(
|
118
|
+
@migration, connection, { :throttler => throttler, :printer => printer }
|
119
|
+
).run
|
120
|
+
|
121
|
+
assert_equal(Lhm::Throttler::SlaveLag::INITIAL_TIMEOUT * 2 * 2, throttler.timeout_seconds)
|
122
|
+
|
123
|
+
slave do
|
124
|
+
count_all(@destination.name).must_equal(15)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'should detect a single slave with no lag in the default configuration' do
|
129
|
+
15.times { |n| execute("insert into origin set id = '#{ (n * n) + 1 }'") }
|
130
|
+
|
131
|
+
printer = mock()
|
132
|
+
printer.expects(:notify).with(instance_of(Integer), instance_of(Integer)).twice
|
133
|
+
printer.expects(:verify)
|
134
|
+
printer.expects(:end)
|
135
|
+
|
136
|
+
throttler = Lhm::Throttler::SlaveLag.new(:stride => 10, :allowed_lag => 0)
|
137
|
+
|
138
|
+
def throttler.slave_hosts
|
139
|
+
['127.0.0.1']
|
140
|
+
end
|
141
|
+
|
142
|
+
if master_slave_mode?
|
143
|
+
def throttler.slave_connection(slave)
|
144
|
+
config = ActiveRecord::Base.connection_pool.spec.config.dup
|
145
|
+
config[:host] = slave
|
146
|
+
config[:port] = 3307
|
147
|
+
ActiveRecord::Base.send('mysql2_connection', config)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
Lhm::Chunker.new(
|
152
|
+
@migration, connection, { :throttler => throttler, :printer => printer }
|
153
|
+
).run
|
154
|
+
|
155
|
+
assert_equal(Lhm::Throttler::SlaveLag::INITIAL_TIMEOUT, throttler.timeout_seconds)
|
156
|
+
assert_equal(0, throttler.send(:max_current_slave_lag))
|
157
|
+
|
158
|
+
slave do
|
159
|
+
count_all(@destination.name).must_equal(15)
|
160
|
+
end
|
161
|
+
|
162
|
+
printer.verify
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'should abort early if the triggers are removed' do
|
166
|
+
15.times { |n| execute("insert into origin set id = '#{ (n * n) + 1 }'") }
|
167
|
+
|
168
|
+
printer = mock()
|
169
|
+
|
170
|
+
failer = Proc.new { false }
|
171
|
+
|
172
|
+
exception = assert_raises do
|
173
|
+
Lhm::Chunker.new(
|
174
|
+
@migration, connection, { :verifier => failer, :printer => printer, :throttler => Lhm::Throttler::Time.new(:stride => 100) }
|
175
|
+
).run
|
176
|
+
end
|
177
|
+
|
178
|
+
assert_match "Verification failed, aborting early", exception.message
|
179
|
+
|
180
|
+
slave do
|
181
|
+
count_all(@destination.name).must_equal(0)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|