lhm-teak 3.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|