lhm-shopify 3.4.0 → 3.5.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +24 -15
  3. data/.gitignore +1 -6
  4. data/Appraisals +24 -0
  5. data/CHANGELOG.md +30 -0
  6. data/Gemfile.lock +66 -0
  7. data/README.md +55 -4
  8. data/Rakefile +11 -0
  9. data/dev.yml +31 -6
  10. data/docker-compose.yml +58 -0
  11. data/gemfiles/activerecord_5.2.gemfile +9 -0
  12. data/gemfiles/activerecord_5.2.gemfile.lock +65 -0
  13. data/gemfiles/activerecord_6.0.gemfile +7 -0
  14. data/gemfiles/activerecord_6.0.gemfile.lock +67 -0
  15. data/gemfiles/activerecord_6.1.gemfile +7 -0
  16. data/gemfiles/activerecord_6.1.gemfile.lock +66 -0
  17. data/gemfiles/activerecord_7.0.0.alpha2.gemfile +7 -0
  18. data/gemfiles/activerecord_7.0.0.alpha2.gemfile.lock +64 -0
  19. data/lhm.gemspec +7 -3
  20. data/lib/lhm/atomic_switcher.rb +5 -11
  21. data/lib/lhm/chunk_insert.rb +7 -10
  22. data/lib/lhm/chunker.rb +21 -10
  23. data/lib/lhm/cleanup/current.rb +9 -12
  24. data/lib/lhm/connection.rb +108 -0
  25. data/lib/lhm/entangler.rb +8 -13
  26. data/lib/lhm/invoker.rb +6 -4
  27. data/lib/lhm/locked_switcher.rb +2 -0
  28. data/lib/lhm/migrator.rb +2 -0
  29. data/lib/lhm/printer.rb +10 -6
  30. data/lib/lhm/proxysql_helper.rb +10 -0
  31. data/lib/lhm/sql_retry.rb +129 -10
  32. data/lib/lhm/throttler/slave_lag.rb +19 -2
  33. data/lib/lhm/version.rb +1 -1
  34. data/lib/lhm.rb +41 -16
  35. data/scripts/helpers/wait-for-dbs.sh +21 -0
  36. data/scripts/mysql/reader/create_replication.sql +10 -0
  37. data/scripts/mysql/writer/create_test_db.sql +1 -0
  38. data/scripts/mysql/writer/create_users.sql +6 -0
  39. data/scripts/proxysql/proxysql.cnf +117 -0
  40. data/spec/integration/atomic_switcher_spec.rb +53 -17
  41. data/spec/integration/chunk_insert_spec.rb +3 -2
  42. data/spec/integration/chunker_spec.rb +18 -16
  43. data/spec/integration/cleanup_spec.rb +49 -38
  44. data/spec/integration/database.yml +25 -0
  45. data/spec/integration/entangler_spec.rb +7 -5
  46. data/spec/integration/integration_helper.rb +25 -10
  47. data/spec/integration/lhm_spec.rb +114 -40
  48. data/spec/integration/lock_wait_timeout_spec.rb +2 -2
  49. data/spec/integration/locked_switcher_spec.rb +4 -4
  50. data/spec/integration/proxysql_spec.rb +34 -0
  51. data/spec/integration/sql_retry/db_connection_helper.rb +52 -0
  52. data/spec/integration/sql_retry/lock_wait_spec.rb +8 -6
  53. data/spec/integration/sql_retry/lock_wait_timeout_test_helper.rb +17 -4
  54. data/spec/integration/sql_retry/proxysql_helper.rb +22 -0
  55. data/spec/integration/sql_retry/retry_with_proxysql_spec.rb +109 -0
  56. data/spec/integration/table_spec.rb +11 -19
  57. data/spec/integration/toxiproxy_helper.rb +40 -0
  58. data/spec/test_helper.rb +24 -0
  59. data/spec/unit/atomic_switcher_spec.rb +4 -6
  60. data/spec/unit/chunk_insert_spec.rb +7 -2
  61. data/spec/unit/chunker_spec.rb +47 -42
  62. data/spec/unit/connection_spec.rb +111 -0
  63. data/spec/unit/entangler_spec.rb +85 -22
  64. data/spec/unit/intersection_spec.rb +4 -4
  65. data/spec/unit/lhm_spec.rb +23 -6
  66. data/spec/unit/locked_switcher_spec.rb +13 -18
  67. data/spec/unit/migrator_spec.rb +17 -19
  68. data/spec/unit/printer_spec.rb +14 -26
  69. data/spec/unit/sql_helper_spec.rb +8 -12
  70. data/spec/unit/table_spec.rb +5 -5
  71. data/spec/unit/throttler/slave_lag_spec.rb +14 -9
  72. data/spec/unit/throttler_spec.rb +12 -12
  73. data/spec/unit/unit_helper.rb +13 -0
  74. metadata +85 -14
  75. data/bin/.gitkeep +0 -0
  76. data/dbdeployer/config.json +0 -32
  77. data/dbdeployer/install.sh +0 -64
  78. data/gemfiles/ar-2.3_mysql.gemfile +0 -6
  79. data/gemfiles/ar-3.2_mysql.gemfile +0 -5
  80. data/gemfiles/ar-3.2_mysql2.gemfile +0 -5
  81. data/gemfiles/ar-4.0_mysql2.gemfile +0 -5
  82. data/gemfiles/ar-4.1_mysql2.gemfile +0 -5
  83. data/gemfiles/ar-4.2_mysql2.gemfile +0 -5
  84. data/gemfiles/ar-5.0_mysql2.gemfile +0 -5
