lhm 1.0.3 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +4 -10
- data/CHANGELOG.md +6 -0
- data/README.md +85 -43
- data/bin/lhm-test-all.sh +10 -0
- data/gemfiles/{ar-2.3.gemfile → ar-2.3_mysql.gemfile} +2 -1
- data/gemfiles/ar-3.2_mysql.gemfile +5 -0
- data/gemfiles/ar-3.2_mysql2.gemfile +5 -0
- data/lhm.gemspec +0 -2
- data/lib/lhm.rb +9 -6
- data/lib/lhm/atomic_switcher.rb +49 -0
- data/lib/lhm/chunker.rb +1 -0
- data/lib/lhm/invoker.rb +21 -4
- data/lib/lhm/locked_switcher.rb +1 -5
- data/lib/lhm/migrator.rb +16 -8
- data/lib/lhm/sql_helper.rb +32 -2
- data/lib/lhm/table.rb +3 -1
- data/lib/lhm/version.rb +1 -1
- data/spec/integration/atomic_switcher_spec.rb +42 -0
- data/spec/integration/integration_helper.rb +31 -11
- data/spec/integration/lhm_spec.rb +38 -16
- data/spec/unit/atomic_switcher_spec.rb +31 -0
- data/spec/unit/locked_switcher_spec.rb +1 -1
- data/spec/unit/migrator_spec.rb +35 -3
- metadata +70 -63
- data/gemfiles/ar-3.1.gemfile +0 -4
data/.travis.yml
CHANGED
@@ -1,16 +1,10 @@
|
|
1
|
+
language: ruby
|
1
2
|
before_script:
|
2
3
|
- "mysql -e 'create database lhm;'"
|
3
4
|
rvm:
|
4
5
|
- 1.8.7
|
5
6
|
- 1.9.3
|
6
7
|
gemfile:
|
7
|
-
- gemfiles/ar-2.
|
8
|
-
- gemfiles/ar-3.
|
9
|
-
|
10
|
-
exclude:
|
11
|
-
- rvm: 1.8.7
|
12
|
-
gemfile: gemfiles/ar-2.3.gemfile
|
13
|
-
branches:
|
14
|
-
only:
|
15
|
-
- master
|
16
|
-
- stable
|
8
|
+
- gemfiles/ar-2.3_mysql.gemfile
|
9
|
+
- gemfiles/ar-3.2_mysql.gemfile
|
10
|
+
- gemfiles/ar-3.2_mysql2.gemfile
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,4 @@
|
|
1
|
-
# Large Hadron Migrator [![Build Status](https://secure.travis-ci.org/soundcloud/large-hadron-migrator.png)]
|
2
|
-
|
3
|
-
Update: There is currently [An issue](https://github.com/soundcloud/large-hadron-migrator/issues/11) with the migration. Fix coming up.
|
1
|
+
# Large Hadron Migrator [![Build Status](https://secure.travis-ci.org/soundcloud/large-hadron-migrator.png)][4]
|
4
2
|
|
5
3
|
Rails style database migrations are a useful way to evolve your data schema in
|
6
4
|
an agile manner. Most Rails projects start like this, and at first, making
|
@@ -22,7 +20,7 @@ table. The InnoDB Plugin provides facilities for online index creation, which
|
|
22
20
|
is great if you are using this engine, but only solves half the problem.
|
23
21
|
|
24
22
|
At SoundCloud we started having migration pains quite a while ago, and after
|
25
|
-
looking around for third party solutions
|
23
|
+
looking around for third party solutions, we decided to create our
|
26
24
|
own. We called it Large Hadron Migrator, and it is a gem for online
|
27
25
|
ActiveRecord migrations.
|
28
26
|
|
@@ -33,62 +31,100 @@ ActiveRecord migrations.
|
|
33
31
|
## The idea
|
34
32
|
|
35
33
|
The basic idea is to perform the migration online while the system is live,
|
36
|
-
without locking the table.
|
37
|
-
facebook tool
|
34
|
+
without locking the table. In contrast to [OAK][0] and the
|
35
|
+
[facebook tool][1], we only use a copy table and triggers.
|
38
36
|
|
39
37
|
The Large Hadron is a test driven Ruby solution which can easily be dropped
|
40
38
|
into an ActiveRecord migration. It presumes a single auto incremented
|
41
39
|
numerical primary key called id as per the Rails convention. Unlike the
|
42
|
-
twitter solution
|
40
|
+
[twitter solution][2], it does not require the presence of an indexed
|
43
41
|
`updated_at` column.
|
44
42
|
|
43
|
+
## Requirements
|
44
|
+
|
45
|
+
Lhm currently only works with MySQL databases and requires an established
|
46
|
+
ActiveRecord connection.
|
47
|
+
|
48
|
+
It is compatible and [continuously tested][4] with Ruby 1.8.7 and Ruby 1.9.x,
|
49
|
+
ActiveRecord 2.3.x and 3.x as well as mysql and mysql2 adapters.
|
50
|
+
|
51
|
+
## Installation
|
52
|
+
|
53
|
+
Install it via `gem install lhm` or add `gem "lhm"` to your Gemfile.
|
54
|
+
|
45
55
|
## Usage
|
46
56
|
|
47
|
-
You can invoke Lhm directly from a plain ruby file after connecting
|
48
|
-
|
57
|
+
You can invoke Lhm directly from a plain ruby file after connecting ActiveRecord
|
58
|
+
to your mysql instance:
|
49
59
|
|
50
|
-
|
60
|
+
```ruby
|
61
|
+
require 'lhm'
|
51
62
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
63
|
+
ActiveRecord::Base.establish_connection(
|
64
|
+
:adapter => 'mysql',
|
65
|
+
:host => '127.0.0.1',
|
66
|
+
:database => 'lhm'
|
67
|
+
)
|
57
68
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
69
|
+
Lhm.change_table :users do |m|
|
70
|
+
m.add_column :arbitrary, "INT(12)"
|
71
|
+
m.add_index [:arbitrary_id, :created_at]
|
72
|
+
m.ddl("alter table %s add column flag tinyint(1)" % m.name)
|
73
|
+
end
|
74
|
+
```
|
63
75
|
|
64
76
|
To use Lhm from an ActiveRecord::Migration in a Rails project, add it to your
|
65
77
|
Gemfile, then invoke as follows:
|
66
78
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
79
|
+
```ruby
|
80
|
+
require 'lhm'
|
81
|
+
|
82
|
+
class MigrateUsers < ActiveRecord::Migration
|
83
|
+
def self.up
|
84
|
+
Lhm.change_table :users do |m|
|
85
|
+
m.add_column :arbitrary, "INT(12)"
|
86
|
+
m.add_index [:arbitrary_id, :created_at]
|
87
|
+
m.ddl("alter table %s add column flag tinyint(1)" % m.name)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.down
|
92
|
+
Lhm.change_table :users do |m|
|
93
|
+
m.remove_index [:arbitrary_id, :created_at]
|
94
|
+
m.remove_column :arbitrary)
|
83
95
|
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
```
|
99
|
+
|
100
|
+
## Table rename strategies
|
101
|
+
|
102
|
+
There are two different table rename strategies available: LockedSwitcher and
|
103
|
+
AtomicSwitcher.
|
104
|
+
|
105
|
+
For all setups which use replication and a MySQL version
|
106
|
+
affected by the the [binlog bug #39675](http://bugs.mysql.com/bug.php?id=39675),
|
107
|
+
we recommend the LockedSwitcher strategy to avoid replication issues. This
|
108
|
+
strategy locks the table being migrated and issues two ALTER TABLE statements.
|
109
|
+
The AtomicSwitcher uses a single atomic RENAME TABLE query and should be favored
|
110
|
+
in setups which do not suffer from the mentioned replication bug.
|
111
|
+
|
112
|
+
Lhm chooses the strategy automatically based on the used MySQL server version,
|
113
|
+
but you can override the behavior with an option:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
Lhm.change_table :users, :atomic_switch => true do |m|
|
117
|
+
# ...
|
118
|
+
end
|
119
|
+
```
|
84
120
|
|
85
121
|
## Contributing
|
86
122
|
|
87
123
|
We'll check out your contribution if you:
|
88
124
|
|
89
|
-
|
90
|
-
|
91
|
-
|
125
|
+
* Provide a comprehensive suite of tests for your fork.
|
126
|
+
* Have a clear and documented rationale for your changes.
|
127
|
+
* Package these up in a pull request.
|
92
128
|
|
93
129
|
We'll do our best to help you out with any contribution issues you may have.
|
94
130
|
|
@@ -96,9 +132,15 @@ We'll do our best to help you out with any contribution issues you may have.
|
|
96
132
|
|
97
133
|
The license is included as LICENSE in this directory.
|
98
134
|
|
99
|
-
##
|
135
|
+
## Similar solutions
|
100
136
|
|
101
|
-
[
|
102
|
-
[1]
|
103
|
-
[2]
|
137
|
+
* [OAK: online alter table][0]
|
138
|
+
* [Facebook][1]
|
139
|
+
* [Twitter][2]
|
140
|
+
* [pt-online-schema-change][3]
|
104
141
|
|
142
|
+
[0]: http://openarkkit.googlecode.com
|
143
|
+
[1]: http://www.facebook.com/note.php?note\_id=430801045932
|
144
|
+
[2]: https://github.com/freels/table_migrator
|
145
|
+
[3]: http://www.percona.com/doc/percona-toolkit/2.1/pt-online-schema-change.html
|
146
|
+
[4]: http://travis-ci.org/soundcloud/large-hadron-migrator
|
data/bin/lhm-test-all.sh
ADDED
data/lhm.gemspec
CHANGED
@@ -19,8 +19,6 @@ Gem::Specification.new do |s|
|
|
19
19
|
s.require_paths = ["lib"]
|
20
20
|
s.executables = ["lhm-kill-queue"]
|
21
21
|
|
22
|
-
# this should be a real dependency, but we're using a different gem in our code
|
23
|
-
s.add_development_dependency "mysql", "~> 2.8.1"
|
24
22
|
s.add_development_dependency "minitest", "= 2.10.0"
|
25
23
|
s.add_development_dependency "rake"
|
26
24
|
|
data/lib/lhm.rb
CHANGED
@@ -21,22 +21,25 @@ module Lhm
|
|
21
21
|
# Alters a table with the changes described in the block
|
22
22
|
#
|
23
23
|
# @param [String, Symbol] table_name Name of the table
|
24
|
-
# @param [Hash]
|
25
|
-
# @option
|
24
|
+
# @param [Hash] options Optional options to alter the chunk / switch behavior
|
25
|
+
# @option options [Fixnum] :stride
|
26
26
|
# Size of a chunk (defaults to: 40,000)
|
27
|
-
# @option
|
27
|
+
# @option options [Fixnum] :throttle
|
28
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?)
|
29
33
|
# @yield [Migrator] Yielded Migrator object records the changes
|
30
34
|
# @return [Boolean] Returns true if the migration finishes
|
31
35
|
# @raise [Error] Raises Lhm::Error in case of a error and aborts the migration
|
32
|
-
def self.change_table(table_name,
|
36
|
+
def self.change_table(table_name, options = {}, &block)
|
33
37
|
connection = ActiveRecord::Base.connection
|
34
38
|
origin = Table.parse(table_name, connection)
|
35
39
|
invoker = Invoker.new(origin, connection)
|
36
40
|
block.call(invoker.migrator)
|
37
|
-
invoker.run(
|
41
|
+
invoker.run(options)
|
38
42
|
|
39
43
|
true
|
40
44
|
end
|
41
45
|
end
|
42
|
-
|
@@ -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
CHANGED
data/lib/lhm/invoker.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
|
4
4
|
require 'lhm/chunker'
|
5
5
|
require 'lhm/entangler'
|
6
|
+
require 'lhm/atomic_switcher'
|
6
7
|
require 'lhm/locked_switcher'
|
7
8
|
require 'lhm/migrator'
|
8
9
|
|
@@ -13,19 +14,35 @@ module Lhm
|
|
13
14
|
# Once the origin and destination tables have converged, origin is archived
|
14
15
|
# and replaced by destination.
|
15
16
|
class Invoker
|
16
|
-
|
17
|
+
include SqlHelper
|
18
|
+
|
19
|
+
attr_reader :migrator, :connection
|
17
20
|
|
18
21
|
def initialize(origin, connection)
|
19
22
|
@connection = connection
|
20
23
|
@migrator = Migrator.new(origin, connection)
|
21
24
|
end
|
22
25
|
|
23
|
-
def run(
|
26
|
+
def run(options = {})
|
27
|
+
if !options.include?(:atomic_switch)
|
28
|
+
if supports_atomic_switch?
|
29
|
+
options[:atomic_switch] = true
|
30
|
+
else
|
31
|
+
raise Error.new(
|
32
|
+
"Using mysql #{version_string}. You must explicitly set " +
|
33
|
+
"options[:atomic_switch] (re SqlHelper#supports_atomic_switch?)")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
24
37
|
migration = @migrator.run
|
25
38
|
|
26
39
|
Entangler.new(migration, @connection).run do
|
27
|
-
Chunker.new(migration, @connection,
|
28
|
-
|
40
|
+
Chunker.new(migration, @connection, options).run
|
41
|
+
if options[:atomic_switch]
|
42
|
+
AtomicSwitcher.new(migration, @connection).run
|
43
|
+
else
|
44
|
+
LockedSwitcher.new(migration, @connection).run
|
45
|
+
end
|
29
46
|
end
|
30
47
|
end
|
31
48
|
end
|
data/lib/lhm/locked_switcher.rb
CHANGED
@@ -6,11 +6,7 @@ require 'lhm/migration'
|
|
6
6
|
require 'lhm/sql_helper'
|
7
7
|
|
8
8
|
module Lhm
|
9
|
-
# Switches origin with destination table
|
10
|
-
# a safe alternative to rename, which can cause slave inconsistencies:
|
11
|
-
#
|
12
|
-
# http://bugs.mysql.com/bug.php?id=39675
|
13
|
-
#
|
9
|
+
# Switches origin with destination table nonatomically using a locked write.
|
14
10
|
# LockedSwitcher adopts the Facebook strategy, with the following caveat:
|
15
11
|
#
|
16
12
|
# "Since alter table causes an implicit commit in innodb, innodb locks get
|
data/lib/lhm/migrator.rb
CHANGED
@@ -95,8 +95,10 @@ module Lhm
|
|
95
95
|
# @param [String, Symbol, Array<String, Symbol>] columns
|
96
96
|
# A column name given as String or Symbol. An Array of Strings or Symbols
|
97
97
|
# for compound indexes. It's possible to pass a length limit.
|
98
|
-
|
99
|
-
|
98
|
+
# @param [String, Symbol] index_name
|
99
|
+
# Optional name of the index to be created
|
100
|
+
def add_index(columns, index_name = nil)
|
101
|
+
ddl(index_ddl(columns, false, index_name))
|
100
102
|
end
|
101
103
|
|
102
104
|
# Add a unique index to a table
|
@@ -112,8 +114,10 @@ module Lhm
|
|
112
114
|
# @param [String, Symbol, Array<String, Symbol>] columns
|
113
115
|
# A column name given as String or Symbol. An Array of Strings or Symbols
|
114
116
|
# for compound indexes. It's possible to pass a length limit.
|
115
|
-
|
116
|
-
|
117
|
+
# @param [String, Symbol] index_name
|
118
|
+
# Optional name of the index to be created
|
119
|
+
def add_unique_index(columns, index_name = nil)
|
120
|
+
ddl(index_ddl(columns, true, index_name))
|
117
121
|
end
|
118
122
|
|
119
123
|
# Remove an index from a table
|
@@ -128,8 +132,11 @@ module Lhm
|
|
128
132
|
# @param [String, Symbol, Array<String, Symbol>] columns
|
129
133
|
# A column name given as String or Symbol. An Array of Strings or Symbols
|
130
134
|
# for compound indexes.
|
131
|
-
|
132
|
-
|
135
|
+
# @param [String, Symbol] index_name
|
136
|
+
# Optional name of the index to be removed
|
137
|
+
def remove_index(columns, index_name = nil)
|
138
|
+
index_name ||= idx_name(@origin.name, columns)
|
139
|
+
ddl("drop index `%s` on `%s`" % [index_name, @name])
|
133
140
|
end
|
134
141
|
|
135
142
|
private
|
@@ -167,9 +174,10 @@ module Lhm
|
|
167
174
|
Table.parse(@origin.destination_name, connection)
|
168
175
|
end
|
169
176
|
|
170
|
-
def index_ddl(cols, unique = nil)
|
177
|
+
def index_ddl(cols, unique = nil, index_name = nil)
|
171
178
|
type = unique ? "unique index" : "index"
|
172
|
-
|
179
|
+
index_name ||= idx_name(@origin.name, cols)
|
180
|
+
parts = [type, index_name, @name, idx_spec(cols)]
|
173
181
|
"create %s `%s` on `%s` (%s)" % parts
|
174
182
|
end
|
175
183
|
end
|
data/lib/lhm/sql_helper.rb
CHANGED
@@ -28,7 +28,7 @@ module Lhm
|
|
28
28
|
[statements].flatten.each do |statement|
|
29
29
|
connection.execute(tagged(statement))
|
30
30
|
end
|
31
|
-
rescue ActiveRecord::StatementInvalid
|
31
|
+
rescue ActiveRecord::StatementInvalid => e
|
32
32
|
error e.message
|
33
33
|
end
|
34
34
|
|
@@ -36,10 +36,14 @@ module Lhm
|
|
36
36
|
[statements].flatten.inject(0) do |memo, statement|
|
37
37
|
memo += connection.update(tagged(statement))
|
38
38
|
end
|
39
|
-
rescue ActiveRecord::StatementInvalid
|
39
|
+
rescue ActiveRecord::StatementInvalid => e
|
40
40
|
error e.message
|
41
41
|
end
|
42
42
|
|
43
|
+
def version_string
|
44
|
+
connection.select_one("show variables like 'version'")["Value"]
|
45
|
+
end
|
46
|
+
|
43
47
|
private
|
44
48
|
|
45
49
|
def tagged(statement)
|
@@ -51,5 +55,31 @@ module Lhm
|
|
51
55
|
column.to_s.match(/`?([^\(]+)`?(\([^\)]+\))?/).captures
|
52
56
|
end
|
53
57
|
end
|
58
|
+
|
59
|
+
# Older versions of MySQL contain an atomic rename bug affecting bin
|
60
|
+
# log order. Affected versions extracted from bug report:
|
61
|
+
#
|
62
|
+
# http://bugs.mysql.com/bug.php?id=39675
|
63
|
+
#
|
64
|
+
# More Info: http://dev.mysql.com/doc/refman/5.5/en/metadata-locking.html
|
65
|
+
def supports_atomic_switch?
|
66
|
+
major, minor, tiny = version_string.split('.').map(&:to_i)
|
67
|
+
|
68
|
+
case major
|
69
|
+
when 4 then return false if minor and minor < 2
|
70
|
+
when 5
|
71
|
+
case minor
|
72
|
+
when 0 then return false if tiny and tiny < 52
|
73
|
+
when 1 then return false
|
74
|
+
when 4 then return false if tiny and tiny < 4
|
75
|
+
when 5 then return false if tiny and tiny < 3
|
76
|
+
end
|
77
|
+
when 6
|
78
|
+
case minor
|
79
|
+
when 0 then return false if tiny and tiny < 11
|
80
|
+
end
|
81
|
+
end
|
82
|
+
return true
|
83
|
+
end
|
54
84
|
end
|
55
85
|
end
|
data/lib/lhm/table.rb
CHANGED
data/lib/lhm/version.rb
CHANGED
@@ -0,0 +1,42 @@
|
|
1
|
+
# Copyright (c) 2011, 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
|
+
|
10
|
+
describe Lhm::AtomicSwitcher do
|
11
|
+
include IntegrationHelper
|
12
|
+
|
13
|
+
before(:each) { connect_master! }
|
14
|
+
|
15
|
+
describe "switching" do
|
16
|
+
before(:each) do
|
17
|
+
@origin = table_create("origin")
|
18
|
+
@destination = table_create("destination")
|
19
|
+
@migration = Lhm::Migration.new(@origin, @destination)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "rename origin to archive" do
|
23
|
+
switcher = Lhm::AtomicSwitcher.new(@migration, connection)
|
24
|
+
switcher.run
|
25
|
+
|
26
|
+
slave do
|
27
|
+
table_exists?(@origin).must_equal true
|
28
|
+
table_read(@migration.archive_name).columns.keys.must_include "origin"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it "rename destination to origin" do
|
33
|
+
switcher = Lhm::AtomicSwitcher.new(@migration, connection)
|
34
|
+
switcher.run
|
35
|
+
|
36
|
+
slave do
|
37
|
+
table_exists?(@destination).must_equal false
|
38
|
+
table_read(@origin.name).columns.keys.must_include "destination"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -4,34 +4,39 @@
|
|
4
4
|
require File.expand_path(File.dirname(__FILE__)) + "/../bootstrap"
|
5
5
|
|
6
6
|
require 'active_record'
|
7
|
+
begin
|
8
|
+
require 'mysql2'
|
9
|
+
rescue LoadError
|
10
|
+
require 'mysql'
|
11
|
+
end
|
7
12
|
require 'lhm/table'
|
8
13
|
require 'lhm/sql_helper'
|
9
14
|
|
10
15
|
module IntegrationHelper
|
11
|
-
attr_accessor :connection
|
12
|
-
|
13
16
|
#
|
14
17
|
# Connectivity
|
15
18
|
#
|
16
19
|
|
20
|
+
def connection
|
21
|
+
ActiveRecord::Base.connection
|
22
|
+
end
|
23
|
+
|
17
24
|
def connect_master!
|
18
|
-
|
25
|
+
connect!(3306)
|
19
26
|
end
|
20
27
|
|
21
28
|
def connect_slave!
|
22
|
-
|
29
|
+
connect!(3307)
|
23
30
|
end
|
24
31
|
|
25
32
|
def connect!(port)
|
26
33
|
ActiveRecord::Base.establish_connection(
|
27
|
-
:adapter => 'mysql',
|
34
|
+
:adapter => defined?(Mysql2) ? 'mysql2' : 'mysql',
|
28
35
|
:host => '127.0.0.1',
|
29
36
|
:database => 'lhm',
|
30
37
|
:username => '',
|
31
38
|
:port => port
|
32
39
|
)
|
33
|
-
|
34
|
-
ActiveRecord::Base.connection
|
35
40
|
end
|
36
41
|
|
37
42
|
def select_one(*args)
|
@@ -43,7 +48,17 @@ module IntegrationHelper
|
|
43
48
|
end
|
44
49
|
|
45
50
|
def execute(*args)
|
46
|
-
|
51
|
+
retries = 10
|
52
|
+
begin
|
53
|
+
connection.execute(*args)
|
54
|
+
rescue ActiveRecord::StatementInvalid => e
|
55
|
+
if (retries -= 1) > 0 && e.message =~ /Table '.*?' doesn't exist/
|
56
|
+
sleep 0.1
|
57
|
+
retry
|
58
|
+
else
|
59
|
+
raise
|
60
|
+
end
|
61
|
+
end
|
47
62
|
end
|
48
63
|
|
49
64
|
def slave(&block)
|
@@ -99,11 +114,16 @@ module IntegrationHelper
|
|
99
114
|
select_value(query).to_i
|
100
115
|
end
|
101
116
|
|
102
|
-
def
|
103
|
-
non_unique = type == :non_unique ? 1 : 0
|
117
|
+
def index_on_columns?(table_name, cols, type = :non_unique)
|
104
118
|
key_name = Lhm::SqlHelper.idx_name(table_name, cols)
|
105
119
|
|
106
|
-
|
120
|
+
index?(table_name, key_name, type)
|
121
|
+
end
|
122
|
+
|
123
|
+
def index?(table_name, key_name, type = :non_unique)
|
124
|
+
non_unique = type == :non_unique ? 1 : 0
|
125
|
+
|
126
|
+
!!select_one(%Q<
|
107
127
|
show indexes in #{ table_name }
|
108
128
|
where key_name = '#{ key_name }'
|
109
129
|
and non_unique = #{ non_unique }
|
@@ -16,7 +16,7 @@ describe Lhm do
|
|
16
16
|
end
|
17
17
|
|
18
18
|
it "should add a column" do
|
19
|
-
Lhm.change_table(:users) do |t|
|
19
|
+
Lhm.change_table(:users, :atomic_switch => false) do |t|
|
20
20
|
t.add_column(:logins, "INT(12) DEFAULT '0'")
|
21
21
|
end
|
22
22
|
|
@@ -32,7 +32,7 @@ describe Lhm do
|
|
32
32
|
it "should copy all rows" do
|
33
33
|
23.times { |n| execute("insert into users set reference = '#{ n }'") }
|
34
34
|
|
35
|
-
Lhm.change_table(:users) do |t|
|
35
|
+
Lhm.change_table(:users, :atomic_switch => false) do |t|
|
36
36
|
t.add_column(:logins, "INT(12) DEFAULT '0'")
|
37
37
|
end
|
38
38
|
|
@@ -42,7 +42,7 @@ describe Lhm do
|
|
42
42
|
end
|
43
43
|
|
44
44
|
it "should remove a column" do
|
45
|
-
Lhm.change_table(:users) do |t|
|
45
|
+
Lhm.change_table(:users, :atomic_switch => false) do |t|
|
46
46
|
t.remove_column(:comment)
|
47
47
|
end
|
48
48
|
|
@@ -52,47 +52,67 @@ describe Lhm do
|
|
52
52
|
end
|
53
53
|
|
54
54
|
it "should add an index" do
|
55
|
-
Lhm.change_table(:users) do |t|
|
55
|
+
Lhm.change_table(:users, :atomic_switch => false) do |t|
|
56
56
|
t.add_index([:comment, :created_at])
|
57
57
|
end
|
58
58
|
|
59
59
|
slave do
|
60
|
-
|
60
|
+
index_on_columns?(:users, [:comment, :created_at]).must_equal(true)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should add an index with a custom name" do
|
65
|
+
Lhm.change_table(:users, :atomic_switch => false) do |t|
|
66
|
+
t.add_index([:comment, :created_at], :my_index_name)
|
67
|
+
end
|
68
|
+
|
69
|
+
slave do
|
70
|
+
index?(:users, :my_index_name).must_equal(true)
|
61
71
|
end
|
62
72
|
end
|
63
73
|
|
64
74
|
it "should add an index on a column with a reserved name" do
|
65
|
-
Lhm.change_table(:users) do |t|
|
75
|
+
Lhm.change_table(:users, :atomic_switch => false) do |t|
|
66
76
|
t.add_index(:group)
|
67
77
|
end
|
68
78
|
|
69
79
|
slave do
|
70
|
-
|
80
|
+
index_on_columns?(:users, :group).must_equal(true)
|
71
81
|
end
|
72
82
|
end
|
73
83
|
|
74
84
|
it "should add a unqiue index" do
|
75
|
-
Lhm.change_table(:users) do |t|
|
85
|
+
Lhm.change_table(:users, :atomic_switch => false) do |t|
|
76
86
|
t.add_unique_index(:comment)
|
77
87
|
end
|
78
88
|
|
79
89
|
slave do
|
80
|
-
|
90
|
+
index_on_columns?(:users, :comment, :unique).must_equal(true)
|
81
91
|
end
|
82
92
|
end
|
83
93
|
|
84
94
|
it "should remove an index" do
|
85
|
-
Lhm.change_table(:users) do |t|
|
95
|
+
Lhm.change_table(:users, :atomic_switch => false) do |t|
|
86
96
|
t.remove_index([:username, :created_at])
|
87
97
|
end
|
88
98
|
|
89
99
|
slave do
|
90
|
-
|
100
|
+
index_on_columns?(:users, [:username, :created_at]).must_equal(false)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should remove an index with a custom name" do
|
105
|
+
Lhm.change_table(:users, :atomic_switch => false) do |t|
|
106
|
+
t.remove_index(:reference, :index_users_on_reference)
|
107
|
+
end
|
108
|
+
|
109
|
+
slave do
|
110
|
+
index?(:users, :index_users_on_reference).must_equal(false)
|
91
111
|
end
|
92
112
|
end
|
93
113
|
|
94
114
|
it "should apply a ddl statement" do
|
95
|
-
Lhm.change_table(:users) do |t|
|
115
|
+
Lhm.change_table(:users, :atomic_switch => false) do |t|
|
96
116
|
t.ddl("alter table %s add column flag tinyint(1)" % t.name)
|
97
117
|
end
|
98
118
|
|
@@ -106,7 +126,7 @@ describe Lhm do
|
|
106
126
|
end
|
107
127
|
|
108
128
|
it "should change a column" do
|
109
|
-
Lhm.change_table(:users) do |t|
|
129
|
+
Lhm.change_table(:users, :atomic_switch => false) do |t|
|
110
130
|
t.change_column(:comment, "varchar(20) DEFAULT 'none' NOT NULL")
|
111
131
|
end
|
112
132
|
|
@@ -122,7 +142,7 @@ describe Lhm do
|
|
122
142
|
it "should change the last column in a table" do
|
123
143
|
table_create(:small_table)
|
124
144
|
|
125
|
-
Lhm.change_table(:small_table) do |t|
|
145
|
+
Lhm.change_table(:small_table, :atomic_switch => false) do |t|
|
126
146
|
t.change_column(:id, "int(5)")
|
127
147
|
end
|
128
148
|
|
@@ -146,7 +166,8 @@ describe Lhm do
|
|
146
166
|
end
|
147
167
|
end
|
148
168
|
|
149
|
-
|
169
|
+
options = { :stride => 10, :throttle => 97, :atomic_switch => false }
|
170
|
+
Lhm.change_table(:users, options) do |t|
|
150
171
|
t.add_column(:parallel, "INT(10) DEFAULT '0'")
|
151
172
|
end
|
152
173
|
|
@@ -167,7 +188,8 @@ describe Lhm do
|
|
167
188
|
end
|
168
189
|
end
|
169
190
|
|
170
|
-
|
191
|
+
options = { :stride => 10, :throttle => 97, :atomic_switch => false }
|
192
|
+
Lhm.change_table(:users, options) do |t|
|
171
193
|
t.add_column(:parallel, "INT(10) DEFAULT '0'")
|
172
194
|
end
|
173
195
|
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
2
|
+
# Schmidt
|
3
|
+
|
4
|
+
require File.expand_path(File.dirname(__FILE__)) + '/unit_helper'
|
5
|
+
|
6
|
+
require 'lhm/table'
|
7
|
+
require 'lhm/migration'
|
8
|
+
require 'lhm/atomic_switcher'
|
9
|
+
|
10
|
+
describe Lhm::AtomicSwitcher do
|
11
|
+
include UnitHelper
|
12
|
+
|
13
|
+
before(:each) do
|
14
|
+
@start = Time.now
|
15
|
+
@origin = Lhm::Table.new("origin")
|
16
|
+
@destination = Lhm::Table.new("destination")
|
17
|
+
@migration = Lhm::Migration.new(@origin, @destination, @start)
|
18
|
+
@switcher = Lhm::AtomicSwitcher.new(@migration, nil)
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "atomic switch" do
|
22
|
+
it "should perform a single atomic rename" do
|
23
|
+
@switcher.
|
24
|
+
statements.
|
25
|
+
must_equal([
|
26
|
+
"rename table `origin` to `#{ @migration.archive_name }`, " +
|
27
|
+
"`destination` to `origin`"
|
28
|
+
])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -15,7 +15,7 @@ describe Lhm::LockedSwitcher do
|
|
15
15
|
@origin = Lhm::Table.new("origin")
|
16
16
|
@destination = Lhm::Table.new("destination")
|
17
17
|
@migration = Lhm::Migration.new(@origin, @destination, @start)
|
18
|
-
@switcher = Lhm::LockedSwitcher.new(@migration)
|
18
|
+
@switcher = Lhm::LockedSwitcher.new(@migration, nil)
|
19
19
|
end
|
20
20
|
|
21
21
|
describe "uncommitted" do
|
data/spec/unit/migrator_spec.rb
CHANGED
@@ -16,14 +16,22 @@ describe Lhm::Migrator do
|
|
16
16
|
|
17
17
|
describe "index changes" do
|
18
18
|
it "should add an index" do
|
19
|
-
@creator.add_index(
|
19
|
+
@creator.add_index(:a)
|
20
|
+
|
21
|
+
@creator.statements.must_equal([
|
22
|
+
"create index `index_alt_on_a` on `lhmn_alt` (`a`)"
|
23
|
+
])
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should add a composite index" do
|
27
|
+
@creator.add_index([:a, :b])
|
20
28
|
|
21
29
|
@creator.statements.must_equal([
|
22
30
|
"create index `index_alt_on_a_and_b` on `lhmn_alt` (`a`, `b`)"
|
23
31
|
])
|
24
32
|
end
|
25
33
|
|
26
|
-
it "should add an index with
|
34
|
+
it "should add an index with prefix length" do
|
27
35
|
@creator.add_index(["a(10)", "b"])
|
28
36
|
|
29
37
|
@creator.statements.must_equal([
|
@@ -31,7 +39,15 @@ describe Lhm::Migrator do
|
|
31
39
|
])
|
32
40
|
end
|
33
41
|
|
34
|
-
it "should add an
|
42
|
+
it "should add an index with a custom name" do
|
43
|
+
@creator.add_index([:a, :b], :custom_index_name)
|
44
|
+
|
45
|
+
@creator.statements.must_equal([
|
46
|
+
"create index `custom_index_name` on `lhmn_alt` (`a`, `b`)"
|
47
|
+
])
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should add a unique index" do
|
35
51
|
@creator.add_unique_index(["a(5)", :b])
|
36
52
|
|
37
53
|
@creator.statements.must_equal([
|
@@ -39,6 +55,14 @@ describe Lhm::Migrator do
|
|
39
55
|
])
|
40
56
|
end
|
41
57
|
|
58
|
+
it "should add a unique index with a custom name" do
|
59
|
+
@creator.add_unique_index([:a, :b], :custom_index_name)
|
60
|
+
|
61
|
+
@creator.statements.must_equal([
|
62
|
+
"create unique index `custom_index_name` on `lhmn_alt` (`a`, `b`)"
|
63
|
+
])
|
64
|
+
end
|
65
|
+
|
42
66
|
it "should remove an index" do
|
43
67
|
@creator.remove_index(["b", "a"])
|
44
68
|
|
@@ -46,6 +70,14 @@ describe Lhm::Migrator do
|
|
46
70
|
"drop index `index_alt_on_b_and_a` on `lhmn_alt`"
|
47
71
|
])
|
48
72
|
end
|
73
|
+
|
74
|
+
it "should remove an index with a custom name" do
|
75
|
+
@creator.remove_index([:a, :b], :custom_index_name)
|
76
|
+
|
77
|
+
@creator.statements.must_equal([
|
78
|
+
"drop index `custom_index_name` on `lhmn_alt`"
|
79
|
+
])
|
80
|
+
end
|
49
81
|
end
|
50
82
|
|
51
83
|
describe "column changes" do
|
metadata
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: lhm
|
3
|
-
version: !ruby/object:Gem::Version
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.1.0
|
4
5
|
prerelease:
|
5
|
-
version: 1.0.3
|
6
6
|
platform: ruby
|
7
|
-
authors:
|
7
|
+
authors:
|
8
8
|
- SoundCloud
|
9
9
|
- Rany Keddo
|
10
10
|
- Tobias Bielohlawek
|
@@ -12,62 +12,65 @@ authors:
|
|
12
12
|
autorequire:
|
13
13
|
bindir: bin
|
14
14
|
cert_chain: []
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
prerelease: false
|
21
|
-
requirement: &id001 !ruby/object:Gem::Requirement
|
15
|
+
date: 2012-04-29 00:00:00.000000000 Z
|
16
|
+
dependencies:
|
17
|
+
- !ruby/object:Gem::Dependency
|
18
|
+
name: minitest
|
19
|
+
requirement: !ruby/object:Gem::Requirement
|
22
20
|
none: false
|
23
|
-
requirements:
|
24
|
-
- -
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: 2.
|
21
|
+
requirements:
|
22
|
+
- - '='
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 2.10.0
|
27
25
|
type: :development
|
28
|
-
version_requirements: *id001
|
29
|
-
- !ruby/object:Gem::Dependency
|
30
|
-
name: minitest
|
31
26
|
prerelease: false
|
32
|
-
|
27
|
+
version_requirements: !ruby/object:Gem::Requirement
|
33
28
|
none: false
|
34
|
-
requirements:
|
35
|
-
- -
|
36
|
-
- !ruby/object:Gem::Version
|
29
|
+
requirements:
|
30
|
+
- - '='
|
31
|
+
- !ruby/object:Gem::Version
|
37
32
|
version: 2.10.0
|
38
|
-
|
39
|
-
version_requirements: *id002
|
40
|
-
- !ruby/object:Gem::Dependency
|
33
|
+
- !ruby/object:Gem::Dependency
|
41
34
|
name: rake
|
42
|
-
|
43
|
-
requirement: &id003 !ruby/object:Gem::Requirement
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
44
36
|
none: false
|
45
|
-
requirements:
|
46
|
-
- -
|
47
|
-
- !ruby/object:Gem::Version
|
48
|
-
version:
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
49
41
|
type: :development
|
50
|
-
version_requirements: *id003
|
51
|
-
- !ruby/object:Gem::Dependency
|
52
|
-
name: activerecord
|
53
42
|
prerelease: false
|
54
|
-
|
43
|
+
version_requirements: !ruby/object:Gem::Requirement
|
44
|
+
none: false
|
45
|
+
requirements:
|
46
|
+
- - ! '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: activerecord
|
51
|
+
requirement: !ruby/object:Gem::Requirement
|
55
52
|
none: false
|
56
|
-
requirements:
|
57
|
-
- -
|
58
|
-
- !ruby/object:Gem::Version
|
59
|
-
version:
|
53
|
+
requirements:
|
54
|
+
- - ! '>='
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
60
57
|
type: :runtime
|
61
|
-
|
62
|
-
|
58
|
+
prerelease: false
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ! '>='
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
description: Migrate large tables without downtime by copying to a temporary table
|
66
|
+
in chunks. The old table is not dropped. Instead, it is moved to timestamp_table_name
|
67
|
+
for verification.
|
63
68
|
email: rany@soundcloud.com, tobi@soundcloud.com, ts@soundcloud.com
|
64
|
-
executables:
|
69
|
+
executables:
|
65
70
|
- lhm-kill-queue
|
66
71
|
extensions: []
|
67
|
-
|
68
72
|
extra_rdoc_files: []
|
69
|
-
|
70
|
-
files:
|
73
|
+
files:
|
71
74
|
- .gitignore
|
72
75
|
- .travis.yml
|
73
76
|
- CHANGELOG.md
|
@@ -78,10 +81,13 @@ files:
|
|
78
81
|
- bin/lhm-spec-clobber.sh
|
79
82
|
- bin/lhm-spec-grants.sh
|
80
83
|
- bin/lhm-spec-setup-cluster.sh
|
81
|
-
-
|
82
|
-
- gemfiles/ar-
|
84
|
+
- bin/lhm-test-all.sh
|
85
|
+
- gemfiles/ar-2.3_mysql.gemfile
|
86
|
+
- gemfiles/ar-3.2_mysql.gemfile
|
87
|
+
- gemfiles/ar-3.2_mysql2.gemfile
|
83
88
|
- lhm.gemspec
|
84
89
|
- lib/lhm.rb
|
90
|
+
- lib/lhm/atomic_switcher.rb
|
85
91
|
- lib/lhm/chunker.rb
|
86
92
|
- lib/lhm/command.rb
|
87
93
|
- lib/lhm/entangler.rb
|
@@ -100,12 +106,14 @@ files:
|
|
100
106
|
- spec/fixtures/origin.ddl
|
101
107
|
- spec/fixtures/small_table.ddl
|
102
108
|
- spec/fixtures/users.ddl
|
109
|
+
- spec/integration/atomic_switcher_spec.rb
|
103
110
|
- spec/integration/chunker_spec.rb
|
104
111
|
- spec/integration/entangler_spec.rb
|
105
112
|
- spec/integration/integration_helper.rb
|
106
113
|
- spec/integration/lhm_spec.rb
|
107
114
|
- spec/integration/locked_switcher_spec.rb
|
108
115
|
- spec/integration/table_spec.rb
|
116
|
+
- spec/unit/atomic_switcher_spec.rb
|
109
117
|
- spec/unit/chunker_spec.rb
|
110
118
|
- spec/unit/entangler_spec.rb
|
111
119
|
- spec/unit/intersection_spec.rb
|
@@ -117,44 +125,43 @@ files:
|
|
117
125
|
- spec/unit/unit_helper.rb
|
118
126
|
homepage: http://github.com/soundcloud/large-hadron-migrator
|
119
127
|
licenses: []
|
120
|
-
|
121
128
|
post_install_message:
|
122
129
|
rdoc_options: []
|
123
|
-
|
124
|
-
require_paths:
|
130
|
+
require_paths:
|
125
131
|
- lib
|
126
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
132
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
127
133
|
none: false
|
128
|
-
requirements:
|
129
|
-
- -
|
130
|
-
- !ruby/object:Gem::Version
|
131
|
-
version:
|
132
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ! '>='
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
133
139
|
none: false
|
134
|
-
requirements:
|
135
|
-
- -
|
136
|
-
- !ruby/object:Gem::Version
|
137
|
-
version:
|
140
|
+
requirements:
|
141
|
+
- - ! '>='
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
version: '0'
|
138
144
|
requirements: []
|
139
|
-
|
140
145
|
rubyforge_project:
|
141
|
-
rubygems_version: 1.8.
|
146
|
+
rubygems_version: 1.8.21
|
142
147
|
signing_key:
|
143
148
|
specification_version: 3
|
144
149
|
summary: online schema changer for mysql
|
145
|
-
test_files:
|
150
|
+
test_files:
|
146
151
|
- spec/README.md
|
147
152
|
- spec/bootstrap.rb
|
148
153
|
- spec/fixtures/destination.ddl
|
149
154
|
- spec/fixtures/origin.ddl
|
150
155
|
- spec/fixtures/small_table.ddl
|
151
156
|
- spec/fixtures/users.ddl
|
157
|
+
- spec/integration/atomic_switcher_spec.rb
|
152
158
|
- spec/integration/chunker_spec.rb
|
153
159
|
- spec/integration/entangler_spec.rb
|
154
160
|
- spec/integration/integration_helper.rb
|
155
161
|
- spec/integration/lhm_spec.rb
|
156
162
|
- spec/integration/locked_switcher_spec.rb
|
157
163
|
- spec/integration/table_spec.rb
|
164
|
+
- spec/unit/atomic_switcher_spec.rb
|
158
165
|
- spec/unit/chunker_spec.rb
|
159
166
|
- spec/unit/entangler_spec.rb
|
160
167
|
- spec/unit/intersection_spec.rb
|
data/gemfiles/ar-3.1.gemfile
DELETED