lhm-shopify 3.4.2 → 3.5.3

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 (68) 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 +15 -0
  6. data/Gemfile.lock +66 -0
  7. data/README.md +53 -4
  8. data/Rakefile +10 -0
  9. data/dev.yml +28 -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 +4 -11
  21. data/lib/lhm/chunk_insert.rb +4 -10
  22. data/lib/lhm/chunker.rb +7 -8
  23. data/lib/lhm/cleanup/current.rb +3 -10
  24. data/lib/lhm/connection.rb +109 -0
  25. data/lib/lhm/entangler.rb +4 -12
  26. data/lib/lhm/invoker.rb +3 -3
  27. data/lib/lhm/proxysql_helper.rb +10 -0
  28. data/lib/lhm/sql_retry.rb +132 -9
  29. data/lib/lhm/throttler/slave_lag.rb +19 -2
  30. data/lib/lhm/version.rb +1 -1
  31. data/lib/lhm.rb +25 -9
  32. data/scripts/helpers/wait-for-dbs.sh +21 -0
  33. data/scripts/mysql/reader/create_replication.sql +10 -0
  34. data/scripts/mysql/writer/create_test_db.sql +1 -0
  35. data/scripts/mysql/writer/create_users.sql +6 -0
  36. data/scripts/proxysql/proxysql.cnf +117 -0
  37. data/spec/integration/atomic_switcher_spec.rb +38 -13
  38. data/spec/integration/chunk_insert_spec.rb +2 -1
  39. data/spec/integration/chunker_spec.rb +1 -1
  40. data/spec/integration/database.yml +25 -0
  41. data/spec/integration/entangler_spec.rb +3 -1
  42. data/spec/integration/integration_helper.rb +24 -9
  43. data/spec/integration/lhm_spec.rb +75 -0
  44. data/spec/integration/proxysql_spec.rb +34 -0
  45. data/spec/integration/sql_retry/db_connection_helper.rb +52 -0
  46. data/spec/integration/sql_retry/lock_wait_spec.rb +8 -6
  47. data/spec/integration/sql_retry/lock_wait_timeout_test_helper.rb +17 -4
  48. data/spec/integration/sql_retry/proxysql_helper.rb +22 -0
  49. data/spec/integration/sql_retry/retry_with_proxysql_spec.rb +108 -0
  50. data/spec/integration/toxiproxy_helper.rb +40 -0
  51. data/spec/test_helper.rb +21 -0
  52. data/spec/unit/chunk_insert_spec.rb +7 -2
  53. data/spec/unit/chunker_spec.rb +46 -42
  54. data/spec/unit/connection_spec.rb +86 -0
  55. data/spec/unit/entangler_spec.rb +51 -10
  56. data/spec/unit/lhm_spec.rb +17 -0
  57. data/spec/unit/throttler/slave_lag_spec.rb +13 -8
  58. metadata +85 -14
  59. data/bin/.gitkeep +0 -0
  60. data/dbdeployer/config.json +0 -32
  61. data/dbdeployer/install.sh +0 -64
  62. data/gemfiles/ar-2.3_mysql.gemfile +0 -6
  63. data/gemfiles/ar-3.2_mysql.gemfile +0 -5
  64. data/gemfiles/ar-3.2_mysql2.gemfile +0 -5
  65. data/gemfiles/ar-4.0_mysql2.gemfile +0 -5
  66. data/gemfiles/ar-4.1_mysql2.gemfile +0 -5
  67. data/gemfiles/ar-4.2_mysql2.gemfile +0 -5
  68. data/gemfiles/ar-5.0_mysql2.gemfile +0 -5