@@ -0,0 +1,109 @@
1
+ require 'minitest/autorun'
2
+ require 'mysql2'
3
+ require 'lhm'
4
+ require 'toxiproxy'
5
+
6
+ require 'integration/sql_retry/lock_wait_timeout_test_helper'
7
+ require 'integration/sql_retry/db_connection_helper'
8
+ require 'integration/sql_retry/proxysql_helper'
9
+ require 'integration/toxiproxy_helper'
10
+
11
+ describe Lhm::SqlRetry, "ProxiSQL tests for LHM retry" do
12
+ include ToxiproxyHelper
13
+
14
+ before(:each) do
15
+ @old_logger = Lhm.logger
16
+ @logger = StringIO.new
17
+ Lhm.logger = Logger.new(@logger)
18
+
19
+ @connection = DBConnectionHelper::new_mysql_connection(:proxysql, true, true)
20
+
21
+ @lhm_retry = Lhm::SqlRetry.new(@connection, retry_options: {},
22
+ reconnect_with_consistent_host: true)
23
+ end
24
+
25
+ after(:each) do
26
+ # Restore default logger
27
+ Lhm.logger = @old_logger
28
+ end
29
+
30
+ it "Will abort if service is down" do
31
+
32
+ e = assert_raises Lhm::Error do
33
+ #Service down
34
+ Toxiproxy[:mysql_proxysql].down do
35
+ @lhm_retry.with_retries do |retriable_connection|
36
+ retriable_connection.execute("INSERT INTO #{DBConnectionHelper.test_table_name} (id) VALUES (2000)")
37
+ end
38
+ end
39
+ end
40
+ assert_equal Lhm::Error, e.class
41
+ assert_match(/LHM tried the reconnection procedure but failed. Aborting/, e.message)
42
+ end
43
+
44
+ it "Will retry until connection is achieved" do
45
+
46
+ #Creating a network blip
47
+ ToxiproxyHelper.with_kill_and_restart(:mysql_proxysql, 2.seconds) do
48
+ @lhm_retry.with_retries do |retriable_connection|
49
+ retriable_connection.execute("INSERT INTO #{DBConnectionHelper.test_table_name} (id) VALUES (2000)")
50
+ end
51
+ end
52
+
53
+ assert_equal @connection.execute("Select * from #{DBConnectionHelper.test_table_name} WHERE id=2000").to_a.first.first, 2000
54
+
55
+ logs = @logger.string.split("\n")
56
+
57
+ assert logs.first.include?("Lost connection to MySQL, will retry to connect to same host")
58
+ assert logs.last.include?("LHM successfully reconnected to initial host")
59
+ end
60
+
61
+ it "Will abort if new writer is not same host" do
62
+ # The hostname will be constant before the blip
63
+ Lhm::SqlRetry.any_instance.stubs(:hostname).returns("mysql-1").then.returns("mysql-2")
64
+ Lhm::SqlRetry.any_instance.stubs(:server_id).returns(1).then.returns(2)
65
+
66
+ # Need new instance for stub to take into effect
67
+ lhm_retry = Lhm::SqlRetry.new(@connection, retry_options: {},
68
+ reconnect_with_consistent_host: true)
69
+
70
+ e = assert_raises Lhm::Error do
71
+ #Creating a network blip
72
+ ToxiproxyHelper.with_kill_and_restart(:mysql_proxysql, 2.seconds) do
73
+ lhm_retry.with_retries do |retriable_connection|
74
+ retriable_connection.execute("INSERT INTO #{DBConnectionHelper.test_table_name} (id) VALUES (2000)")
75
+ end
76
+ end
77
+ end
78
+
79
+ assert_equal e.class, Lhm::Error
80
+ assert_match(/LHM tried the reconnection procedure but failed. Aborting/, e.message)
81
+
82
+ logs = @logger.string.split("\n")
83
+
84
+ assert logs.first.include?("Lost connection to MySQL, will retry to connect to same host")
85
+ assert logs.last.include?("Reconnected to wrong host. Started migration on: mysql-1 (server_id: 1), but reconnected to: mysql-2 (server_id: 2).")
86
+ end
87
+
88
+ it "Will abort if failover happens (mimicked with proxySQL)" do
89
+ e = assert_raises Lhm::Error do
90
+ #Creates a failover by switching the target hostgroup for the #hostname
91
+ ProxySQLHelper.with_lhm_hostgroup_flip do
92
+ #Creating a network blip
93
+ ToxiproxyHelper.with_kill_and_restart(:mysql_proxysql, 2.seconds) do
94
+ @lhm_retry.with_retries do |retriable_connection|
95
+ retriable_connection.execute("INSERT INTO #{DBConnectionHelper.test_table_name} (id) VALUES (2000)")
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ assert_equal e.class, Lhm::Error
102
+ assert_match(/LHM tried the reconnection procedure but failed. Aborting/, e.message)
103
+
104
+ logs = @logger.string.split("\n")
105
+
106
+ assert logs.first.include?("Lost connection to MySQL, will retry to connect to same host")
107
+ assert logs.last.include?("Reconnected to wrong host. Started migration on: mysql-1 (server_id: 1), but reconnected to: mysql-2 (server_id: 2).")
108
+ end
109
+ end
@@ -15,28 +15,24 @@ describe Lhm::Table do
15
15
  end
