lhm-shopify 3.4.0 → 3.5.5

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