@@ -0,0 +1,6 @@
1
+ # Creates replication user in Writer
2
+ CREATE USER IF NOT EXISTS 'writer'@'%' IDENTIFIED BY 'password';
3
+ CREATE USER IF NOT EXISTS 'reader'@'%' IDENTIFIED BY 'password';
4
+
5
+ CREATE USER IF NOT EXISTS 'replication'@'%' IDENTIFIED BY 'password';
6
+ GRANT REPLICATION SLAVE ON *.* TO' replication'@'%' IDENTIFIED BY 'password';
@@ -0,0 +1,117 @@
1
+ #file proxysql.cfg
2
+
3
+ datadir="/var/lib/proxysql"
4
+ restart_on_missing_heartbeats=999999
5
+ query_parser_token_delimiters=","
6
+ query_parser_key_value_delimiters=":"
7
+ unit_of_work_identifiers="consistent_read_id"
8
+
9
+ admin_variables=
10
+ {
11
+ mysql_ifaces="0.0.0.0:6032"
12
+ admin_credentials="admin:password;remote-admin:password"
13
+ }
14
+
15
+ mysql_servers =
16
+ (
17
+ {
18
+ address="mysql-1"
19
+ port=3306
20
+ hostgroup=0
21
+ max_connections=200
22
+ },
23
+ {
24
+ address="mysql-2"
25
+ port=3306
26
+ hostgroup=1
27
+ max_connections=200
28
+ }
29
+ )
30
+
31
+ mysql_variables=
32
+ {
33
+ session_idle_ms=1
34
+ auto_increment_delay_multiplex=0
35
+
36
+ threads=8
37
+ max_connections=100000
38
+ interfaces="0.0.0.0:3306"
39
+ server_version="5.7.18-proxysql"
40
+ connect_timeout_server=10000
41
+ connect_timeout_server_max=10000
42
+ connect_retries_on_failure=0
43
+ default_charset="utf8mb4"
44
+ free_connections_pct=100
45
+ connection_warming=true
46
+ max_allowed_packet=16777216
47
+ monitor_enabled=false
48
+ query_retries_on_failure=0
49
+ shun_on_failures=999999
50
+ shun_recovery_time_sec=0
51
+ kill_backend_connection_when_disconnect=false
52
+ stats_time_backend_query=false
53
+ stats_time_query_processor=false
54
+ max_stmts_per_connection=5
55
+ default_max_latency_ms=999999
56
+ wait_timeout=1800000
57
+ eventslog_format=3
58
+ log_multiplexing_disabled=true
59
+ log_unhealthy_connections=false
60
+ }
61
+
62
+ # defines all the MySQL users
63
+ mysql_users:
64
+ (
65
+ {
66
+ username = "root"
67
+ password = "password"
68
+ default_hostgroup = 0
69
+ max_connections=1000
70
+ active = 1
71
+ },
72
+ {
73
+ username = "writer"
74
+ password = "password"
75
+ default_hostgroup = 0
76
+ max_connections=50000
77
+ active = 1
78
+ transaction_persistent=1
79
+ },
80
+ {
81
+ username = "reader"
82
+ password = "password"
83
+ default_hostgroup = 1
84
+ max_connections=50000
85
+ active = 1
86
+ transaction_persistent=1
87
+ }
88
+ )
89
+
90
+ #defines MySQL Query Rules
91
+ mysql_query_rules:
92
+ (
93
+ {
94
+ rule_id = 1
95
+ active = 1
96
+ match_digest = "@@SESSION"
97
+ multiplex = 2
98
+ },
99
+ {
100
+ rule_id = 2
101
+ active = 1
102
+ match_digest = "@@global\.server_id"
103
+ multiplex = 2
104
+ },
105
+ {
106
+ rule_id = 3
107
+ active = 1
108
+ match_digest = "@@global\.hostname"
109
+ multiplex = 2
110
+ },
111
+ {
112
+ rule_id = 4
113
+ active = 1
114
+ match_pattern = "maintenance:lhm"
115
+ destination_hostgroup = 0
116
+ }
117
+ )
@@ -6,6 +6,7 @@ require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
6
6
  require 'lhm/table'
7
7
  require 'lhm/migration'
8
8
  require 'lhm/atomic_switcher'
9
+ require 'lhm/connection'
9
10
 