16
16
 
17
17
  it 'should parse primary key' do
18
- @table.pk.must_equal('pk')
18
+ value(@table.pk).must_equal('pk')
19
19
  end
20
20
 
21
21
  it 'should parse indices' do
22
- @table.
23
- indices['index_custom_primary_key_on_id'].
24
- must_equal(['id'])
22
+ value(@table.indices['index_custom_primary_key_on_id']).must_equal(['id'])
25
23
  end
26
24
 
27
25
  it 'should parse columns' do
28
- @table.
29
- columns['id'][:type].
30
- must_match(/(bigint|int)\(\d+\)/)
26
+ value(@table.columns['id'][:type]).must_match(/(bigint|int)\(\d+\)/)
31
27
  end
32
28
 
33
29
  it 'should return true for method that should be renamed' do
34
- @table.satisfies_id_column_requirement?.must_equal true
30
+ value(@table.satisfies_id_column_requirement?).must_equal true
35
31
  end
36
32
 
37
33
  it 'should support bigint tables' do
38
34
  @table = table_create(:bigint_table)
39
- @table.satisfies_id_column_requirement?.must_equal true
35
+ value(@table.satisfies_id_column_requirement?).must_equal true
40
36
  end
41
37
  end
42
38
 
@@ -47,7 +43,7 @@ describe Lhm::Table do
47
43
 
48
44
  it 'should return false for a non-int id column' do
49
45
  @table = table_create(:wo_id_int_column)
