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
data/lib/lhm/sql_retry.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'retriable'
2
2
  require 'lhm/sql_helper'
3
+ require 'lhm/proxysql_helper'
3
4
 
4
5
  module Lhm
5
6
  # SqlRetry standardizes the interface for retry behavior in components like
@@ -15,22 +16,140 @@ module Lhm
15
16
  # https://github.com/kamui/retriable. Additionally, a "log_prefix" option,
16
17
  # which is unique to SqlRetry can be used to prefix log output.
17
18
  class SqlRetry
18
- def initialize(connection, options = {})
19
+ RECONNECT_SUCCESSFUL_MESSAGE = "LHM successfully reconnected to initial host:"
20
+ CLOUDSQL_VERSION_COMMENT = "(Google)"
21
+ # Will retry for 120 seconds (approximately, since connecting takes time).
22
+ RECONNECT_RETRY_MAX_ITERATION = 120
23
+ RECONNECT_RETRY_INTERVAL = 1
24
+ # Will abort the LHM if it had to reconnect more than 25 times in a single run (indicator that there might be
25
+ # something wrong with the network and would be better to run the LHM at a later time).
26
+ RECONNECTION_MAXIMUM = 25
27
+
28
+ MYSQL_VAR_NAMES = {
29
+ hostname: "@@global.hostname",
30
+ server_id: "@@global.server_id",
31
+ version_comment: "@@version_comment",
32
+ }
33
+
34
+ def initialize(connection, retry_options: {}, reconnect_with_consistent_host: false)
19
35
  @connection = connection
20
- @log_prefix = options.delete(:log_prefix)
21
- @retry_config = default_retry_config.dup.merge!(options)
36
+ self.retry_config = retry_options
37
+ self.reconnect_with_consistent_host = reconnect_with_consistent_host
22
38
  end
23
39
 
24
- def with_retries
25
- Retriable.retriable(retry_config) do
26
- yield(@connection)
40
+ # Complete explanation of algorithm: https://github.com/Shopify/lhm/pull/112
41
+ def with_retries(log_prefix: nil)
42
+ @log_prefix = log_prefix || "" # No prefix. Just logs
43
+
44
+ # Amount of time LHM had to reconnect. Aborting if more than RECONNECTION_MAXIMUM
45
+ reconnection_counter = 0
46
+
47
+ Retriable.retriable(@retry_config) do
48
+ # Using begin -> rescue -> end for Ruby 2.4 compatibility
49
+ begin
50
+ if @reconnect_with_consistent_host
51
+ raise Lhm::Error.new("MySQL host has changed since the start of the LHM. Aborting to avoid data-loss") unless same_host_as_initial?
52
+ end
53
+
54
+ yield(@connection)
55
+ rescue StandardError => e
56
+ # Not all errors should trigger a reconnect. Some errors such be raised and abort the LHM (such as reconnecting to the wrong host).
57
+ # The error will be raised the connection is still active (i.e. no need to reconnect) or if the connection is
58
+ # dead (i.e. not active) and @reconnect_with_host is false (i.e. instructed not to reconnect)
59
+ raise e if @connection.active? || (!@connection.active? && !@reconnect_with_consistent_host)
60
+
61
+ # Lhm could be stuck in a weird state where it loses connection, reconnects and re looses-connection instantly
62
+ # after, creating an infinite loop (because of the usage of `retry`). Hence, abort after 25 reconnections
63
+ raise Lhm::Error.new("LHM reached host reconnection max of #{RECONNECTION_MAXIMUM} times. " \
64
+ "Please try again later.") if reconnection_counter > RECONNECTION_MAXIMUM
65
+
66
+ reconnection_counter += 1
67
+ if reconnect_with_host_check!
68
+ retry
69
+ else
70
+ raise Lhm::Error.new("LHM tried the reconnection procedure but failed. Aborting")
71
+ end
72
+ end
27
73
  end
28
74
  end
29
75
 
30
- attr_reader :retry_config
76
+ # Both attributes will have defined setters
77
+ attr_reader :retry_config, :reconnect_with_consistent_host
78
+ attr_accessor :connection
79
+
80
+ def retry_config=(retry_options)
81
+ @retry_config = default_retry_config.dup.merge!(retry_options)
82
+ end
83
+
84
+ def reconnect_with_consistent_host=(reconnect)
85
+ if (@reconnect_with_consistent_host = reconnect)
86
+ @initial_hostname = hostname
87
+ @initial_server_id = server_id
88
+ end
89
+ end
31
90
 
