lhm 1.0.3 → 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/.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 []
|
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 [][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