lhm-teak 3.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +43 -0
  3. data/.gitignore +12 -0
  4. data/.rubocop.yml +183 -0
  5. data/.travis.yml +21 -0
  6. data/Appraisals +24 -0
  7. data/CHANGELOG.md +254 -0
  8. data/Gemfile +5 -0
  9. data/Gemfile.lock +67 -0
  10. data/LICENSE +27 -0
  11. data/README.md +335 -0
  12. data/Rakefile +33 -0
  13. data/dev.yml +45 -0
  14. data/docker-compose.yml +60 -0
  15. data/gemfiles/activerecord_5.2.gemfile +9 -0
  16. data/gemfiles/activerecord_5.2.gemfile.lock +66 -0
  17. data/gemfiles/activerecord_6.0.gemfile +7 -0
  18. data/gemfiles/activerecord_6.0.gemfile.lock +68 -0
  19. data/gemfiles/activerecord_6.1.gemfile +7 -0
  20. data/gemfiles/activerecord_6.1.gemfile.lock +67 -0
  21. data/gemfiles/activerecord_7.0.0.alpha2.gemfile +7 -0
  22. data/gemfiles/activerecord_7.0.0.alpha2.gemfile.lock +65 -0
  23. data/lhm.gemspec +38 -0
  24. data/lib/lhm/atomic_switcher.rb +46 -0
  25. data/lib/lhm/chunk_finder.rb +62 -0
  26. data/lib/lhm/chunk_insert.rb +61 -0
  27. data/lib/lhm/chunker.rb +95 -0
  28. data/lib/lhm/cleanup/current.rb +71 -0
  29. data/lib/lhm/command.rb +48 -0
  30. data/lib/lhm/connection.rb +108 -0
  31. data/lib/lhm/entangler.rb +112 -0
  32. data/lib/lhm/intersection.rb +51 -0
  33. data/lib/lhm/invoker.rb +100 -0
  34. data/lib/lhm/locked_switcher.rb +76 -0
  35. data/lib/lhm/migration.rb +51 -0
  36. data/lib/lhm/migrator.rb +244 -0
  37. data/lib/lhm/printer.rb +63 -0
  38. data/lib/lhm/proxysql_helper.rb +10 -0
  39. data/lib/lhm/railtie.rb +9 -0
  40. data/lib/lhm/sql_helper.rb +77 -0
  41. data/lib/lhm/sql_retry.rb +180 -0
  42. data/lib/lhm/table.rb +121 -0
  43. data/lib/lhm/table_name.rb +23 -0
  44. data/lib/lhm/test_support.rb +35 -0
  45. data/lib/lhm/throttler/slave_lag.rb +162 -0
  46. data/lib/lhm/throttler/threads_running.rb +53 -0
  47. data/lib/lhm/throttler/time.rb +29 -0
  48. data/lib/lhm/throttler.rb +36 -0
  49. data/lib/lhm/timestamp.rb +11 -0
  50. data/lib/lhm/version.rb +6 -0
  51. data/lib/lhm-shopify.rb +1 -0
  52. data/lib/lhm.rb +156 -0
  53. data/scripts/helpers/wait-for-dbs.sh +21 -0
  54. data/scripts/mysql/reader/create_replication.sql +10 -0
  55. data/scripts/mysql/writer/create_test_db.sql +1 -0
  56. data/scripts/mysql/writer/create_users.sql +6 -0
  57. data/scripts/proxysql/proxysql.cnf +117 -0
  58. data/shipit.rubygems.yml +0 -0
  59. data/spec/.lhm.example +4 -0
  60. data/spec/README.md +58 -0
  61. data/spec/fixtures/bigint_table.ddl +4 -0
  62. data/spec/fixtures/composite_primary_key.ddl +6 -0
  63. data/spec/fixtures/composite_primary_key_dest.ddl +6 -0
  64. data/spec/fixtures/custom_primary_key.ddl +6 -0
  65. data/spec/fixtures/custom_primary_key_dest.ddl +6 -0
  66. data/spec/fixtures/destination.ddl +6 -0
  67. data/spec/fixtures/lines.ddl +7 -0
  68. data/spec/fixtures/origin.ddl +6 -0
  69. data/spec/fixtures/permissions.ddl +5 -0
  70. data/spec/fixtures/small_table.ddl +4 -0
  71. data/spec/fixtures/tracks.ddl +5 -0
  72. data/spec/fixtures/users.ddl +14 -0
  73. data/spec/fixtures/wo_id_int_column.ddl +6 -0
  74. data/spec/integration/atomic_switcher_spec.rb +129 -0
  75. data/spec/integration/chunk_insert_spec.rb +30 -0
  76. data/spec/integration/chunker_spec.rb +269 -0
  77. data/spec/integration/cleanup_spec.rb +147 -0
  78. data/spec/integration/database.yml +25 -0
  79. data/spec/integration/entangler_spec.rb +68 -0
  80. data/spec/integration/integration_helper.rb +252 -0
  81. data/spec/integration/invoker_spec.rb +33 -0
  82. data/spec/integration/lhm_spec.rb +659 -0
  83. data/spec/integration/lock_wait_timeout_spec.rb +30 -0
  84. data/spec/integration/locked_switcher_spec.rb +50 -0
  85. data/spec/integration/proxysql_spec.rb +34 -0
  86. data/spec/integration/sql_retry/db_connection_helper.rb +52 -0
  87. data/spec/integration/sql_retry/lock_wait_spec.rb +127 -0
  88. data/spec/integration/sql_retry/lock_wait_timeout_test_helper.rb +114 -0
  89. data/spec/integration/sql_retry/proxysql_helper.rb +22 -0
  90. data/spec/integration/sql_retry/retry_with_proxysql_spec.rb +109 -0
  91. data/spec/integration/table_spec.rb +83 -0
  92. data/spec/integration/toxiproxy_helper.rb +40 -0
  93. data/spec/test_helper.rb +69 -0
  94. data/spec/unit/atomic_switcher_spec.rb +29 -0
  95. data/spec/unit/chunk_finder_spec.rb +73 -0
  96. data/spec/unit/chunk_insert_spec.rb +67 -0
  97. data/spec/unit/chunker_spec.rb +176 -0
  98. data/spec/unit/connection_spec.rb +111 -0
  99. data/spec/unit/entangler_spec.rb +187 -0
  100. data/spec/unit/intersection_spec.rb +51 -0
  101. data/spec/unit/lhm_spec.rb +46 -0
  102. data/spec/unit/locked_switcher_spec.rb +46 -0
  103. data/spec/unit/migrator_spec.rb +144 -0
  104. data/spec/unit/printer_spec.rb +85 -0
  105. data/spec/unit/sql_helper_spec.rb +28 -0
  106. data/spec/unit/table_name_spec.rb +39 -0
  107. data/spec/unit/table_spec.rb +47 -0
  108. data/spec/unit/throttler/slave_lag_spec.rb +322 -0
  109. data/spec/unit/throttler/threads_running_spec.rb +64 -0
  110. data/spec/unit/throttler_spec.rb +124 -0
  111. data/spec/unit/unit_helper.rb +26 -0
  112. metadata +366 -0
@@ -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