32
91
  private
33
92
 
93
+ def hostname
94
+ mysql_single_value(MYSQL_VAR_NAMES[:hostname])
95
+ end
96
+
97
+ def server_id
98
+ mysql_single_value(MYSQL_VAR_NAMES[:server_id])
99
+ end
100
+
101
+ def cloudsql?
102
+ mysql_single_value(MYSQL_VAR_NAMES[:version_comment]).include?(CLOUDSQL_VERSION_COMMENT)
103
+ end
104
+
105
+ def mysql_single_value(name)
106
+ query = Lhm::ProxySQLHelper.tagged("SELECT #{name} LIMIT 1")
107
+
108
+ @connection.execute(query).to_a.first.tap do |record|
109
+ return record&.first
110
+ end
111
+ end
112
+
113
+ def same_host_as_initial?
114
+ return @initial_server_id == server_id if cloudsql?
115
+ @initial_hostname == hostname
116
+ end
117
+
118
+ def log_with_prefix(message, level = :info)
119
+ message.prepend("[#{@log_prefix}] ") if @log_prefix
120
+ Lhm.logger.public_send(level, message)
121
+ end
122
+
123
+ def reconnect_with_host_check!
124
+ log_with_prefix("Lost connection to MySQL, will retry to connect to same host")
125
+
126
+ RECONNECT_RETRY_MAX_ITERATION.times do
127
+ begin
128
+ sleep(RECONNECT_RETRY_INTERVAL)
129
+
130
+ # tries to reconnect. On failure will trigger a retry
131
+ @connection.reconnect!
132
+
133
+ if same_host_as_initial?
134
+ # This is not an actual error, but controlled way to get the parent `Retriable.retriable` to retry
135
+ # the statement that failed (since the Retriable gem only retries on errors).
136
+ log_with_prefix("LHM successfully reconnected to initial host: #{@initial_hostname} (server_id: #{@initial_server_id})")
137
+ return true
138
+ else
139
+ # New Master --> abort LHM (reconnecting will not change anything)
140
+ log_with_prefix("Reconnected to wrong host. Started migration on: #{@initial_hostname} (server_id: #{@initial_server_id}), but reconnected to: #{hostname} (server_id: #{server_id}).", :error)
141
+ return false
142
+ end
143
+ rescue StandardError => e
144
+ # Retry if ActiveRecord cannot reach host
145
+ next if /Lost connection to MySQL server at 'reading initial communication packet'/ === e.message
146
+ log_with_prefix("Encountered error: [#{e.class}] #{e.message}. Will stop reconnection procedure.", :info)
147
+ return false
148
+ end
149
+ end
150
+ false
151
+ end
152
+
34
153
  # For a full list of configuration options see https://github.com/kamui/retriable
35
154
  def default_retry_config
36
155
  {
@@ -43,6 +162,8 @@ module Lhm
43
162
  /Lost connection to MySQL server during query/,
44
163
  /Max connect timeout reached/,
45
164
  /Unknown MySQL server host/,
165
+ /connection is locked to hostgroup/,
166
+ /The MySQL server is running with the --read-only option so it cannot execute this statement/,
46
167
  ]
47
168
  },
48
169
  multiplier: 1, # each successive interval grows by this factor
@@ -51,9 +172,7 @@ module Lhm
51
172
  rand_factor: 0, # percentage to randomize the next retry interval time
52
173
  max_elapsed_time: Float::INFINITY, # max total time in seconds that code is allowed to keep being retried
53
174
  on_retry: Proc.new do |exception, try_number, total_elapsed_time, next_interval|
54
- log = "#{exception.class}: '#{exception.message}' - #{try_number} tries in #{total_elapsed_time} seconds and #{next_interval} seconds until the next try."
55
- log.prepend("[#{@log_prefix}] ") if @log_prefix
56
- Lhm.logger.info(log)
175
+ log_with_prefix("#{exception.class}: '#{exception.message}' - #{try_number} tries in #{total_elapsed_time} seconds and #{next_interval} seconds until the next try.", :error)
57
176
  end
58
177
  }.freeze
59
178
  end
@@ -124,8 +124,8 @@ module Lhm
124
124
  else
