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.
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