50
- @table.satisfies_id_column_requirement?.must_equal false
46
+ value(@table.satisfies_id_column_requirement?).must_equal false
51
47
  end
52
48
  end
53
49
  end
@@ -60,15 +56,15 @@ describe Lhm::Table do
60
56
  end
61
57
 
62
58
  it 'should parse table name in show create table' do
63
- @table.name.must_equal('users')
59
+ value(@table.name).must_equal('users')
64
60
  end
65
61
 
66
62
  it 'should parse primary key' do
67
- @table.pk.must_equal('id')
63
+ value(@table.pk).must_equal('id')
68
64
  end
69
65
 
70
66
  it 'should parse column type in show create table' do
71
- @table.columns['username'][:type].must_equal('varchar(255)')
67
+ value(@table.columns['username'][:type]).must_equal('varchar(255)')
72
68
  end
73
69
 
74
70
  it 'should parse column metadata' do
@@ -76,15 +72,11 @@ describe Lhm::Table do
76
72
  end
77
73
 
78
74
  it 'should parse indices' do
79
- @table.
80
- indices['index_users_on_username_and_created_at'].
81
- must_equal(['username', 'created_at'])
75
+ value(@table.indices['index_users_on_username_and_created_at']).must_equal(['username', 'created_at'])
82
76
  end
83
77
 
84
78
  it 'should parse index' do
85
- @table.
86
- indices['index_users_on_reference'].
87
- must_equal(['reference'])
79
+ value(@table.indices['index_users_on_reference']).must_equal(['reference'])
88
80
  end
89
81
  end
90
82
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+ require 'toxiproxy'
3
+
4
+ module ToxiproxyHelper
5
+ class << self
6
+
7
+ def included(base)
8
+ Toxiproxy.reset
9
+
10
+ # listen on localhost, but toxiproxy is in a container itself, thus the upstream uses the Docker-Compose DNS
11
+ Toxiproxy.populate(
12
+ [
13
+ {
14
+ name: 'mysql_master',
15
+ listen: '0.0.0.0:22220',
16
+ upstream: 'mysql-1:3306'
17
+ },
18
+ {
19
+ name: 'mysql_proxysql',
20
+ listen: '0.0.0.0:22222',
21
+ upstream: 'proxysql:3306'
22
+ }
23
+ ])
24
+ end
25
+
26
+ def with_kill_and_restart(target, restart_after)
27
+ thread = Thread.new do
28
+ sleep(restart_after) unless restart_after.nil?
29
+ Toxiproxy[target].enable
30
+ end
31
+
32
+ Toxiproxy[target].disable
33
+
34
+ yield
35
+
36
+ ensure
37
+ thread.join
38
+ end
39
+ end
40
+ end
data/spec/test_helper.rb CHANGED
@@ -10,6 +10,8 @@ require 'minitest/autorun'
10
10
  require 'minitest/spec'
11
11
  require 'minitest/mock'
12
12
  require 'mocha/minitest'
13
+ require 'after_do'
14
+ require 'byebug'
13
15
  require 'pathname'
14
16
  require 'lhm'
15
17
 
@@ -17,6 +19,8 @@ $project = Pathname.new(File.dirname(__FILE__) + '/..').cleanpath
17
19
  $spec = $project.join('spec')
18
20
  $fixtures = $spec.join('fixtures')
19
21
 
22
+ $db_name = 'test'
23
+
20
24
  require 'active_record'
21
25
  require 'mysql2'
22
26
 
@@ -24,6 +28,9 @@ logger = Logger.new STDOUT
24
28
  logger.level = Logger::WARN
25
29
  Lhm.logger = logger
26
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
+
27
34
  def without_verbose(&block)
28
35
  old_verbose, $VERBOSE = $VERBOSE, nil
29
36
  yield
@@ -43,3 +50,20 @@ end
43
50
  def throttler
44
51
  Lhm::Throttler::Time.new(:stride => 100)
45
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
+
@@ -20,12 +20,10 @@ describe Lhm::AtomicSwitcher do
20
20
 
21
21
  describe 'atomic switch' do
22
22
  it 'should perform a single atomic rename' do