125
125
  raise ArgumentError, "Expected #{config_proc.inspect} to respond to `call`"
126
126
  end
127
- else # otherwise default to ActiveRecord provided config
128
- ActiveRecord::Base.connection_pool.spec.config.dup
127
+ else
128
+ db_config
129
129
  end
130
130
  config.deep_symbolize_keys!
131
131
  config[:host] = @host
@@ -140,6 +140,23 @@ module Lhm
140
140
  [nil]
141
141
  end
142
142
  end
143
+
144
+ private
145
+
146
+ def db_config
147
+ if ar_supports_db_config?
148
+ ActiveRecord::Base.connection_pool.db_config.configuration_hash.dup
149
+ else
150
+ ActiveRecord::Base.connection_pool.spec.config.dup
151
+ end
152
+ end
153
+
154
+ def ar_supports_db_config?
155
+ # https://api.rubyonrails.org/v6.0/classes/ActiveRecord/ConnectionAdapters/ConnectionPool.html <-- has spec
156
+ # vs
157
+ # https://api.rubyonrails.org/v6.1/classes/ActiveRecord/ConnectionAdapters/ConnectionPool.html <-- has db_config
158
+ ActiveRecord::VERSION::MAJOR > 6 || ActiveRecord::VERSION::MAJOR == 6 && ActiveRecord::VERSION::MINOR >= 1
159
+ end
143
160
  end
144
161
  end
145
162
  end
data/lib/lhm/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # Schmidt
3
3
 
4
4
  module Lhm
5
- VERSION = '3.4.0'
5
+ VERSION = '3.5.5'
6
6
  end
data/lib/lhm.rb CHANGED
@@ -8,6 +8,8 @@ require 'lhm/throttler'
8
8
  require 'lhm/version'
9
9
  require 'lhm/cleanup/current'
10
10
  require 'lhm/sql_retry'
11
+ require 'lhm/proxysql_helper'
12
+ require 'lhm/connection'
11
13
  require 'lhm/test_support'
12
14
  require 'lhm/railtie' if defined?(Rails::Railtie)
13
15
  require 'logger'
@@ -26,7 +28,7 @@ module Lhm
26
28
  extend Throttler
27
29
  extend self
28
30
 
29
- DEFAULT_LOGGER_OPTIONS = { level: Logger::INFO, file: STDOUT }
31
+ DEFAULT_LOGGER_OPTIONS = { level: Logger::INFO, file: STDOUT }
30
32
 
31
33
  # Alters a table with the changes described in the block
32
34
  #
@@ -44,15 +46,19 @@ module Lhm
44
46
  # Use atomic switch to rename tables (defaults to: true)
45
47
  # If using a version of mysql affected by atomic switch bug, LHM forces user
46
48
  # to set this option (see SqlHelper#supports_atomic_switch?)
49
+ # @option options [Boolean] :reconnect_with_consistent_host
50
+ # Active / Deactivate ProxySQL-aware reconnection procedure (default to: false)
47
51
  # @yield [Migrator] Yielded Migrator object records the changes
48
52
  # @return [Boolean] Returns true if the migration finishes
49
53
  # @raise [Error] Raises Lhm::Error in case of a error and aborts the migration
50
54
  def change_table(table_name, options = {}, &block)
51
- origin = Table.parse(table_name, connection)
52
- invoker = Invoker.new(origin, connection)
53
- block.call(invoker.migrator)
54
- invoker.run(options)
55
- true
55
+ with_flags(options) do
56
+ origin = Table.parse(table_name, connection)
57
+ invoker = Invoker.new(origin, connection)
58
+ block.call(invoker.migrator)
59
+ invoker.run(options)
60
+ true
61
+ end
56
62
  end
57
63
 
58
64
  # Cleanup tables and triggers
@@ -81,16 +87,19 @@ module Lhm
81
87
  Lhm::Cleanup::Current.new(run, table_name, connection, options).execute
82
88
  end
83
89
 
90
+ # Setups DB connection
91
+ #
92
+ # @param [ActiveRecord::Base] connection ActiveRecord Connection
84
93
  def setup(connection)
85
- @@connection = connection
94
+ @@connection = Connection.new(connection: connection)
86
95
  end
87
96
 
97
+ # Returns DB connection (or initializes it if not created yet)
88
98
  def connection