10
11
  describe Lhm::AtomicSwitcher do
11
12
  include IntegrationHelper
@@ -15,9 +16,9 @@ describe Lhm::AtomicSwitcher do
15
16
  describe 'switching' do
16
17
  before(:each) do
17
18
  Thread.abort_on_exception = true
18
- @origin = table_create('origin')
19
+ @origin = table_create('origin')
19
20
  @destination = table_create('destination')
20
- @migration = Lhm::Migration.new(@origin, @destination)
21
+ @migration = Lhm::Migration.new(@origin, @destination)
21
22
  @logs = StringIO.new
22
23
  Lhm.logger = Logger.new(@logs)
23
24
  @connection.execute('SET GLOBAL innodb_lock_wait_timeout=3')
@@ -29,26 +30,48 @@ describe Lhm::AtomicSwitcher do
29
30
  end
30
31
 
31
32
  it 'should retry and log on lock wait timeouts' do
32
- connection = mock()
33
- connection.stubs(:data_source_exists?).returns(true)
34
- connection.stubs(:execute).raises(ActiveRecord::StatementInvalid, 'Lock wait timeout exceeded; try restarting transaction.').then.returns(true)
33
+ ar_connection = mock()
34
+ ar_connection.stubs(:data_source_exists?).returns(true)
35
+ ar_connection.stubs(:active?).returns(true)
36
+ ar_connection.stubs(:execute).returns([["dummy"]], [["dummy"]], [["dummy"]])
37
+ .then
38
+ .raises(ActiveRecord::StatementInvalid, 'Lock wait timeout exceeded; try restarting transaction.')
39
+ .then
40
+ .returns([["dummy"]]) # Matches initial host -> triggers retry
35
41
 
36
- switcher = Lhm::AtomicSwitcher.new(@migration, connection, retriable: {base_interval: 0})
42
+ connection = Lhm::Connection.new(connection: ar_connection, options: {reconnect_with_consistent_host: true})
43
+
44
+
45
+ switcher = Lhm::AtomicSwitcher.new(@migration, connection, retriable: { tries: 3, base_interval: 0 })
37
46
 
38
47
  assert switcher.run
39
48
 
40
49
  log_messages = @logs.string.split("\n")
41
50
  assert_equal(2, log_messages.length)
42
51
  assert log_messages[0].include? "Starting run of class=Lhm::AtomicSwitcher"
52
+ # On failure of this assertion, check for Lhm::Connection#file
43
53
  assert log_messages[1].include? "[AtomicSwitcher] ActiveRecord::StatementInvalid: 'Lock wait timeout exceeded; try restarting transaction.' - 1 tries"
44
54
  end
45
55
 
46
56
  it 'should give up on lock wait timeouts after a configured number of tries' do
47
- connection = mock()
48
- connection.stubs(:data_source_exists?).returns(true)
49
- connection.stubs(:execute).twice.raises(ActiveRecord::StatementInvalid, 'Lock wait timeout exceeded; try restarting transaction.')
50
-
51
- switcher = Lhm::AtomicSwitcher.new(@migration, connection, retriable: {tries: 2, base_interval: 0})
57
+ ar_connection = mock()
58
+ ar_connection.stubs(:data_source_exists?).returns(true)
59
+ ar_connection.stubs(:active?).returns(true)
60
+ ar_connection.stubs(:execute).returns([["dummy"]], [["dummy"]], [["dummy"]])
61
+ .then
62
+ .raises(ActiveRecord::StatementInvalid, 'Lock wait timeout exceeded; try restarting transaction.')
63
+ .then
64
+ .returns([["dummy"]]) # triggers retry 1
65
+ .then
66
+ .raises(ActiveRecord::StatementInvalid, 'Lock wait timeout exceeded; try restarting transaction.')
67
+ .then
68
+ .returns([["dummy"]]) # triggers retry 2
69
+ .then
70
+ .raises(ActiveRecord::StatementInvalid, 'Lock wait timeout exceeded; try restarting transaction.') # triggers retry 2
71
+
72
+ connection = Lhm::Connection.new(connection: ar_connection, options: {reconnect_with_consistent_host: true})
73
+
74
+ switcher = Lhm::AtomicSwitcher.new(@migration, connection, retriable: { tries: 2, base_interval: 0 })
52
75
 