23
- @switcher.
24
- atomic_switch.
25
- must_equal(
26
- "rename table `origin` to `#{ @migration.archive_name }`, " \
27
- '`destination` to `origin`'
28
- )
23
+ value(@switcher.atomic_switch).must_equal(
24
+ "rename table `origin` to `#{ @migration.archive_name }`, " \
25
+ '`destination` to `origin`'
26
+ )
29
27
  end
30
28
  end
31
29
  end
@@ -4,17 +4,22 @@
4
4
  require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
5
5
 
6
6
  require 'lhm/chunk_insert'
7
+ require 'lhm/connection'
7
8
 
8
9
  describe Lhm::ChunkInsert do
9
10
  before(:each) do
10
- @connection = stub(:connection)
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})
11
14
  @origin = Lhm::Table.new('foo')
12
15
  @destination = Lhm::Table.new('bar')
13
16
  end
14
17
 
15
18
  describe "#sql" do
16
19
  describe "when migration has no conditions" do
17
- before { @migration = Lhm::Migration.new(@origin, @destination) }
20
+ before do
21
+ @migration = Lhm::Migration.new(@origin, @destination)
22
+ end
18
23
 
19
24
  it "uses a simple where clause" do
20
25
  assert_equal(
@@ -7,15 +7,20 @@ require 'lhm/table'
7
7
  require 'lhm/migration'
8
8
  require 'lhm/chunker'
9
9
  require 'lhm/throttler'
10
+ require 'lhm/connection'
10
11
 
11
12
  describe Lhm::Chunker do
12
13
  include UnitHelper
13
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
+
14
18
  before(:each) do
15
19
  @origin = Lhm::Table.new('foo')
16
20
  @destination = Lhm::Table.new('bar')
17
21
  @migration = Lhm::Migration.new(@origin, @destination)
18
22
  @connection = mock()
23
+ @connection.stubs(:execute).returns([["dummy"]])
19
24
  # This is a poor man's stub
20
25
  @throttler = Object.new
21
26
  def @throttler.run
@@ -37,11 +42,11 @@ describe Lhm::Chunker do
37
42
  5
38
43
  end
39
44
 
40
- @connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 4/)).returns(7)
41
- @connection.expects(:select_value).with(regexp_matches(/where id >= 8 order by id limit 1 offset 4/)).returns(21)
42
- @connection.expects(:update).with(regexp_matches(/between 1 and 7/)).returns(2)
43
- @connection.expects(:update).with(regexp_matches(/between 8 and 10/)).returns(2)
44
- @connection.expects(:query).twice.with(regexp_matches(/show warnings/)).returns([])
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([])
45
50
 
46
51
  @chunker.run
47
52
  end
@@ -52,17 +57,17 @@ describe Lhm::Chunker do
52
57
  2
53
58
  end
54
59
 
55
- @connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/)).returns(2)
56
- @connection.expects(:select_value).with(regexp_matches(/where id >= 3 order by id limit 1 offset 1/)).returns(4)
57
- @connection.expects(:select_value).with(regexp_matches(/where id >= 5 order by id limit 1 offset 1/)).returns(6)
58
- @connection.expects(:select_value).with(regexp_matches(/where id >= 7 order by id limit 1 offset 1/)).returns(8)
59
- @connection.expects(:select_value).with(regexp_matches(/where id >= 9 order by id limit 1 offset 1/)).returns(10)
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)
60
65
 
61
- @connection.expects(:update).with(regexp_matches(/between 1 and 2/)).returns(2)
62
- @connection.expects(:update).with(regexp_matches(/between 3 and 4/)).returns(2)
63
- @connection.expects(:update).with(regexp_matches(/between 5 and 6/)).returns(2)
64
- @connection.expects(:update).with(regexp_matches(/between 7 and 8/)).returns(2)
65
- @connection.expects(:update).with(regexp_matches(/between 9 and 10/)).returns(2)
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)
66
71
 
67
72
  @chunker.run
68
73
  end
@@ -79,17 +84,17 @@ describe Lhm::Chunker do
79
84
  end
80
85
  end
81
86
 