89
- @@connection ||=
90
- begin
91
- raise 'Please call Lhm.setup' unless defined?(ActiveRecord)
92
- ActiveRecord::Base.connection
93
- end
99
+ @@connection ||= begin
100
+ raise 'Please call Lhm.setup' unless defined?(ActiveRecord)
101
+ @@connection = Connection.new(connection: ActiveRecord::Base.connection)
102
+ end
94
103
  end
95
104
 
96
105
  def self.logger=(new_logger)
@@ -114,18 +123,34 @@ module Lhm
114
123
  triggers.each do |trigger|
115
124
  connection.execute("drop trigger if exists #{trigger}")
116
125
  end
126
+ logger.info("Dropped triggers #{triggers.join(', ')}")
127
+
117
128
  tables.each do |table|
118
129
  connection.execute("drop table if exists #{table}")
119
130
  end
131
+ logger.info("Dropped tables #{tables.join(', ')}")
132
+
120
133
  true
121
134
  elsif tables.empty? && triggers.empty?
122
- puts 'Everything is clean. Nothing to do.'
135
+ logger.info('Everything is clean. Nothing to do.')
123
136
  true
124
137
  else
125
- puts "Would drop LHM backup tables: #{tables.join(', ')}."
126
- puts "Would drop LHM triggers: #{triggers.join(', ')}."
127
- puts 'Run with Lhm.cleanup(true) to drop all LHM triggers and tables, or Lhm.cleanup_current_run(true, table_name) to clean up a specific LHM.'
138
+ logger.info("Would drop LHM backup tables: #{tables.join(', ')}.")
139
+ logger.info("Would drop LHM triggers: #{triggers.join(', ')}.")
140
+ logger.info('Run with Lhm.cleanup(true) to drop all LHM triggers and tables, or Lhm.cleanup_current_run(true, table_name) to clean up a specific LHM.')
128
141
  false
129
142
  end
130
143
  end
144
+
145
+ def with_flags(options)
146
+ old_flags = {
147
+ reconnect_with_consistent_host: Lhm.connection.reconnect_with_consistent_host,
148
+ }
149
+
150
+ Lhm.connection.reconnect_with_consistent_host = options[:reconnect_with_consistent_host] || false
151
+
152
+ yield
153
+ ensure
154
+ Lhm.connection.reconnect_with_consistent_host = old_flags[:reconnect_with_consistent_host]
155
+ end
131
156
  end
@@ -0,0 +1,21 @@
1
+ #!/bin/bash
2
+ # Wait for writer
3
+ echo "Waiting for MySQL-1: "
4
+ while ! (mysqladmin ping --host="127.0.0.1" --port=33006 --user=root --password=password --silent 2> /dev/null); do
5
+ echo -ne "."
6
+ sleep 1
7
+ done
8
+ # Wait for reader
9
+ echo "Waiting for MySQL-2: "
10
+ while ! (mysqladmin ping --host="127.0.0.1" --port=33007 --user=root --password=password --silent 2> /dev/null); do
11
+ echo -ne "."
12
+ sleep 1
13
+ done
14
+ # Wait for proxysql
15
+ echo "Waiting for ProxySQL:"
16
+ while ! (mysqladmin ping --host="127.0.0.1" --port=33005 --user=root --password=password --silent 2> /dev/null); do
17
+ echo -ne "."
18
+ sleep 1
19
+ done
20
+
21
+ echo "All DBs are ready"
@@ -0,0 +1,10 @@
1
+ STOP SLAVE;
2
+ CHANGE MASTER TO
3
+ MASTER_HOST='mysql-1',
4
+ MASTER_AUTO_POSITION=1,
5
+ MASTER_USER='replication',
6
+ MASTER_PASSWORD='password',
7
+ MASTER_CONNECT_RETRY=1,
8
+ MASTER_RETRY_COUNT=300; -- 5 minutes
9
+
10
+ start slave;
@@ -0,0 +1 @@
1
+ CREATE DATABASE test;
@@ -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,59 @@ 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
41
+
42
+ connection = Lhm::Connection.new(connection: ar_connection, options: {
43
+ reconnect_with_consistent_host: true,
44
+ retriable: {
45
+ tries: 3,
46
+ base_interval: 0
47
+ }
48
+ })
35
49
 
36
- switcher = Lhm::AtomicSwitcher.new(@migration, connection, retriable: {base_interval: 0})
50
+ switcher = Lhm::AtomicSwitcher.new(@migration, connection)
37
51
 