53
76
  assert_raises(ActiveRecord::StatementInvalid) { switcher.run }
54
77
  end
@@ -62,8 +85,10 @@ describe Lhm::AtomicSwitcher do
62
85
  end
63
86
 
64
87
  it "should raise when destination doesn't exist" do
65
- connection = mock()
66
- connection.stubs(:data_source_exists?).returns(false)
88
+ ar_connection = mock()
89
+ ar_connection.stubs(:data_source_exists?).returns(false)
90
+
91
+ connection = Lhm::Connection.new(connection: ar_connection)
67
92
 
68
93
  switcher = Lhm::AtomicSwitcher.new(@migration, connection)
69
94
 
@@ -11,7 +11,8 @@ describe Lhm::ChunkInsert do
11
11
  @destination = table_create(:destination)
12
12
  @migration = Lhm::Migration.new(@origin, @destination)
13
13
  execute("insert into origin set id = 1001")
14
- @instance = Lhm::ChunkInsert.new(@migration, connection, 1001, 1001)
14
+ @connection = Lhm::Connection.new(connection: connection)
15
+ @instance = Lhm::ChunkInsert.new(@migration, @connection, 1001, 1001)
15
16
  end
16
17
 
17
18
  it "returns the count" do
@@ -223,7 +223,7 @@ describe Lhm::Chunker do
223
223
 
224
224
  if master_slave_mode?
225
225
  def throttler.slave_connection(slave)
226
- config = ActiveRecord::Base.connection_pool.spec.config.dup
226
+ config = ActiveRecord::Base.connection_pool.db_config.configuration_hash.dup
227
227
  config[:host] = slave
228
228
  config[:port] = 3307
229
229
  ActiveRecord::Base.send('mysql2_connection', config)
@@ -0,0 +1,25 @@
1
+ master:
2
+ host: mysql-1
3
+ user: root
4
+ password: password
5
+ port: 33006
6
+ slave:
7
+ host: mysql-2
8
+ user: root
9
+ password: password
10
+ port: 33007
11
+ proxysql:
12
+ host: proxysql
13
+ user: root
14
+ password: password
15
+ port: 33005
16
+ master_toxic:
17
+ host: toxiproxy
18
+ user: root
19
+ password: password
20
+ port: 22220
21
+ proxysql_toxic:
22
+ host: toxiproxy
23
+ user: root
24
+ password: password
25
+ port: 22222
@@ -6,6 +6,7 @@ require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
6
6
  require 'lhm/table'
7
7
  require 'lhm/migration'
8
8
  require 'lhm/entangler'
9
+ require 'lhm/connection'
9
10
 
10
11
  describe Lhm::Entangler do
11
12
  include IntegrationHelper
@@ -17,7 +18,8 @@ describe Lhm::Entangler do
17
18
  @origin = table_create('origin')
18
19
  @destination = table_create('destination')
19
20
  @migration = Lhm::Migration.new(@origin, @destination)
20
- @entangler = Lhm::Entangler.new(@migration, connection)
21
+ @connection = Lhm::Connection.new(connection: connection)
22
+ @entangler = Lhm::Entangler.new(@migration, @connection)
21
23
  end
22
24
 
23
25
  it 'should replay inserts from origin into destination' do
@@ -35,13 +35,21 @@ module IntegrationHelper
35
35
  @connection
36
36
  end
37
37
 
38
+ def connect_proxysql!
39
+ connect!(
40
+ '127.0.0.1',
41
+ $db_config['proxysql']['port'],
42
+ $db_config['proxysql']['user'],
43
+ $db_config['proxysql']['password'],
44
+ )
45
+ end
46
+
38
47
  def connect_master!