82
- @connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/)).returns(2)
83
- @connection.expects(:select_value).with(regexp_matches(/where id >= 3 order by id limit 1 offset 2/)).returns(5)
84
- @connection.expects(:select_value).with(regexp_matches(/where id >= 6 order by id limit 1 offset 2/)).returns(8)
85
- @connection.expects(:select_value).with(regexp_matches(/where id >= 9 order by id limit 1 offset 2/)).returns(nil)
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)
86
91
 
87
- @connection.expects(:update).with(regexp_matches(/between 1 and 2/)).returns(2)
88
- @connection.expects(:update).with(regexp_matches(/between 3 and 5/)).returns(2)
89
- @connection.expects(:update).with(regexp_matches(/between 6 and 8/)).returns(2)
90
- @connection.expects(:update).with(regexp_matches(/between 9 and 10/)).returns(2)
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)
91
96
 
92
- @connection.expects(:query).twice.with(regexp_matches(/show warnings/)).returns([])
97
+ @connection.expects(:execute).twice.with(regexp_matches(/show warnings/),EXPECTED_RETRY_FLAGS_CHUNKER).returns([])
93
98
 
94
99
  @chunker.run
95
100
  end
@@ -99,8 +104,8 @@ describe Lhm::Chunker do
99
104
  :start => 1,
100
105
  :limit => 1)
101
106
 
102
- @connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 0/)).returns(nil)
103
- @connection.expects(:update).with(regexp_matches(/between 1 and 1/)).returns(1)
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)
104
109
 
105
110
  @chunker.run
106
111
  end
@@ -113,17 +118,17 @@ describe Lhm::Chunker do
113
118
  2
114
119
  end
115
120
 
116
- @connection.expects(:select_value).with(regexp_matches(/where id >= 2 order by id limit 1 offset 1/)).returns(3)
117
- @connection.expects(:select_value).with(regexp_matches(/where id >= 4 order by id limit 1 offset 1/)).returns(5)
118
- @connection.expects(:select_value).with(regexp_matches(/where id >= 6 order by id limit 1 offset 1/)).returns(7)
119
- @connection.expects(:select_value).with(regexp_matches(/where id >= 8 order by id limit 1 offset 1/)).returns(9)
120
- @connection.expects(:select_value).with(regexp_matches(/where id >= 10 order by id limit 1 offset 1/)).returns(nil)
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)
121
126
 
122
- @connection.expects(:update).with(regexp_matches(/between 2 and 3/)).returns(2)
123
- @connection.expects(:update).with(regexp_matches(/between 4 and 5/)).returns(2)
124
- @connection.expects(:update).with(regexp_matches(/between 6 and 7/)).returns(2)
125
- @connection.expects(:update).with(regexp_matches(/between 8 and 9/)).returns(2)
126
- @connection.expects(:update).with(regexp_matches(/between 10 and 10/)).returns(1)
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)
127
132
 
128
133
  @chunker.run
129
134
  end
@@ -137,9 +142,9 @@ describe Lhm::Chunker do
137
142
  2
138
143
  end
139
144
 
140
- @connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/)).returns(2)
141
- @connection.expects(:update).with(regexp_matches(/where \(foo.created_at > '2013-07-10' or foo.baz = 'quux'\) and `foo`/)).returns(1)
142
- @connection.expects(:query).with(regexp_matches(/show warnings/)).returns([])
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([])
143
148
 
144
149
  def @migration.conditions
145
150
  "where foo.created_at > '2013-07-10' or foo.baz = 'quux'"
@@ -157,9 +162,9 @@ describe Lhm::Chunker do
157
162
  2
158
163
  end
159
164
 
160
- @connection.expects(:select_value).with(regexp_matches(/where id >= 1 order by id limit 1 offset 1/)).returns(2)
161
- @connection.expects(:update).with(regexp_matches(/inner join bar on foo.id = bar.foo_id and/)).returns(1)
162
- @connection.expects(:query).with(regexp_matches(/show warnings/)).returns([])
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([])
163
168
 
164
169
  def @migration.conditions
165
170
  'inner join bar on foo.id = bar.foo_id'
@@ -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