38
52
  assert switcher.run
39
53
 
40
54
  log_messages = @logs.string.split("\n")
41
55
  assert_equal(2, log_messages.length)
42
56
  assert log_messages[0].include? "Starting run of class=Lhm::AtomicSwitcher"
57
+ # On failure of this assertion, check for Lhm::Connection#file
43
58
  assert log_messages[1].include? "[AtomicSwitcher] ActiveRecord::StatementInvalid: 'Lock wait timeout exceeded; try restarting transaction.' - 1 tries"
44
59
  end
45
60
 
46
61
  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.')
62
+ ar_connection = mock()
63
+ ar_connection.stubs(:data_source_exists?).returns(true)
64
+ ar_connection.stubs(:active?).returns(true)
65
+ ar_connection.stubs(:execute).returns([["dummy"]], [["dummy"]], [["dummy"]])
66
+ .then
67
+ .raises(ActiveRecord::StatementInvalid, 'Lock wait timeout exceeded; try restarting transaction.')
68
+ .then
69
+ .returns([["dummy"]]) # triggers retry 1
70
+ .then
71
+ .raises(ActiveRecord::StatementInvalid, 'Lock wait timeout exceeded; try restarting transaction.')
72
+ .then
73
+ .returns([["dummy"]]) # triggers retry 2
74
+ .then
75
+ .raises(ActiveRecord::StatementInvalid, 'Lock wait timeout exceeded; try restarting transaction.') # triggers retry 2
76
+
77
+ connection = Lhm::Connection.new(connection: ar_connection, options: {
78
+ reconnect_with_consistent_host: true,
79
+ retriable: {
80
+ tries: 2,
81
+ base_interval: 0
82
+ }
83
+ })
50
84
 
51
- switcher = Lhm::AtomicSwitcher.new(@migration, connection, retriable: {tries: 2, base_interval: 0})
85
+ switcher = Lhm::AtomicSwitcher.new(@migration, connection)
52
86
 
53
87
  assert_raises(ActiveRecord::StatementInvalid) { switcher.run }
54
88
  end
@@ -58,12 +92,14 @@ describe Lhm::AtomicSwitcher do
58
92
  switcher.send :define_singleton_method, :atomic_switch do
59
93
  'SELECT * FROM nonexistent'
60
94
  end
61
- -> { switcher.run }.must_raise(ActiveRecord::StatementInvalid)
95
+ value(-> { switcher.run }).must_raise(ActiveRecord::StatementInvalid)
62
96
  end
63
97
 
64
98
  it "should raise when destination doesn't exist" do
65
- connection = mock()
66
- connection.stubs(:data_source_exists?).returns(false)
99
+ ar_connection = mock()
100
+ ar_connection.stubs(:data_source_exists?).returns(false)
101
+
102
+ connection = Lhm::Connection.new(connection: ar_connection)
67
103
 
68
104
  switcher = Lhm::AtomicSwitcher.new(@migration, connection)
69
105
 
@@ -75,8 +111,8 @@ describe Lhm::AtomicSwitcher do
75
111
  switcher.run
76
112
 
77
113
  slave do
78
- data_source_exists?(@origin).must_equal true
79
- table_read(@migration.archive_name).columns.keys.must_include 'origin'
114
+ value(data_source_exists?(@origin)).must_equal true
115
+ value(table_read(@migration.archive_name).columns.keys).must_include 'origin'
80
116
  end
81
117
  end
82
118
 
@@ -85,8 +121,8 @@ describe Lhm::AtomicSwitcher do
85
121
  switcher.run
86
122
 
87
123
  slave do
88
- data_source_exists?(@destination).must_equal false
89
- table_read(@origin.name).columns.keys.must_include 'destination'
124
+ value(data_source_exists?(@destination)).must_equal false
125
+ value(table_read(@origin.name).columns.keys).must_include 'destination'
90
126
  end
91
127
  end
92
128
  end
@@ -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
@@ -22,7 +23,7 @@ describe Lhm::ChunkInsert do
22
23
  @instance.insert_and_return_count_of_rows_created
23
24
 
24
25
  slave do
25
- count_all(@destination.name).must_equal(1)
26
+ value(count_all(@destination.name)).must_equal(1)
26
27
  end
27
28
  end
28
29
  end