39
48
  connect!(
40
49
  '127.0.0.1',
41
50
  $db_config['master']['port'],
42
51
  $db_config['master']['user'],
43
52
  $db_config['master']['password'],
44
- $db_config['master']['socket']
45
53
  )
46
54
  end
47
55
 
@@ -51,28 +59,35 @@ module IntegrationHelper
51
59
  $db_config['slave']['port'],
52
60
  $db_config['slave']['user'],
53
61
  $db_config['slave']['password'],
54
- $db_config['slave']['socket']
55
62
  )
56
63
  end
57
64
 
58
- def connect!(hostname, port, user, password, socket)
59
- adapter = ar_conn(hostname, port, user, password, socket)
60
- Lhm.setup(adapter)
65
+ def connect_master_with_toxiproxy!(with_retry: false)
66
+ connect!(
67
+ '127.0.0.1',
68
+ $db_config['master_toxic']['port'],
69
+ $db_config['master_toxic']['user'],
70
+ $db_config['master_toxic']['password'],
71
+ with_retry)
72
+ end
73
+
74
+ def connect!(hostname, port, user, password, with_retry = false)
75
+ adapter = ar_conn(hostname, port, user, password)
76
+ Lhm.setup(adapter,{reconnect_with_consistent_host: with_retry})
61
77
  unless defined?(@@cleaned_up)
62
78
  Lhm.cleanup(true)
63
79
  @@cleaned_up = true
64
80
  end
65
- @connection = adapter
81
+ @connection = Lhm.connection
66
82
  end
67
83
 
68
- def ar_conn(host, port, user, password, socket)
84
+ def ar_conn(host, port, user, password)
69
85
  ActiveRecord::Base.establish_connection(
70
86
  :adapter => 'mysql2',
71
87
  :host => host,
72
88
  :username => user,
73
89
  :port => port,
74
90
  :password => password,
75
- :socket => socket,
76
91
  :database => $db_name
77
92
  )
78
93
  ActiveRecord::Base.connection
@@ -122,7 +137,7 @@ module IntegrationHelper
122
137
  # Helps testing behaviour when another client locks the db
123
138
  def start_locking_thread(lock_for, queue, locking_query)
124
139
  Thread.new do
125
- conn = Mysql2::Client.new(host: '127.0.0.1', database: $db_name, user: 'root', port: 3306)
140
+ conn = new_mysql_connection
126
141
  conn.query('BEGIN')
127
142
  conn.query(locking_query)
128
143
  queue.push(true)
@@ -2,6 +2,7 @@
2
2
  # Schmidt
3
3
 
4
4
  require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
5
+ require 'integration/toxiproxy_helper'
5
6
 
6
7
  describe Lhm do
7
8
  include IntegrationHelper
@@ -581,5 +582,79 @@ describe Lhm do
581
582
  end
582
583
  end
583
584
  end
