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.
- checksums.yaml +7 -0
- data/.github/workflows/test.yml +43 -0
- data/.gitignore +12 -0
- data/.rubocop.yml +183 -0
- data/.travis.yml +21 -0
- data/Appraisals +24 -0
- data/CHANGELOG.md +254 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +67 -0
- data/LICENSE +27 -0
- data/README.md +335 -0
- data/Rakefile +33 -0
- data/dev.yml +45 -0
- data/docker-compose.yml +60 -0
- data/gemfiles/activerecord_5.2.gemfile +9 -0
- data/gemfiles/activerecord_5.2.gemfile.lock +66 -0
- data/gemfiles/activerecord_6.0.gemfile +7 -0
- data/gemfiles/activerecord_6.0.gemfile.lock +68 -0
- data/gemfiles/activerecord_6.1.gemfile +7 -0
- data/gemfiles/activerecord_6.1.gemfile.lock +67 -0
- data/gemfiles/activerecord_7.0.0.alpha2.gemfile +7 -0
- data/gemfiles/activerecord_7.0.0.alpha2.gemfile.lock +65 -0
- data/lhm.gemspec +38 -0
- data/lib/lhm/atomic_switcher.rb +46 -0
- data/lib/lhm/chunk_finder.rb +62 -0
- data/lib/lhm/chunk_insert.rb +61 -0
- data/lib/lhm/chunker.rb +95 -0
- data/lib/lhm/cleanup/current.rb +71 -0
- data/lib/lhm/command.rb +48 -0
- data/lib/lhm/connection.rb +108 -0
- data/lib/lhm/entangler.rb +112 -0
- data/lib/lhm/intersection.rb +51 -0
- data/lib/lhm/invoker.rb +100 -0
- data/lib/lhm/locked_switcher.rb +76 -0
- data/lib/lhm/migration.rb +51 -0
- data/lib/lhm/migrator.rb +244 -0
- data/lib/lhm/printer.rb +63 -0
- data/lib/lhm/proxysql_helper.rb +10 -0
- data/lib/lhm/railtie.rb +9 -0
- data/lib/lhm/sql_helper.rb +77 -0
- data/lib/lhm/sql_retry.rb +180 -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/slave_lag.rb +162 -0
- data/lib/lhm/throttler/threads_running.rb +53 -0
- data/lib/lhm/throttler/time.rb +29 -0
- data/lib/lhm/throttler.rb +36 -0
- data/lib/lhm/timestamp.rb +11 -0
- data/lib/lhm/version.rb +6 -0
- data/lib/lhm-shopify.rb +1 -0
- data/lib/lhm.rb +156 -0
- data/scripts/helpers/wait-for-dbs.sh +21 -0
- data/scripts/mysql/reader/create_replication.sql +10 -0
- data/scripts/mysql/writer/create_test_db.sql +1 -0
- data/scripts/mysql/writer/create_users.sql +6 -0
- data/scripts/proxysql/proxysql.cnf +117 -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 +6 -0
- data/spec/fixtures/composite_primary_key_dest.ddl +6 -0
- data/spec/fixtures/custom_primary_key.ddl +6 -0
- data/spec/fixtures/custom_primary_key_dest.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 +129 -0
- data/spec/integration/chunk_insert_spec.rb +30 -0
- data/spec/integration/chunker_spec.rb +269 -0
- data/spec/integration/cleanup_spec.rb +147 -0
- data/spec/integration/database.yml +25 -0
- data/spec/integration/entangler_spec.rb +68 -0
- data/spec/integration/integration_helper.rb +252 -0
- data/spec/integration/invoker_spec.rb +33 -0
- data/spec/integration/lhm_spec.rb +659 -0
- data/spec/integration/lock_wait_timeout_spec.rb +30 -0
- data/spec/integration/locked_switcher_spec.rb +50 -0
- data/spec/integration/proxysql_spec.rb +34 -0
- data/spec/integration/sql_retry/db_connection_helper.rb +52 -0
- data/spec/integration/sql_retry/lock_wait_spec.rb +127 -0
- data/spec/integration/sql_retry/lock_wait_timeout_test_helper.rb +114 -0
- data/spec/integration/sql_retry/proxysql_helper.rb +22 -0
- data/spec/integration/sql_retry/retry_with_proxysql_spec.rb +109 -0
- data/spec/integration/table_spec.rb +83 -0
- data/spec/integration/toxiproxy_helper.rb +40 -0
- data/spec/test_helper.rb +69 -0
- data/spec/unit/atomic_switcher_spec.rb +29 -0
- data/spec/unit/chunk_finder_spec.rb +73 -0
- data/spec/unit/chunk_insert_spec.rb +67 -0
- data/spec/unit/chunker_spec.rb +176 -0
- data/spec/unit/connection_spec.rb +111 -0
- data/spec/unit/entangler_spec.rb +187 -0
- data/spec/unit/intersection_spec.rb +51 -0
- data/spec/unit/lhm_spec.rb +46 -0
- data/spec/unit/locked_switcher_spec.rb +46 -0
- data/spec/unit/migrator_spec.rb +144 -0
- data/spec/unit/printer_spec.rb +85 -0
- data/spec/unit/sql_helper_spec.rb +28 -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 +322 -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 +26 -0
- metadata +366 -0
data/spec/test_helper.rb
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
|
2
|
+
# Schmidt
|
|
3
|
+
|
|
4
|
+
if ENV['COV']
|
|
5
|
+
require 'simplecov'
|
|
6
|
+
SimpleCov.start
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require 'minitest/autorun'
|
|
10
|
+
require 'minitest/spec'
|
|
11
|
+
require 'minitest/mock'
|
|
12
|
+
require 'mocha/minitest'
|
|
13
|
+
require 'after_do'
|
|
14
|
+
require 'byebug'
|
|
15
|
+
require 'pathname'
|
|
16
|
+
require 'lhm'
|
|
17
|
+
|
|
18
|
+
$project = Pathname.new(File.dirname(__FILE__) + '/..').cleanpath
|
|
19
|
+
$spec = $project.join('spec')
|
|
20
|
+
$fixtures = $spec.join('fixtures')
|
|
21
|
+
|
|
22
|
+
$db_name = 'test'
|
|
23
|
+
|
|
24
|
+
require 'active_record'
|
|
25
|
+
require 'mysql2'
|
|
26
|
+
|
|
27
|
+
logger = Logger.new STDOUT
|
|
28
|
+
logger.level = Logger::WARN
|
|
29
|
+
Lhm.logger = logger
|
|
30
|
+
|
|
31
|
+
# Want test to be efficient without having to wait the normal value of 120s
|
|
32
|
+
Lhm::SqlRetry::RECONNECT_RETRY_MAX_ITERATION = 4
|
|
33
|
+
|
|
34
|
+
def without_verbose(&block)
|
|
35
|
+
old_verbose, $VERBOSE = $VERBOSE, nil
|
|
36
|
+
yield
|
|
37
|
+
ensure
|
|
38
|
+
$VERBOSE = old_verbose
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def printer
|
|
42
|
+
printer = Lhm::Printer::Base.new
|
|
43
|
+
|
|
44
|
+
def printer.notify(*) ;end
|
|
45
|
+
def printer.end(*) [] ;end
|
|
46
|
+
|
|
47
|
+
printer
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def throttler
|
|
51
|
+
Lhm::Throttler::Time.new(:stride => 100)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def init_test_db
|
|
55
|
+
db_config = YAML.load_file(File.expand_path(File.dirname(__FILE__)) + '/integration/database.yml')
|
|
56
|
+
conn = Mysql2::Client.new(
|
|
57
|
+
:host => '127.0.0.1',
|
|
58
|
+
:username => db_config['master']['user'],
|
|
59
|
+
:password => db_config['master']['password'],
|
|
60
|
+
:port => db_config['master']['port']
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
conn.query("DROP DATABASE IF EXISTS #{$db_name}")
|
|
64
|
+
conn.query("CREATE DATABASE #{$db_name}")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
init_test_db
|
|
68
|
+
|
|
69
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
|
2
|
+
# Schmidt
|
|
3
|
+
|
|
4
|
+
require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
|
|
5
|
+
|
|
6
|
+
require 'lhm/table'
|
|
7
|
+
require 'lhm/migration'
|
|
8
|
+
require 'lhm/atomic_switcher'
|
|
9
|
+
|
|
10
|
+
describe Lhm::AtomicSwitcher do
|
|
11
|
+
include UnitHelper
|
|
12
|
+
|
|
13
|
+
before(:each) do
|
|
14
|
+
@start = Time.now
|
|
15
|
+
@origin = Lhm::Table.new('origin')
|
|
16
|
+
@destination = Lhm::Table.new('destination')
|
|
17
|
+
@migration = Lhm::Migration.new(@origin, @destination, @start)
|
|
18
|
+
@switcher = Lhm::AtomicSwitcher.new(@migration, nil)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe 'atomic switch' do
|
|
22
|
+
it 'should perform a single atomic rename' do
|
|
23
|
+
value(@switcher.atomic_switch).must_equal(
|
|
24
|
+
"rename table `origin` to `#{ @migration.archive_name }`, " \
|
|
25
|
+
'`destination` to `origin`'
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
|
2
|
+
# Schmidt
|
|
3
|
+
|
|
4
|
+
require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
|
|
5
|
+
|
|
6
|
+
describe Lhm::ChunkFinder do
|
|
7
|
+
before(:each) do
|
|
8
|
+
@origin = Lhm::Table.new('foo')
|
|
9
|
+
@destination = Lhm::Table.new('bar')
|
|
10
|
+
@migration = Lhm::Migration.new(@origin, @destination)
|
|
11
|
+
@connection = mock()
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe '#validate' do
|
|
15
|
+
describe 'when start is greater than limit' do
|
|
16
|
+
it 'raises' do
|
|
17
|
+
assert_raises { Lhm::ChunkFinder.new(@connection, @migration, {start: 2, limit: 1}).validate }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe 'when start is greater than limit' do
|
|
22
|
+
it 'does not raise' do
|
|
23
|
+
Lhm::ChunkFinder.new(@connection, @migration, {start: 1, limit: 2}).validate # does not raise
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe '#start' do
|
|
29
|
+
describe 'when initialized with 5' do
|
|
30
|
+
before(:each) do
|
|
31
|
+
@instance = Lhm::ChunkFinder.new(@connection, @migration, {start: 5, limit: 6})
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'returns 5' do
|
|
35
|
+
assert_equal @instance.send(:start), 5
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe 'when initialized with nil and the min(id) is 22' do
|
|
40
|
+
before(:each) do
|
|
41
|
+
@connection.expects(:select_value).returns(22)
|
|
42
|
+
@instance = Lhm::ChunkFinder.new(@migration, @connection, {limit: 6})
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'returns 22' do
|
|
46
|
+
assert_equal @instance.send(:start), 22
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
describe '#limit' do
|
|
52
|
+
describe 'when initialized with 6' do
|
|
53
|
+
before(:each) do
|
|
54
|
+
@instance = Lhm::ChunkFinder.new(@connection, @migration, {start: 5, limit: 6})
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'returns 6' do
|
|
58
|
+
assert_equal @instance.send(:limit), 6
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe 'when initialized with nil and the max(id) is 33' do
|
|
63
|
+
before(:each) do
|
|
64
|
+
@connection.expects(:select_value).returns(33)
|
|
65
|
+
@instance = Lhm::ChunkFinder.new(@migration, @connection, {start: 5})
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'returns 33' do
|
|
69
|
+
assert_equal @instance.send(:limit), 33
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
|
2
|
+
# Schmidt
|
|
3
|
+
|
|
4
|
+
require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
|
|
5
|
+
|
|
6
|
+
require 'lhm/chunk_insert'
|
|
7
|
+
require 'lhm/connection'
|
|
8
|
+
|
|
9
|
+
describe Lhm::ChunkInsert do
|
|
10
|
+
before(:each) do
|
|
11
|
+
ar_connection = mock()
|
|
12
|
+
ar_connection.stubs(:execute).returns([["dummy"]])
|
|
13
|
+
@connection = Lhm::Connection.new(connection: ar_connection, options: {reconnect_with_consistent_host: false})
|
|
14
|
+
@origin = Lhm::Table.new('foo')
|
|
15
|
+
@destination = Lhm::Table.new('bar')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe "#sql" do
|
|
19
|
+
describe "when migration has no conditions" do
|
|
20
|
+
before do
|
|
21
|
+
@migration = Lhm::Migration.new(@origin, @destination)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "uses a simple where clause" do
|
|
25
|
+
assert_equal(
|
|
26
|
+
Lhm::ChunkInsert.new(@migration, @connection, 1, 2).send(:sql),
|
|
27
|
+
"insert ignore into `bar` () select from `foo` where `foo`.`id` between 1 and 2"
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe "when migration has a WHERE condition" do
|
|
33
|
+
before do
|
|
34
|
+
@migration = Lhm::Migration.new(
|
|
35
|
+
@origin,
|
|
36
|
+
@destination,
|
|
37
|
+
"where foo.created_at > '2013-07-10' or foo.baz = 'quux'"
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "combines the clause with the chunking WHERE condition" do
|
|
42
|
+
assert_equal(
|
|
43
|
+
Lhm::ChunkInsert.new(@migration, @connection, 1, 2).send(:sql),
|
|
44
|
+
"insert ignore into `bar` () select from `foo` where (foo.created_at > '2013-07-10' or foo.baz = 'quux') and `foo`.`id` between 1 and 2"
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe "when migration has a WHERE as a proc" do
|
|
50
|
+
before do
|
|
51
|
+
@date = Date.today.to_s
|
|
52
|
+
@migration = Lhm::Migration.new(
|
|
53
|
+
@origin,
|
|
54
|
+
@destination,
|
|
55
|
+
-> { "where foo.created_at > '#{@date}' or foo.baz = 'quux'" }
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "combines the clause with the chunking WHERE condition" do
|
|
60
|
+
assert_equal(
|
|
61
|
+
Lhm::ChunkInsert.new(@migration, @connection, 1, 2).send(:sql),
|
|
62
|
+
"insert ignore into `bar` () select from `foo` where (foo.created_at > '#{@date}' or foo.baz = 'quux') and `foo`.`id` between 1 and 2"
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
|
2
|
+
# Schmidt
|
|
3
|
+
|
|
4
|
+
require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
|
|
5
|
+
|
|
6
|
+
require 'lhm/table'
|
|
7
|
+
require 'lhm/migration'
|
|
8
|
+
require 'lhm/chunker'
|
|
9
|
+
require 'lhm/throttler'
|
|
10
|
+
require 'lhm/connection'
|
|
11
|
+
|
|
12
|
+
describe Lhm::Chunker do
|
|
13
|
+
include UnitHelper
|
|
14
|
+
|
|
15
|
+
EXPECTED_RETRY_FLAGS_CHUNKER = {:should_retry => true, :log_prefix => "Chunker"}
|
|
16
|
+
EXPECTED_RETRY_FLAGS_CHUNK_INSERT = {:should_retry => true, :log_prefix => "ChunkInsert"}
|
|
17
|
+
|
|
18
|
+
before(:each) do
|
|
19
|
+
@origin = Lhm::Table.new('foo')
|
|
20
|
+
@destination = Lhm::Table.new('bar')
|
|
21
|
+
@migration = Lhm::Migration.new(@origin, @destination)
|
|
22
|
+
@connection = mock()
|
|
23
|
+
@connection.stubs(:execute).returns([["dummy"]])
|
|
24
|
+
# This is a poor man's stub
|
|
25
|
+
@throttler = Object.new
|
|
26
|
+
def @throttler.run
|
|
27
|
+
# noop
|
|
28
|
+
end
|
|
29
|
+
def @throttler.stride
|
|
30
|
+
1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
@chunker = Lhm::Chunker.new(@migration, @connection, :throttler => @throttler,
|
|
34
|
+
:start => 1,
|
|
35
|
+
:limit => 10)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe '#run' do
|
|
39
|
+
|
|
40
|
+
it 'detects the max id to use in the chunk using the stride and use it if it is lower than the limit' do
|
|
41
|
+
def @throttler.stride
|
|
42
|
+
5
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 4/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(7)
|
|
46
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 8 order by id limit 1 offset 4/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(21)
|
|
47
|
+
@connection.expects(:update).with(regexp_matches(/between 1 and 7/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
|
48
|
+
@connection.expects(:update).with(regexp_matches(/between 8 and 10/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
|
49
|
+
@connection.expects(:execute).twice.with(regexp_matches(/show warnings/),EXPECTED_RETRY_FLAGS_CHUNKER).returns([])
|
|
50
|
+
|
|
51
|
+
@chunker.run
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
it 'chunks the result set according to the stride size' do
|
|
56
|
+
def @throttler.stride
|
|
57
|
+
2
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(2)
|
|
61
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 3 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(4)
|
|
62
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 5 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(6)
|
|
63
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 7 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(8)
|
|
64
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 9 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(10)
|
|
65
|
+
|
|
66
|
+
@connection.expects(:update).with(regexp_matches(/between 1 and 2/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
|
67
|
+
@connection.expects(:update).with(regexp_matches(/between 3 and 4/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
|
68
|
+
@connection.expects(:update).with(regexp_matches(/between 5 and 6/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
|
69
|
+
@connection.expects(:update).with(regexp_matches(/between 7 and 8/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
|
70
|
+
@connection.expects(:update).with(regexp_matches(/between 9 and 10/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
|
71
|
+
|
|
72
|
+
@chunker.run
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'handles stride changes during execution' do
|
|
76
|
+
# roll our own stubbing
|
|
77
|
+
def @throttler.stride
|
|
78
|
+
@run_count ||= 0
|
|
79
|
+
@run_count = @run_count + 1
|
|
80
|
+
if @run_count > 1
|
|
81
|
+
3
|
|
82
|
+
else
|
|
83
|
+
2
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(2)
|
|
88
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 3 order by id limit 1 offset 2/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(5)
|
|
89
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 6 order by id limit 1 offset 2/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(8)
|
|
90
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 9 order by id limit 1 offset 2/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(nil)
|
|
91
|
+
|
|
92
|
+
@connection.expects(:update).with(regexp_matches(/between 1 and 2/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
|
93
|
+
@connection.expects(:update).with(regexp_matches(/between 3 and 5/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
|
94
|
+
@connection.expects(:update).with(regexp_matches(/between 6 and 8/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
|
95
|
+
@connection.expects(:update).with(regexp_matches(/between 9 and 10/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
|
96
|
+
|
|
97
|
+
@connection.expects(:execute).twice.with(regexp_matches(/show warnings/),EXPECTED_RETRY_FLAGS_CHUNKER).returns([])
|
|
98
|
+
|
|
99
|
+
@chunker.run
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'correctly copies single record tables' do
|
|
103
|
+
@chunker = Lhm::Chunker.new(@migration, @connection, :throttler => @throttler,
|
|
104
|
+
:start => 1,
|
|
105
|
+
:limit => 1)
|
|
106
|
+
|
|
107
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 0/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(nil)
|
|
108
|
+
@connection.expects(:update).with(regexp_matches(/between 1 and 1/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(1)
|
|
109
|
+
|
|
110
|
+
@chunker.run
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'copies the last record of a table, even it is the start of the last chunk' do
|
|
114
|
+
@chunker = Lhm::Chunker.new(@migration, @connection, :throttler => @throttler,
|
|
115
|
+
:start => 2,
|
|
116
|
+
:limit => 10)
|
|
117
|
+
def @throttler.stride
|
|
118
|
+
2
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 2 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(3)
|
|
122
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 4 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(5)
|
|
123
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 6 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(7)
|
|
124
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 8 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(9)
|
|
125
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 10 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(nil)
|
|
126
|
+
|
|
127
|
+
@connection.expects(:update).with(regexp_matches(/between 2 and 3/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
|
128
|
+
@connection.expects(:update).with(regexp_matches(/between 4 and 5/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
|
129
|
+
@connection.expects(:update).with(regexp_matches(/between 6 and 7/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
|
130
|
+
@connection.expects(:update).with(regexp_matches(/between 8 and 9/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(2)
|
|
131
|
+
@connection.expects(:update).with(regexp_matches(/between 10 and 10/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(1)
|
|
132
|
+
|
|
133
|
+
@chunker.run
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
it 'separates filter conditions from chunking conditions' do
|
|
138
|
+
@chunker = Lhm::Chunker.new(@migration, @connection, :throttler => @throttler,
|
|
139
|
+
:start => 1,
|
|
140
|
+
:limit => 2)
|
|
141
|
+
def @throttler.stride
|
|
142
|
+
2
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(2)
|
|
146
|
+
@connection.expects(:update).with(regexp_matches(/where \(foo.created_at > '2013-07-10' or foo.baz = 'quux'\) and `foo`/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(1)
|
|
147
|
+
@connection.expects(:execute).with(regexp_matches(/show warnings/),EXPECTED_RETRY_FLAGS_CHUNKER).returns([])
|
|
148
|
+
|
|
149
|
+
def @migration.conditions
|
|
150
|
+
"where foo.created_at > '2013-07-10' or foo.baz = 'quux'"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
@chunker.run
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it "doesn't mess with inner join filters" do
|
|
157
|
+
@chunker = Lhm::Chunker.new(@migration, @connection, :throttler => @throttler,
|
|
158
|
+
:start => 1,
|
|
159
|
+
:limit => 2)
|
|
160
|
+
|
|
161
|
+
def @throttler.stride
|
|
162
|
+
2
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
@connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/),EXPECTED_RETRY_FLAGS_CHUNKER).returns(2)
|
|
166
|
+
@connection.expects(:update).with(regexp_matches(/inner join bar on foo.id = bar.foo_id and/),EXPECTED_RETRY_FLAGS_CHUNK_INSERT).returns(1)
|
|
167
|
+
@connection.expects(:execute).with(regexp_matches(/show warnings/),EXPECTED_RETRY_FLAGS_CHUNKER).returns([])
|
|
168
|
+
|
|
169
|
+
def @migration.conditions
|
|
170
|
+
'inner join bar on foo.id = bar.foo_id'
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
@chunker.run
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
require 'lhm/connection'
|
|
2
|
+
require 'lhm/proxysql_helper'
|
|
3
|
+
|
|
4
|
+
describe Lhm::Connection do
|
|
5
|
+
|
|
6
|
+
LOCK_WAIT = ActiveRecord::StatementInvalid.new('Lock wait timeout exceeded; try restarting transaction.')
|
|
7
|
+
|
|
8
|
+
before(:each) do
|
|
9
|
+
@logs = StringIO.new
|
|
10
|
+
Lhm.logger = Logger.new(@logs)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "Should find use calling file as prefix" do
|
|
14
|
+
ar_connection = mock()
|
|
15
|
+
ar_connection.stubs(:execute).raises(LOCK_WAIT).then.returns(true)
|
|
16
|
+
ar_connection.stubs(:active?).returns(true)
|
|
17
|
+
|
|
18
|
+
connection = Lhm::Connection.new(connection: ar_connection, options: {
|
|
19
|
+
retriable: {
|
|
20
|
+
base_interval: 0
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
connection.execute("SHOW TABLES", should_retry: true)
|
|
25
|
+
|
|
26
|
+
log_messages = @logs.string.split("\n")
|
|
27
|
+
assert_equal(1, log_messages.length)
|
|
28
|
+
assert log_messages.first.include?("[ConnectionSpec]")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "#execute should be retried" do
|
|
32
|
+
ar_connection = mock()
|
|
33
|
+
ar_connection.stubs(:execute).raises(LOCK_WAIT)
|
|
34
|
+
.then.raises(LOCK_WAIT)
|
|
35
|
+
.then.returns(true)
|
|
36
|
+
ar_connection.stubs(:active?).returns(true)
|
|
37
|
+
|
|
38
|
+
connection = Lhm::Connection.new(connection: ar_connection, options: {
|
|
39
|
+
retriable: {
|
|
40
|
+
base_interval: 0,
|
|
41
|
+
tries: 3
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
connection.execute("SHOW TABLES", should_retry: true)
|
|
46
|
+
|
|
47
|
+
log_messages = @logs.string.split("\n")
|
|
48
|
+
assert_equal(2, log_messages.length)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "#update should be retried" do
|
|
52
|
+
ar_connection = mock()
|
|
53
|
+
ar_connection.stubs(:update).raises(LOCK_WAIT)
|
|
54
|
+
.then.raises(LOCK_WAIT)
|
|
55
|
+
.then.returns(1)
|
|
56
|
+
ar_connection.stubs(:active?).returns(true)
|
|
57
|
+
|
|
58
|
+
connection = Lhm::Connection.new(connection: ar_connection, options: {
|
|
59
|
+
retriable: {
|
|
60
|
+
base_interval: 0,
|
|
61
|
+
tries: 3
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
val = connection.update("SHOW TABLES", should_retry: true)
|
|
66
|
+
|
|
67
|
+
log_messages = @logs.string.split("\n")
|
|
68
|
+
assert_equal val, 1
|
|
69
|
+
assert_equal(2, log_messages.length)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "#select_value should be retried" do
|
|
73
|
+
ar_connection = mock()
|
|
74
|
+
ar_connection.stubs(:select_value).raises(LOCK_WAIT)
|
|
75
|
+
.then.raises(LOCK_WAIT)
|
|
76
|
+
.then.returns("dummy")
|
|
77
|
+
ar_connection.stubs(:active?).returns(true)
|
|
78
|
+
|
|
79
|
+
connection = Lhm::Connection.new(connection: ar_connection, options: {
|
|
80
|
+
retriable: {
|
|
81
|
+
base_interval: 0,
|
|
82
|
+
tries: 3
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
val = connection.select_value("SHOW TABLES", should_retry: true)
|
|
87
|
+
|
|
88
|
+
log_messages = @logs.string.split("\n")
|
|
89
|
+
assert_equal val, "dummy"
|
|
90
|
+
assert_equal(2, log_messages.length)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "Queries should be tagged with ProxySQL tag if reconnect_with_consistent_host is enabled" do
|
|
94
|
+
ar_connection = mock()
|
|
95
|
+
ar_connection.expects(:public_send).with(:select_value, "SHOW TABLES #{Lhm::ProxySQLHelper::ANNOTATION}").returns("dummy")
|
|
96
|
+
ar_connection.stubs(:execute).times(4).returns([["dummy"]])
|
|
97
|
+
ar_connection.stubs(:active?).returns(true)
|
|
98
|
+
|
|
99
|
+
connection = Lhm::Connection.new(connection: ar_connection, options: {
|
|
100
|
+
reconnect_with_consistent_host: true,
|
|
101
|
+
retriable: {
|
|
102
|
+
base_interval: 0,
|
|
103
|
+
tries: 3
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
val = connection.select_value("SHOW TABLES", should_retry: true)
|
|
108
|
+
|
|
109
|
+
assert_equal val, "dummy"
|
|
110
|
+
end
|
|
111
|
+
end
|