sbader-lhm 1.1.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.
- data/.gitignore +6 -0
- data/.travis.yml +10 -0
- data/CHANGELOG.md +99 -0
- data/LICENSE +27 -0
- data/README.md +146 -0
- data/Rakefile +20 -0
- data/bin/lhm-kill-queue +172 -0
- data/bin/lhm-spec-clobber.sh +36 -0
- data/bin/lhm-spec-grants.sh +25 -0
- data/bin/lhm-spec-setup-cluster.sh +67 -0
- data/bin/lhm-test-all.sh +10 -0
- data/gemfiles/ar-2.3_mysql.gemfile +5 -0
- data/gemfiles/ar-3.2_mysql.gemfile +5 -0
- data/gemfiles/ar-3.2_mysql2.gemfile +5 -0
- data/lhm.gemspec +27 -0
- data/lib/lhm.rb +45 -0
- data/lib/lhm/atomic_switcher.rb +49 -0
- data/lib/lhm/chunker.rb +114 -0
- data/lib/lhm/command.rb +46 -0
- data/lib/lhm/entangler.rb +98 -0
- data/lib/lhm/intersection.rb +63 -0
- data/lib/lhm/invoker.rb +49 -0
- data/lib/lhm/locked_switcher.rb +71 -0
- data/lib/lhm/migration.rb +30 -0
- data/lib/lhm/migrator.rb +219 -0
- data/lib/lhm/sql_helper.rb +85 -0
- data/lib/lhm/table.rb +97 -0
- data/lib/lhm/version.rb +6 -0
- data/spec/.lhm.example +4 -0
- data/spec/README.md +51 -0
- data/spec/bootstrap.rb +13 -0
- data/spec/fixtures/destination.ddl +6 -0
- data/spec/fixtures/origin.ddl +6 -0
- data/spec/fixtures/small_table.ddl +4 -0
- data/spec/fixtures/users.ddl +12 -0
- data/spec/integration/atomic_switcher_spec.rb +42 -0
- data/spec/integration/chunker_spec.rb +32 -0
- data/spec/integration/entangler_spec.rb +66 -0
- data/spec/integration/integration_helper.rb +140 -0
- data/spec/integration/lhm_spec.rb +204 -0
- data/spec/integration/locked_switcher_spec.rb +42 -0
- data/spec/integration/table_spec.rb +48 -0
- data/spec/unit/atomic_switcher_spec.rb +31 -0
- data/spec/unit/chunker_spec.rb +111 -0
- data/spec/unit/entangler_spec.rb +76 -0
- data/spec/unit/intersection_spec.rb +39 -0
- data/spec/unit/locked_switcher_spec.rb +51 -0
- data/spec/unit/migration_spec.rb +23 -0
- data/spec/unit/migrator_spec.rb +134 -0
- data/spec/unit/sql_helper_spec.rb +32 -0
- data/spec/unit/table_spec.rb +34 -0
- data/spec/unit/unit_helper.rb +14 -0
- metadata +173 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
|
3
|
+
set -e
|
4
|
+
set -u
|
5
|
+
|
6
|
+
source ~/.lhm
|
7
|
+
|
8
|
+
lhmkill() {
|
9
|
+
ps -ef | gsed -n "/[m]ysqld.*lhm-cluster/p" | awk '{ print $2 }' | xargs kill
|
10
|
+
sleep 5
|
11
|
+
}
|
12
|
+
|
13
|
+
echo stopping other running mysql instance
|
14
|
+
launchctl remove com.mysql.mysqld || { echo launchctl did not remove mysqld; }
|
15
|
+
"$mysqldir"/bin/mysqladmin shutdown || { echo mysqladmin did not shut down anything; }
|
16
|
+
|
17
|
+
echo killing lhm-cluster
|
18
|
+
lhmkill
|
19
|
+
|
20
|
+
echo removing $basedir
|
21
|
+
rm -rf "$basedir"
|
22
|
+
|
23
|
+
echo setting up cluster
|
24
|
+
bin/lhm-spec-setup-cluster.sh
|
25
|
+
|
26
|
+
echo staring instances
|
27
|
+
"$mysqldir"/bin/mysqld --defaults-file="$basedir/master/my.cnf" 2>&1 >$basedir/master/lhm.log &
|
28
|
+
"$mysqldir"/bin/mysqld --defaults-file="$basedir/slave/my.cnf" 2>&1 >$basedir/slave/lhm.log &
|
29
|
+
sleep 5
|
30
|
+
|
31
|
+
echo running grants
|
32
|
+
bin/lhm-spec-grants.sh
|
33
|
+
|
34
|
+
trap lhmkill SIGTERM SIGINT
|
35
|
+
|
36
|
+
wait
|
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
|
3
|
+
source ~/.lhm
|
4
|
+
|
5
|
+
master() { "$mysqldir"/bin/mysql --protocol=TCP -P $master_port -uroot; }
|
6
|
+
slave() { "$mysqldir"/bin/mysql --protocol=TCP -P $slave_port -uroot; }
|
7
|
+
|
8
|
+
# set up master
|
9
|
+
|
10
|
+
echo "create user 'slave'@'localhost' identified by 'slave'" | master
|
11
|
+
echo "grant replication slave on *.* to 'slave'@'localhost'" | master
|
12
|
+
|
13
|
+
# set up slave
|
14
|
+
|
15
|
+
echo "change master to master_user = 'slave', master_password = 'slave', master_port = 3306, master_host = 'localhost'" | slave
|
16
|
+
echo "start slave" | slave
|
17
|
+
echo "show slave status \G" | slave
|
18
|
+
|
19
|
+
# setup for test
|
20
|
+
|
21
|
+
echo "grant all privileges on *.* to ''@'localhost'" | master
|
22
|
+
echo "grant all privileges on *.* to ''@'localhost'" | slave
|
23
|
+
|
24
|
+
echo "create database lhm" | master
|
25
|
+
echo "create database if not exists lhm" | slave
|
@@ -0,0 +1,67 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
|
3
|
+
#
|
4
|
+
# Set up master slave cluster for lhm specs
|
5
|
+
#
|
6
|
+
|
7
|
+
set -e
|
8
|
+
set -u
|
9
|
+
|
10
|
+
source ~/.lhm
|
11
|
+
|
12
|
+
#
|
13
|
+
# Main
|
14
|
+
#
|
15
|
+
|
16
|
+
install_bin="$(echo ./*/mysql_install_db)"
|
17
|
+
|
18
|
+
mkdir -p "$basedir/master/data" "$basedir/slave/data"
|
19
|
+
|
20
|
+
cat <<-CNF > $basedir/master/my.cnf
|
21
|
+
[mysqld]
|
22
|
+
pid-file = $basedir/master/mysqld.pid
|
23
|
+
socket = $basedir/master/mysqld.sock
|
24
|
+
port = $master_port
|
25
|
+
log_output = FILE
|
26
|
+
log-error = $basedir/master/error.log
|
27
|
+
datadir = $basedir/master/data
|
28
|
+
log-bin = master-bin
|
29
|
+
log-bin-index = master-bin.index
|
30
|
+
server-id = 1
|
31
|
+
CNF
|
32
|
+
|
33
|
+
cat <<-CNF > $basedir/slave/my.cnf
|
34
|
+
[mysqld]
|
35
|
+
pid-file = $basedir/slave/mysqld.pid
|
36
|
+
socket = $basedir/slave/mysqld.sock
|
37
|
+
port = $slave_port
|
38
|
+
log_output = FILE
|
39
|
+
log-error = $basedir/slave/error.log
|
40
|
+
datadir = $basedir/slave/data
|
41
|
+
relay-log = slave-relay-bin
|
42
|
+
relay-log-index = slave-relay-bin.index
|
43
|
+
server-id = 2
|
44
|
+
|
45
|
+
# replication (optional filters)
|
46
|
+
|
47
|
+
# replicate-do-table = lhm.users
|
48
|
+
# replicate-do-table = lhm.lhmn_users
|
49
|
+
# replicate-wild-do-table = lhm.lhma_%_users
|
50
|
+
|
51
|
+
# replicate-do-table = lhm.origin
|
52
|
+
# replicate-do-table = lhm.lhmn_origin
|
53
|
+
# replicate-wild-do-table = lhm.lhma_%_origin
|
54
|
+
|
55
|
+
# replicate-do-table = lhm.destination
|
56
|
+
# replicate-do-table = lhm.lhmn_destination
|
57
|
+
# replicate-wild-do-table = lhm.lhma_%_destination
|
58
|
+
CNF
|
59
|
+
|
60
|
+
# build system tables
|
61
|
+
|
62
|
+
(
|
63
|
+
cd "$mysqldir"
|
64
|
+
$install_bin --datadir="$basedir/master/data"
|
65
|
+
$install_bin --datadir="$basedir/slave/data"
|
66
|
+
|
67
|
+
)
|
data/bin/lhm-test-all.sh
ADDED
data/lhm.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$:.unshift(lib) unless $:.include?(lib)
|
5
|
+
|
6
|
+
require 'lhm/version'
|
7
|
+
|
8
|
+
Gem::Specification.new do |s|
|
9
|
+
s.name = "sbader-lhm"
|
10
|
+
s.version = Lhm::VERSION
|
11
|
+
s.platform = Gem::Platform::RUBY
|
12
|
+
s.authors = ["SoundCloud", "Rany Keddo", "Tobias Bielohlawek", "Tobias Schmidt"]
|
13
|
+
s.email = %q{rany@soundcloud.com, tobi@soundcloud.com, ts@soundcloud.com}
|
14
|
+
s.summary = %q{online schema changer for mysql}
|
15
|
+
s.description = %q{Migrate large tables without downtime by copying to a temporary table in chunks. The old table is not dropped. Instead, it is moved to timestamp_table_name for verification.}
|
16
|
+
s.homepage = %q{http://github.com/soundcloud/large-hadron-migrator}
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
s.executables = ["lhm-kill-queue"]
|
21
|
+
|
22
|
+
s.add_development_dependency "minitest", "= 2.10.0"
|
23
|
+
s.add_development_dependency "rake"
|
24
|
+
|
25
|
+
s.add_dependency "activerecord"
|
26
|
+
end
|
27
|
+
|
data/lib/lhm.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
2
|
+
# Schmidt
|
3
|
+
|
4
|
+
require 'active_record'
|
5
|
+
require 'lhm/table'
|
6
|
+
require 'lhm/invoker'
|
7
|
+
require 'lhm/version'
|
8
|
+
|
9
|
+
# Large hadron migrator - online schema change tool
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
#
|
13
|
+
# Lhm.change_table(:users) do |m|
|
14
|
+
# m.add_column(:arbitrary, "INT(12)")
|
15
|
+
# m.add_index([:arbitrary, :created_at])
|
16
|
+
# m.ddl("alter table %s add column flag tinyint(1)" % m.name)
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
module Lhm
|
20
|
+
|
21
|
+
# Alters a table with the changes described in the block
|
22
|
+
#
|
23
|
+
# @param [String, Symbol] table_name Name of the table
|
24
|
+
# @param [Hash] options Optional options to alter the chunk / switch behavior
|
25
|
+
# @option options [Fixnum] :stride
|
26
|
+
# Size of a chunk (defaults to: 40,000)
|
27
|
+
# @option options [Fixnum] :throttle
|
28
|
+
# Time to wait between chunks in milliseconds (defaults to: 100)
|
29
|
+
# @option options [Boolean] :atomic_switch
|
30
|
+
# Use atomic switch to rename tables (defaults to: true)
|
31
|
+
# If using a version of mysql affected by atomic switch bug, LHM forces user
|
32
|
+
# to set this option (see SqlHelper#supports_atomic_switch?)
|
33
|
+
# @yield [Migrator] Yielded Migrator object records the changes
|
34
|
+
# @return [Boolean] Returns true if the migration finishes
|
35
|
+
# @raise [Error] Raises Lhm::Error in case of a error and aborts the migration
|
36
|
+
def self.change_table(table_name, options = {}, &block)
|
37
|
+
connection = ActiveRecord::Base.connection
|
38
|
+
origin = Table.parse(table_name, connection)
|
39
|
+
invoker = Invoker.new(origin, connection)
|
40
|
+
block.call(invoker.migrator)
|
41
|
+
invoker.run(options)
|
42
|
+
|
43
|
+
true
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
2
|
+
# Schmidt
|
3
|
+
|
4
|
+
require 'lhm/command'
|
5
|
+
require 'lhm/migration'
|
6
|
+
require 'lhm/sql_helper'
|
7
|
+
|
8
|
+
module Lhm
|
9
|
+
# Switches origin with destination table using an atomic rename.
|
10
|
+
#
|
11
|
+
# It should only be used if the MySQL server version is not affected by the
|
12
|
+
# bin log affecting bug #39675. This can be verified using
|
13
|
+
# Lhm::SqlHelper.supports_atomic_switch?.
|
14
|
+
class AtomicSwitcher
|
15
|
+
include Command
|
16
|
+
include SqlHelper
|
17
|
+
|
18
|
+
attr_reader :connection
|
19
|
+
|
20
|
+
def initialize(migration, connection = nil)
|
21
|
+
@migration = migration
|
22
|
+
@connection = connection
|
23
|
+
@origin = migration.origin
|
24
|
+
@destination = migration.destination
|
25
|
+
end
|
26
|
+
|
27
|
+
def statements
|
28
|
+
atomic_switch
|
29
|
+
end
|
30
|
+
|
31
|
+
def atomic_switch
|
32
|
+
[
|
33
|
+
"rename table `#{ @origin.name }` to `#{ @migration.archive_name }`, " +
|
34
|
+
"`#{ @destination.name }` to `#{ @origin.name }`"
|
35
|
+
]
|
36
|
+
end
|
37
|
+
|
38
|
+
def validate
|
39
|
+
unless table?(@origin.name) && table?(@destination.name)
|
40
|
+
error "`#{ @origin.name }` and `#{ @destination.name }` must exist"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
def execute
|
46
|
+
sql statements
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/lhm/chunker.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
# Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
2
|
+
# Schmidt
|
3
|
+
|
4
|
+
require 'lhm/command'
|
5
|
+
require 'lhm/sql_helper'
|
6
|
+
|
7
|
+
module Lhm
|
8
|
+
class Chunker
|
9
|
+
include Command
|
10
|
+
include SqlHelper
|
11
|
+
|
12
|
+
attr_reader :connection
|
13
|
+
|
14
|
+
# Copy from origin to destination in chunks of size `stride`. Sleeps for
|
15
|
+
# `throttle` milliseconds between each stride.
|
16
|
+
def initialize(migration, connection = nil, options = {})
|
17
|
+
@migration = migration
|
18
|
+
@connection = connection
|
19
|
+
@stride = options[:stride] || 40_000
|
20
|
+
@throttle = options[:throttle] || 100
|
21
|
+
@start = options[:start] || select_start
|
22
|
+
@limit = options[:limit] || select_limit
|
23
|
+
end
|
24
|
+
|
25
|
+
# Copies chunks of size `stride`, starting from `start` up to id `limit`.
|
26
|
+
def up_to(&block)
|
27
|
+
1.upto(traversable_chunks_size) do |n|
|
28
|
+
yield(bottom(n), top(n))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def traversable_chunks_size
|
33
|
+
@limit && @start ? ((@limit - @start + 1) / @stride.to_f).ceil : 0
|
34
|
+
end
|
35
|
+
|
36
|
+
def bottom(chunk)
|
37
|
+
(chunk - 1) * @stride + @start
|
38
|
+
end
|
39
|
+
|
40
|
+
def top(chunk)
|
41
|
+
[chunk * @stride + @start - 1, @limit].min
|
42
|
+
end
|
43
|
+
|
44
|
+
def copy(lowest, highest)
|
45
|
+
"insert ignore into `#{ destination_name }` (#{ columns }) " +
|
46
|
+
"select #{ columns_with_joins } from `#{ origin_name }` " +
|
47
|
+
"#{ joins } " +
|
48
|
+
"where #{origin_name}.`id` between #{ lowest } and #{ highest }"
|
49
|
+
end
|
50
|
+
|
51
|
+
def joins
|
52
|
+
@migration.insert_joins.map{|j| "join #{ j[:table] } on #{ j[:statement] }"}.join(" ")
|
53
|
+
end
|
54
|
+
|
55
|
+
def join_fields_typed
|
56
|
+
@migration.insert_joins.map {|j| "#{j[:table]}.`#{j[:origin_field]}`"}
|
57
|
+
end
|
58
|
+
|
59
|
+
def join_fields
|
60
|
+
@migration.insert_joins.map {|j| "`#{ j[:destination_field] }`"}
|
61
|
+
end
|
62
|
+
|
63
|
+
def select_start
|
64
|
+
start = connection.select_value("select min(id) from #{ origin_name }")
|
65
|
+
start ? start.to_i : nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def select_limit
|
69
|
+
limit = connection.select_value("select max(id) from #{ origin_name }")
|
70
|
+
limit ? limit.to_i : nil
|
71
|
+
end
|
72
|
+
|
73
|
+
def throttle_seconds
|
74
|
+
@throttle / 1000.0
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def destination_name
|
80
|
+
@migration.destination.name
|
81
|
+
end
|
82
|
+
|
83
|
+
def origin_name
|
84
|
+
@migration.origin.name
|
85
|
+
end
|
86
|
+
|
87
|
+
def columns
|
88
|
+
@columns ||= (@migration.intersection.escaped + join_fields).join(", ")
|
89
|
+
end
|
90
|
+
|
91
|
+
def columns_with_joins
|
92
|
+
@columns_with_joins ||= (@migration.intersection.typed_unjoined(origin_name) + join_fields_typed).join(", ")
|
93
|
+
end
|
94
|
+
|
95
|
+
def validate
|
96
|
+
if @start && @limit && @start > @limit
|
97
|
+
error("impossible chunk options (limit must be greater than start)")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def execute
|
102
|
+
up_to do |lowest, highest|
|
103
|
+
affected_rows = update(copy(lowest, highest))
|
104
|
+
|
105
|
+
if affected_rows > 0
|
106
|
+
sleep(throttle_seconds)
|
107
|
+
end
|
108
|
+
|
109
|
+
print "."
|
110
|
+
end
|
111
|
+
print "\n"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
data/lib/lhm/command.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
2
|
+
# Schmidt
|
3
|
+
|
4
|
+
module Lhm
|
5
|
+
class Error < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
module Command
|
9
|
+
def run(&block)
|
10
|
+
validate
|
11
|
+
|
12
|
+
if(block_given?)
|
13
|
+
before
|
14
|
+
block.call(self)
|
15
|
+
after
|
16
|
+
else
|
17
|
+
execute
|
18
|
+
end
|
19
|
+
rescue
|
20
|
+
revert
|
21
|
+
raise
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def validate
|
27
|
+
end
|
28
|
+
|
29
|
+
def revert
|
30
|
+
end
|
31
|
+
|
32
|
+
def execute
|
33
|
+
raise NotImplementedError.new(self.class.name)
|
34
|
+
end
|
35
|
+
|
36
|
+
def before
|
37
|
+
end
|
38
|
+
|
39
|
+
def after
|
40
|
+
end
|
41
|
+
|
42
|
+
def error(msg)
|
43
|
+
raise Error.new(msg)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|