585
+
586
+ describe 'connection' do
587
+ include ToxiproxyHelper
588
+
589
+ before(:each) do
590
+ @logs = StringIO.new
591
+ Lhm.logger = Logger.new(@logs)
592
+ end
593
+
594
+ it " should not try to reconnect if reconnect_with_consistent_host is not provided" do
595
+ connect_master_with_toxiproxy!(with_retry: false)
596
+
597
+ table_create(:users)
598
+ 100.times { |n| execute("insert into users set reference = '#{ n }'") }
599
+
600
+ assert_raises ActiveRecord::StatementInvalid do
601
+ Toxiproxy[:mysql_master].down do
602
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
603
+ t.ddl("ALTER TABLE #{t.name} CHANGE id id bigint (20) NOT NULL")
604
+ t.ddl("ALTER TABLE #{t.name} DROP PRIMARY KEY, ADD PRIMARY KEY (username, id)")
605
+ t.ddl("ALTER TABLE #{t.name} ADD INDEX (id)")
606
+ t.ddl("ALTER TABLE #{t.name} CHANGE id id bigint (20) NOT NULL AUTO_INCREMENT")
607
+ end
608
+ end
609
+ end
610
+ end
611
+
612
+ it "should reconnect if reconnect_with_consistent_host is true" do
613
+ connect_master_with_toxiproxy!(with_retry: true)
614
+ mysql_disabled = false
615
+
616
+ table_create(:users)
617
+ 100.times { |n| execute("insert into users set reference = '#{ n }'") }
618
+
619
+ # Redeclare Lhm::ChunkInsert to use Hook to disable MySQL writer for 3 seconds before first insert
620
+ Lhm::ChunkInsert.class_eval do
621
+ extend AfterDo
622
+
623
+ before(:insert_and_return_count_of_rows_created) do
624
+ unless mysql_disabled
625
+ mysql_disabled = true
626
+ Thread.new do
627
+ Toxiproxy[:mysql_master].down do
628
+ sleep 3
629
+ end
630
+ end
631
+ end
632
+ end
633
+
634
+ # Need to call `#method_added` manually to have the hooks take into effect
635
+ method_added(:insert_and_return_count_of_rows_created)
636
+ end
637
+
638
+ Lhm.change_table(:users, :atomic_switch => false) do |t|
639
+ t.ddl("ALTER TABLE #{t.name} CHANGE id id bigint (20) NOT NULL")
640
+ t.ddl("ALTER TABLE #{t.name} DROP PRIMARY KEY, ADD PRIMARY KEY (username, id)")
641
+ t.ddl("ALTER TABLE #{t.name} ADD INDEX (id)")
642
+ t.ddl("ALTER TABLE #{t.name} CHANGE id id bigint (20) NOT NULL AUTO_INCREMENT")
643
+ end
644
+
645
+ log_lines = @logs.string.split("\n")
646
+
647
+ assert log_lines.one?{ |line| line.include?("Lost connection to MySQL, will retry to connect to same host")}
648
+ assert log_lines.any?{ |line| line.include?("Lost connection to MySQL server at 'reading initial communication packet'")}
649
+ assert log_lines.one?{ |line| line.include?("LHM successfully reconnected to initial host")}
650
+ assert log_lines.one?{ |line| line.include?("100% complete")}
651
+
652
+ Lhm::ChunkInsert.remove_all_callbacks
653
+
654
+ slave do
655
+ value(count_all(:users)).must_equal(100)
656
+ end
657
+ end
658
+ end
584
659
  end
585
660
  end
