lhm-teak 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/test.yml +43 -0
- data/.gitignore +12 -0
- data/.rubocop.yml +183 -0
- data/.travis.yml +21 -0
- data/Appraisals +24 -0
- data/CHANGELOG.md +254 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +67 -0
- data/LICENSE +27 -0
- data/README.md +335 -0
- data/Rakefile +33 -0
- data/dev.yml +45 -0
- data/docker-compose.yml +60 -0
- data/gemfiles/activerecord_5.2.gemfile +9 -0
- data/gemfiles/activerecord_5.2.gemfile.lock +66 -0
- data/gemfiles/activerecord_6.0.gemfile +7 -0
- data/gemfiles/activerecord_6.0.gemfile.lock +68 -0
- data/gemfiles/activerecord_6.1.gemfile +7 -0
- data/gemfiles/activerecord_6.1.gemfile.lock +67 -0
- data/gemfiles/activerecord_7.0.0.alpha2.gemfile +7 -0
- data/gemfiles/activerecord_7.0.0.alpha2.gemfile.lock +65 -0
- data/lhm.gemspec +38 -0
- data/lib/lhm/atomic_switcher.rb +46 -0
- data/lib/lhm/chunk_finder.rb +62 -0
- data/lib/lhm/chunk_insert.rb +61 -0
- data/lib/lhm/chunker.rb +95 -0
- data/lib/lhm/cleanup/current.rb +71 -0
- data/lib/lhm/command.rb +48 -0
- data/lib/lhm/connection.rb +108 -0
- data/lib/lhm/entangler.rb +112 -0
- data/lib/lhm/intersection.rb +51 -0
- data/lib/lhm/invoker.rb +100 -0
- data/lib/lhm/locked_switcher.rb +76 -0
- data/lib/lhm/migration.rb +51 -0
- data/lib/lhm/migrator.rb +244 -0
- data/lib/lhm/printer.rb +63 -0
- data/lib/lhm/proxysql_helper.rb +10 -0
- data/lib/lhm/railtie.rb +9 -0
- data/lib/lhm/sql_helper.rb +77 -0
- data/lib/lhm/sql_retry.rb +180 -0
- data/lib/lhm/table.rb +121 -0
- data/lib/lhm/table_name.rb +23 -0
- data/lib/lhm/test_support.rb +35 -0
- data/lib/lhm/throttler/slave_lag.rb +162 -0
- data/lib/lhm/throttler/threads_running.rb +53 -0
- data/lib/lhm/throttler/time.rb +29 -0
- data/lib/lhm/throttler.rb +36 -0
- data/lib/lhm/timestamp.rb +11 -0
- data/lib/lhm/version.rb +6 -0
- data/lib/lhm-shopify.rb +1 -0
- data/lib/lhm.rb +156 -0
- data/scripts/helpers/wait-for-dbs.sh +21 -0
- data/scripts/mysql/reader/create_replication.sql +10 -0
- data/scripts/mysql/writer/create_test_db.sql +1 -0
- data/scripts/mysql/writer/create_users.sql +6 -0
- data/scripts/proxysql/proxysql.cnf +117 -0
- data/shipit.rubygems.yml +0 -0
- data/spec/.lhm.example +4 -0
- data/spec/README.md +58 -0
- data/spec/fixtures/bigint_table.ddl +4 -0
- data/spec/fixtures/composite_primary_key.ddl +6 -0
- data/spec/fixtures/composite_primary_key_dest.ddl +6 -0
- data/spec/fixtures/custom_primary_key.ddl +6 -0
- data/spec/fixtures/custom_primary_key_dest.ddl +6 -0
- data/spec/fixtures/destination.ddl +6 -0
- data/spec/fixtures/lines.ddl +7 -0
- data/spec/fixtures/origin.ddl +6 -0
- data/spec/fixtures/permissions.ddl +5 -0
- data/spec/fixtures/small_table.ddl +4 -0
- data/spec/fixtures/tracks.ddl +5 -0
- data/spec/fixtures/users.ddl +14 -0
- data/spec/fixtures/wo_id_int_column.ddl +6 -0
- data/spec/integration/atomic_switcher_spec.rb +129 -0
- data/spec/integration/chunk_insert_spec.rb +30 -0
- data/spec/integration/chunker_spec.rb +269 -0
- data/spec/integration/cleanup_spec.rb +147 -0
- data/spec/integration/database.yml +25 -0
- data/spec/integration/entangler_spec.rb +68 -0
- data/spec/integration/integration_helper.rb +252 -0
- data/spec/integration/invoker_spec.rb +33 -0
- data/spec/integration/lhm_spec.rb +659 -0
- data/spec/integration/lock_wait_timeout_spec.rb +30 -0
- data/spec/integration/locked_switcher_spec.rb +50 -0
- data/spec/integration/proxysql_spec.rb +34 -0
- data/spec/integration/sql_retry/db_connection_helper.rb +52 -0
- data/spec/integration/sql_retry/lock_wait_spec.rb +127 -0
- data/spec/integration/sql_retry/lock_wait_timeout_test_helper.rb +114 -0
- data/spec/integration/sql_retry/proxysql_helper.rb +22 -0
- data/spec/integration/sql_retry/retry_with_proxysql_spec.rb +109 -0
- data/spec/integration/table_spec.rb +83 -0
- data/spec/integration/toxiproxy_helper.rb +40 -0
- data/spec/test_helper.rb +69 -0
- data/spec/unit/atomic_switcher_spec.rb +29 -0
- data/spec/unit/chunk_finder_spec.rb +73 -0
- data/spec/unit/chunk_insert_spec.rb +67 -0
- data/spec/unit/chunker_spec.rb +176 -0
- data/spec/unit/connection_spec.rb +111 -0
- data/spec/unit/entangler_spec.rb +187 -0
- data/spec/unit/intersection_spec.rb +51 -0
- data/spec/unit/lhm_spec.rb +46 -0
- data/spec/unit/locked_switcher_spec.rb +46 -0
- data/spec/unit/migrator_spec.rb +144 -0
- data/spec/unit/printer_spec.rb +85 -0
- data/spec/unit/sql_helper_spec.rb +28 -0
- data/spec/unit/table_name_spec.rb +39 -0
- data/spec/unit/table_spec.rb +47 -0
- data/spec/unit/throttler/slave_lag_spec.rb +322 -0
- data/spec/unit/throttler/threads_running_spec.rb +64 -0
- data/spec/unit/throttler_spec.rb +124 -0
- data/spec/unit/unit_helper.rb +26 -0
- metadata +366 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require 'lhm/throttler/time'
|
|
2
|
+
require 'lhm/throttler/slave_lag'
|
|
3
|
+
require 'lhm/throttler/threads_running'
|
|
4
|
+
|
|
5
|
+
module Lhm
|
|
6
|
+
module Throttler
|
|
7
|
+
CLASSES = { :time_throttler => Throttler::Time,
|
|
8
|
+
:slave_lag_throttler => Throttler::SlaveLag,
|
|
9
|
+
:threads_running_throttler => Throttler::ThreadsRunning }
|
|
10
|
+
|
|
11
|
+
def throttler
|
|
12
|
+
@throttler ||= Throttler::Time.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def setup_throttler(type, options = {})
|
|
16
|
+
@throttler = Factory.create_throttler(type, options)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class Factory
|
|
20
|
+
def self.create_throttler(type, options = {})
|
|
21
|
+
case type
|
|
22
|
+
when Lhm::Command
|
|
23
|
+
type
|
|
24
|
+
when Symbol
|
|
25
|
+
CLASSES[type].new(options)
|
|
26
|
+
when String
|
|
27
|
+
CLASSES[type.to_sym].new(options)
|
|
28
|
+
when Class
|
|
29
|
+
type.new(options)
|
|
30
|
+
else
|
|
31
|
+
raise ArgumentError, 'type argument must be a Symbol, String or Class'
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/lhm/version.rb
ADDED
data/lib/lhm-shopify.rb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "lhm"
|
data/lib/lhm.rb
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
|
2
|
+
# Schmidt
|
|
3
|
+
|
|
4
|
+
require 'lhm/table_name'
|
|
5
|
+
require 'lhm/table'
|
|
6
|
+
require 'lhm/invoker'
|
|
7
|
+
require 'lhm/throttler'
|
|
8
|
+
require 'lhm/version'
|
|
9
|
+
require 'lhm/cleanup/current'
|
|
10
|
+
require 'lhm/sql_retry'
|
|
11
|
+
require 'lhm/proxysql_helper'
|
|
12
|
+
require 'lhm/connection'
|
|
13
|
+
require 'lhm/test_support'
|
|
14
|
+
require 'lhm/railtie' if defined?(Rails::Railtie)
|
|
15
|
+
require 'logger'
|
|
16
|
+
|
|
17
|
+
# Large hadron migrator - online schema change tool
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
#
|
|
21
|
+
# Lhm.change_table(:users) do |m|
|
|
22
|
+
# m.add_column(:arbitrary, "INT(12)")
|
|
23
|
+
# m.add_index([:arbitrary, :created_at])
|
|
24
|
+
# m.ddl("alter table %s add column flag tinyint(1)" % m.name)
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
module Lhm
|
|
28
|
+
extend Throttler
|
|
29
|
+
extend self
|
|
30
|
+
|
|
31
|
+
DEFAULT_LOGGER_OPTIONS = { level: Logger::INFO, file: STDOUT }
|
|
32
|
+
|
|
33
|
+
# Alters a table with the changes described in the block
|
|
34
|
+
#
|
|
35
|
+
# @param [String, Symbol] table_name Name of the table
|
|
36
|
+
# @param [Hash] options Optional options to alter the chunk / switch behavior
|
|
37
|
+
# @option options [Integer] :stride
|
|
38
|
+
# Size of a chunk (defaults to: 2,000)
|
|
39
|
+
# @option options [Integer] :throttle
|
|
40
|
+
# Time to wait between chunks in milliseconds (defaults to: 100)
|
|
41
|
+
# @option options [Integer] :start
|
|
42
|
+
# Primary Key position at which to start copying chunks
|
|
43
|
+
# @option options [Integer] :limit
|
|
44
|
+
# Primary Key position at which to stop copying chunks
|
|
45
|
+
# @option options [Boolean] :atomic_switch
|
|
46
|
+
# Use atomic switch to rename tables (defaults to: true)
|
|
47
|
+
# If using a version of mysql affected by atomic switch bug, LHM forces user
|
|
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)
|
|
51
|
+
# @yield [Migrator] Yielded Migrator object records the changes
|
|
52
|
+
# @return [Boolean] Returns true if the migration finishes
|
|
53
|
+
# @raise [Error] Raises Lhm::Error in case of a error and aborts the migration
|
|
54
|
+
def change_table(table_name, options = {}, &block)
|
|
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
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Cleanup tables and triggers
|
|
65
|
+
#
|
|
66
|
+
# @param [Boolean] run execute now or just display information
|
|
67
|
+
# @param [Hash] options Optional options to alter cleanup behaviour
|
|
68
|
+
# @option options [Time] :until
|
|
69
|
+
# Filter to only remove tables up to specified time (defaults to: nil)
|
|
70
|
+
def cleanup(run = false, options = {})
|
|
71
|
+
lhm_tables = connection.select_values('show tables').select { |name| name =~ /^lhm(a|n)_/ }
|
|
72
|
+
if options[:until]
|
|
73
|
+
lhm_tables.select! do |table|
|
|
74
|
+
table_date_time = Time.strptime(table, 'lhma_%Y_%m_%d_%H_%M_%S')
|
|
75
|
+
table_date_time <= options[:until]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
lhm_triggers = connection.select_values('show triggers').collect do |trigger|
|
|
80
|
+
trigger.respond_to?(:trigger) ? trigger.trigger : trigger
|
|
81
|
+
end.select { |name| name =~ /^lhmt/ }
|
|
82
|
+
|
|
83
|
+
drop_tables_and_triggers(run, lhm_triggers, lhm_tables)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def cleanup_current_run(run, table_name, options = {})
|
|
87
|
+
Lhm::Cleanup::Current.new(run, table_name, connection, options).execute
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Setups DB connection
|
|
91
|
+
#
|
|
92
|
+
# @param [ActiveRecord::Base] connection ActiveRecord Connection
|
|
93
|
+
def setup(connection)
|
|
94
|
+
@@connection = Connection.new(connection: connection)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Returns DB connection (or initializes it if not created yet)
|
|
98
|
+
def connection
|
|
99
|
+
@@connection ||= begin
|
|
100
|
+
raise 'Please call Lhm.setup' unless defined?(ActiveRecord)
|
|
101
|
+
@@connection = Connection.new(connection: ActiveRecord::Base.connection)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.logger=(new_logger)
|
|
106
|
+
@@logger = new_logger
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def self.logger
|
|
110
|
+
@@logger ||=
|
|
111
|
+
begin
|
|
112
|
+
logger = Logger.new(DEFAULT_LOGGER_OPTIONS[:file])
|
|
113
|
+
logger.level = DEFAULT_LOGGER_OPTIONS[:level]
|
|
114
|
+
logger.formatter = nil
|
|
115
|
+
logger
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def drop_tables_and_triggers(run = false, triggers, tables)
|
|
122
|
+
if run
|
|
123
|
+
triggers.each do |trigger|
|
|
124
|
+
connection.execute("drop trigger if exists #{trigger}")
|
|
125
|
+
end
|
|
126
|
+
logger.info("Dropped triggers #{triggers.join(', ')}")
|
|
127
|
+
|
|
128
|
+
tables.each do |table|
|
|
129
|
+
connection.execute("drop table if exists #{table}")
|
|
130
|
+
end
|
|
131
|
+
logger.info("Dropped tables #{tables.join(', ')}")
|
|
132
|
+
|
|
133
|
+
true
|
|
134
|
+
elsif tables.empty? && triggers.empty?
|
|
135
|
+
logger.info('Everything is clean. Nothing to do.')
|
|
136
|
+
true
|
|
137
|
+
else
|
|
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.')
|
|
141
|
+
false
|
|
142
|
+
end
|
|
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
|
|
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 @@
|
|
|
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
|
+
)
|
data/shipit.rubygems.yml
ADDED
|
File without changes
|
data/spec/.lhm.example
ADDED
data/spec/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Preparing for master slave integration tests
|
|
2
|
+
|
|
3
|
+
## Configuration
|
|
4
|
+
|
|
5
|
+
create ~/.lhm:
|
|
6
|
+
|
|
7
|
+
mysqldir=/usr/local/mysql
|
|
8
|
+
basedir=~/lhm-cluster
|
|
9
|
+
master_port=3306
|
|
10
|
+
slave_port=3307
|
|
11
|
+
|
|
12
|
+
mysqldir specifies the location of your mysql install. basedir is the
|
|
13
|
+
directory master and slave databases will get installed into.
|
|
14
|
+
|
|
15
|
+
## Automatic setup
|
|
16
|
+
|
|
17
|
+
### Run
|
|
18
|
+
|
|
19
|
+
bin/lhm-spec-clobber.sh
|
|
20
|
+
|
|
21
|
+
You can set the integration specs up to run against a master slave setup by
|
|
22
|
+
running the included that. This deletes the configured lhm master slave setup and reinstalls and configures a master slave setup.
|
|
23
|
+
|
|
24
|
+
Follow the manual instructions if you want more control over this process.
|
|
25
|
+
|
|
26
|
+
## Manual setup
|
|
27
|
+
|
|
28
|
+
### set up instances
|
|
29
|
+
|
|
30
|
+
bin/lhm-spec-setup-cluster.sh
|
|
31
|
+
|
|
32
|
+
### start instances
|
|
33
|
+
|
|
34
|
+
basedir=/opt/lhm-luster
|
|
35
|
+
mysqld --defaults-file="$basedir/master/my.cnf"
|
|
36
|
+
mysqld --defaults-file="$basedir/slave/my.cnf"
|
|
37
|
+
|
|
38
|
+
### run the grants
|
|
39
|
+
|
|
40
|
+
bin/lhm-spec-grants.sh
|
|
41
|
+
|
|
42
|
+
## run specs
|
|
43
|
+
|
|
44
|
+
Setup the dependency gems
|
|
45
|
+
|
|
46
|
+
export BUNDLE_GEMFILE=gemfiles/ar-4.2_mysql2.gemfile
|
|
47
|
+
bundle install
|
|
48
|
+
|
|
49
|
+
To run specs in slave mode, set the MASTER_SLAVE=1 when running tests:
|
|
50
|
+
|
|
51
|
+
MASTER_SLAVE=1 bundle exec rake specs
|
|
52
|
+
|
|
53
|
+
# connecting
|
|
54
|
+
|
|
55
|
+
you can connect by running (with the respective ports):
|
|
56
|
+
|
|
57
|
+
mysql --protocol=TCP -p3307
|
|
58
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
CREATE TABLE `users` (
|
|
2
|
+
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
3
|
+
`reference` int(11) DEFAULT NULL,
|
|
4
|
+
`username` varchar(255) DEFAULT NULL,
|
|
5
|
+
`group` varchar(255) DEFAULT 'Superfriends',
|
|
6
|
+
`created_at` datetime DEFAULT NULL,
|
|
7
|
+
`comment` varchar(20) DEFAULT NULL,
|
|
8
|
+
`description` text,
|
|
9
|
+
PRIMARY KEY (`id`),
|
|
10
|
+
UNIQUE KEY `index_users_on_reference` (`reference`),
|
|
11
|
+
KEY `index_users_on_username_and_created_at` (`username`,`created_at`),
|
|
12
|
+
KEY `index_with_a_custom_name` (`username`,`group`)
|
|
13
|
+
|
|
14
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
|
2
|
+
# Schmidt
|
|
3
|
+
|
|
4
|
+
require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
|
|
5
|
+
|
|
6
|
+
require 'lhm/table'
|
|
7
|
+
require 'lhm/migration'
|
|
8
|
+
require 'lhm/atomic_switcher'
|
|
9
|
+
require 'lhm/connection'
|
|
10
|
+
|
|
11
|
+
describe Lhm::AtomicSwitcher do
|
|
12
|
+
include IntegrationHelper
|
|
13
|
+
|
|
14
|
+
before(:each) { connect_master! }
|
|
15
|
+
|
|
16
|
+
describe 'switching' do
|
|
17
|
+
before(:each) do
|
|
18
|
+
Thread.abort_on_exception = true
|
|
19
|
+
@origin = table_create('origin')
|
|
20
|
+
@destination = table_create('destination')
|
|
21
|
+
@migration = Lhm::Migration.new(@origin, @destination)
|
|
22
|
+
@logs = StringIO.new
|
|
23
|
+
Lhm.logger = Logger.new(@logs)
|
|
24
|
+
@connection.execute('SET GLOBAL innodb_lock_wait_timeout=3')
|
|
25
|
+
@connection.execute('SET GLOBAL lock_wait_timeout=3')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
after(:each) do
|
|
29
|
+
Thread.abort_on_exception = false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'should retry and log on lock wait timeouts' do
|
|
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
|
+
})
|
|
49
|
+
|
|
50
|
+
switcher = Lhm::AtomicSwitcher.new(@migration, connection)
|
|
51
|
+
|
|
52
|
+
assert switcher.run
|
|
53
|
+
|
|
54
|
+
log_messages = @logs.string.split("\n")
|
|
55
|
+
assert_equal(2, log_messages.length)
|
|
56
|
+
assert log_messages[0].include? "Starting run of class=Lhm::AtomicSwitcher"
|
|
57
|
+
# On failure of this assertion, check for Lhm::Connection#file
|
|
58
|
+
assert log_messages[1].include? "[AtomicSwitcher] ActiveRecord::StatementInvalid: 'Lock wait timeout exceeded; try restarting transaction.' - 1 tries"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'should give up on lock wait timeouts after a configured number of tries' do
|
|
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
|
+
})
|
|
84
|
+
|
|
85
|
+
switcher = Lhm::AtomicSwitcher.new(@migration, connection)
|
|
86
|
+
|
|
87
|
+
assert_raises(ActiveRecord::StatementInvalid) { switcher.run }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'should raise on non lock wait timeout exceptions' do
|
|
91
|
+
switcher = Lhm::AtomicSwitcher.new(@migration, connection)
|
|
92
|
+
switcher.send :define_singleton_method, :atomic_switch do
|
|
93
|
+
'SELECT * FROM nonexistent'
|
|
94
|
+
end
|
|
95
|
+
value(-> { switcher.run }).must_raise(ActiveRecord::StatementInvalid)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "should raise when destination doesn't exist" do
|
|
99
|
+
ar_connection = mock()
|
|
100
|
+
ar_connection.stubs(:data_source_exists?).returns(false)
|
|
101
|
+
|
|
102
|
+
connection = Lhm::Connection.new(connection: ar_connection)
|
|
103
|
+
|
|
104
|
+
switcher = Lhm::AtomicSwitcher.new(@migration, connection)
|
|
105
|
+
|
|
106
|
+
assert_raises(Lhm::Error) { switcher.run }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'rename origin to archive' do
|
|
110
|
+
switcher = Lhm::AtomicSwitcher.new(@migration, connection)
|
|
111
|
+
switcher.run
|
|
112
|
+
|
|
113
|
+
slave do
|
|
114
|
+
value(data_source_exists?(@origin)).must_equal true
|
|
115
|
+
value(table_read(@migration.archive_name).columns.keys).must_include 'origin'
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'rename destination to origin' do
|
|
120
|
+
switcher = Lhm::AtomicSwitcher.new(@migration, connection)
|
|
121
|
+
switcher.run
|
|
122
|
+
|
|
123
|
+
slave do
|
|
124
|
+
value(data_source_exists?(@destination)).must_equal false
|
|
125
|
+
value(table_read(@origin.name).columns.keys).must_include 'destination'
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|