@@ -0,0 +1,34 @@
1
+ describe "ProxySQL integration" do
2
+ it "Should contact the writer" do
3
+ conn = Mysql2::Client.new(
4
+ host: '127.0.0.1',
5
+ username: "writer",
6
+ password: "password",
7
+ port: "33005",
8
+ )
9
+
10
+ assert_equal conn.query("SELECT @@global.hostname as host").each.first["host"], "mysql-1"
11
+ end
12
+
13
+ it "Should contact the reader" do
14
+ conn = Mysql2::Client.new(
15
+ host: '127.0.0.1',
16
+ username: "reader",
17
+ password: "password",
18
+ port: "33005",
19
+ )
20
+
21
+ assert_equal conn.query("SELECT @@global.hostname as host").each.first["host"], "mysql-2"
22
+ end
23
+
24
+ it "Should override default hostgroup from user if rule matches" do
25
+ conn = Mysql2::Client.new(
26
+ host: '127.0.0.1',
27
+ username: "reader",
28
+ password: "password",
29
+ port: "33005",
30
+ )
31
+
32
+ assert_equal conn.query("SELECT @@global.hostname as host #{Lhm::ProxySQLHelper::ANNOTATION}").each.first["host"], "mysql-1"
33
+ end
34
+ end
@@ -0,0 +1,52 @@
1
+ require 'yaml'
2
+ require 'mysql2'
3
+
4
+ class DBConnectionHelper
5
+
6
+ DATABASE_CONFIG_FILE = "database.yml"
7
+
8
+ class << self
9
+ def db_config
10
+ @db_config ||= YAML.load_file(File.expand_path(File.dirname(__FILE__)) + "/../#{DATABASE_CONFIG_FILE}")
11
+ end
12
+
13
+ def new_mysql_connection(role = :master, with_data = false, toxic = false)
14
+
15
+ key = role.to_s + toxic_postfix(toxic)
16
+
17
+ conn = ActiveRecord::Base.establish_connection(
18
+ :host => '127.0.0.1',
19
+ :adapter => "mysql2",
20
+ :username => db_config[key]['user'],
21
+ :password => db_config[key]['password'],
22
+ :database => test_db_name,
23
+ :port => db_config[key]['port']
24
+ )
25
+ conn = conn.connection
26
+ init_with_dummy_data(conn) if with_data
27
+ conn
28
+ end
29
+
30
+ def toxic_postfix(toxic)
31
+ toxic ? "_toxic" : ""
32
+ end
33
+
34
+ def test_db_name
35
+ @test_db_name ||= "test"
36
+ end
37
+
38
+ def test_table_name
39
+ @test_table_name ||= "test"
40
+ end
41
+
42
+ def init_with_dummy_data(conn)
43
+ conn.execute("DROP TABLE IF EXISTS #{test_table_name} ")
44
+ conn.execute("CREATE TABLE #{test_table_name} (id int)")
45
+
46
+ 1.upto(9) do |i|
47
+ query = "INSERT INTO #{test_table_name} (id) VALUE (#{i})"
48
+ conn.execute(query)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -21,6 +21,8 @@ describe Lhm::SqlRetry do
21
21
 
22
22
  # Assert our pre-conditions
23
23
  assert_equal 2, @helper.record_count
24
+
25
+ Mysql2::Client.any_instance.stubs(:active?).returns(true)
24
26
  end
25
27
 
26
28
  after(:each) do
@@ -41,7 +43,7 @@ describe Lhm::SqlRetry do
41
43
 
42
44
  exception = assert_raises { @helper.trigger_wait_lock }
43
45
 
44
- assert_equal "Lock wait timeout exceeded; try restarting transaction", exception.message
46
+ assert_match /Lock wait timeout exceeded; try restarting transaction/, exception.message
45
47
  assert_equal Mysql2::Error::TimeoutError, exception.class
46
48
 
47
49
  assert_equal 2, @helper.record_count # no records inserted
@@ -53,12 +55,12 @@ describe Lhm::SqlRetry do
53
55
  it "successfully executes the SQL despite the errors encountered" do
54
56
  # Start a thread to retry, once the lock is held, execute the block
55
57
  @helper.with_waiting_lock do |waiting_connection|
56
- sql_retry = Lhm::SqlRetry.new(waiting_connection, {
58
+ sql_retry = Lhm::SqlRetry.new(waiting_connection, retry_options: {
57
59
  base_interval: 0.2, # first retry after 200ms
58
60
  multiplier: 1, # subsequent retries wait 1x longer than first retry (no change)
59
61
  tries: 3, # we only need 3 tries (including the first) for the scenario described below
60
62
  rand_factor: 0 # do not introduce randomness to wait timer
61
- })
63
+ }, reconnect_with_consistent_host: false)
62
64
 
63
65
  # RetryTestHelper is configured to hold lock for 5 seconds and timeout after 2 seconds.
64
66
  # Therefore the sequence of events will be:
@@ -96,12 +98,12 @@ describe Lhm::SqlRetry do
96
98
  puts "*" * 64
97
99
  # Start a thread to retry, once the lock is held, execute the block
98
100
  @helper.with_waiting_lock do |waiting_connection|
99
- sql_retry = Lhm::SqlRetry.new(waiting_connection, {
101
+ sql_retry = Lhm::SqlRetry.new(waiting_connection, retry_options: {
100
102
  base_interval: 0.2, # first retry after 200ms
101
103
  multiplier: 1, # subsequent retries wait 1x longer than first retry (no change)
102
104
  tries: 2, # we need 3 tries (including the first) for the scenario described below, but we only get two...we will fail
103
105
  rand_factor: 0 # do not introduce randomness to wait timer
104
- })
106
+ }, reconnect_with_consistent_host: false)
105
107
 
106
108
  # RetryTestHelper is configured to hold lock for 5 seconds and timeout after 2 seconds.
107
109
  # Therefore the sequence of events will be:
@@ -116,7 +118,7 @@ describe Lhm::SqlRetry do
116
118
 
117
119
  exception = assert_raises { @helper.trigger_wait_lock }
118
120
 
119
- assert_equal "Lock wait timeout exceeded; try restarting transaction", exception.message
121
+ assert_match /Lock wait timeout exceeded; try restarting transaction/, exception.message
120
122
  assert_equal Mysql2::Error::TimeoutError, exception.class
121
123
 
122
124
  assert_equal 2, @helper.record_count # no records inserted
@@ -1,5 +1,7 @@
1
- require 'yaml'
1
+ require 'integration/integration_helper'
2
+
2
3
  class LockWaitTimeoutTestHelper
4
+
3
5
  def initialize(lock_duration:, innodb_lock_wait_timeout:)
4
6
  # This connection will be used exclusively to setup the test,
5
7
  # assert pre-conditions and assert post-conditions.
@@ -68,7 +70,7 @@ class LockWaitTimeoutTestHelper
68
70
 
69
71
  def insert_records_at_ids(connection, ids)
70
72
  ids.each do |id|
71
- connection.query "INSERT INTO #{test_table_name} (id) VALUES (#{id})"
73
+ mysql_exec(connection, "INSERT INTO #{test_table_name} (id) VALUES (#{id})")
72
74
  end
73
75
  end
74
76
 
@@ -79,11 +81,10 @@ class LockWaitTimeoutTestHelper
79
81
  def new_mysql_connection
80
82
  Mysql2::Client.new(
81
83
  host: '127.0.0.1',
82
- database: test_db_name,
83
84
  username: db_config['master']['user'],
84
85
  password: db_config['master']['password'],
85
86
  port: db_config['master']['port'],
86
- socket: db_config['master']['socket']
87
+ database: test_db_name
87
88
  )
88
89
  end
89
90
 
@@ -98,4 +99,16 @@ class LockWaitTimeoutTestHelper
98
99
  def test_table_name
99
100
  @test_table_name ||= "lock_wait"
100
101
  end
102
+
103
+ private
104
+
105
+ def mysql_exec(connection, statement)
106
+ if connection.class == Mysql2::Client
107
+ connection.query(statement)
108
+ elsif connection.class.to_s.include?("ActiveRecord")
109
+ connection.execute(statement)
110
+ else
111
+ raise StandardError.new("Unrecognized MySQL client")
112
+ end
113
+ end
101
114
  end
@@ -0,0 +1,22 @@
1
+ class ProxySQLHelper
2
+ class << self
3
+ # Flips the destination hostgroup for /maintenance:lhm/ from 0 (i.e. writer) to 1 (i.e. reader)
4
+ def with_lhm_hostgroup_flip
5
+ conn = Mysql2::Client.new(
6
+ host: '127.0.0.1',
7
+ username: "remote-admin",
8
+ password: "password",
9
+ port: "6032",
10
+ )
11
+
12
+ begin
13
+ conn.query("UPDATE mysql_query_rules SET destination_hostgroup=1 WHERE match_pattern=\"maintenance:lhm\"")
14
+ conn.query("LOAD MYSQL QUERY RULES TO RUNTIME;")
15
+ yield
16
+ ensure
17
+ conn.query("UPDATE mysql_query_rules SET destination_hostgroup=0 WHERE match_pattern=\"maintenance:lhm\"")
18
+ conn.query("LOAD MYSQL QUERY RULES TO RUNTIME;")
19
+ end
20
+ end
21
+ end
22
+ end