lhm 1.0.0.rc2 → 1.0.0.rc3
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/.config +3 -0
- data/.gitignore +1 -0
- data/.travis.yml +9 -3
- data/CHANGELOG.md +10 -0
- data/README.md +28 -24
- data/Rakefile +2 -1
- data/gemfiles/ar-2.3.gemfile +4 -0
- data/gemfiles/ar-3.1.gemfile +4 -0
- data/lhm.gemspec +1 -1
- data/lib/lhm.rb +30 -8
- data/lib/lhm/chunker.rb +53 -34
- data/lib/lhm/command.rb +15 -42
- data/lib/lhm/entangler.rb +13 -20
- data/lib/lhm/intersection.rb +3 -7
- data/lib/lhm/invoker.rb +9 -14
- data/lib/lhm/locked_switcher.rb +22 -26
- data/lib/lhm/migration.rb +2 -8
- data/lib/lhm/migrator.rb +76 -56
- data/lib/lhm/sql_helper.rb +45 -0
- data/lib/lhm/table.rb +2 -10
- data/lib/lhm/version.rb +6 -0
- data/spec/README.md +26 -0
- data/spec/bootstrap.rb +2 -5
- data/spec/config/.config +3 -0
- data/spec/config/clobber +36 -0
- data/spec/config/grants +25 -0
- data/spec/config/setup-cluster +61 -0
- data/spec/fixtures/destination.ddl +0 -1
- data/spec/fixtures/origin.ddl +0 -1
- data/spec/fixtures/users.ddl +1 -1
- data/spec/integration/chunker_spec.rb +10 -9
- data/spec/integration/entangler_spec.rb +16 -10
- data/spec/integration/integration_helper.rb +43 -12
- data/spec/integration/lhm_spec.rb +66 -42
- data/spec/integration/locked_switcher_spec.rb +11 -10
- data/spec/unit/chunker_spec.rb +50 -18
- data/spec/unit/entangler_spec.rb +2 -5
- data/spec/unit/intersection_spec.rb +2 -5
- data/spec/unit/locked_switcher_spec.rb +2 -5
- data/spec/unit/migration_spec.rb +2 -5
- data/spec/unit/migrator_spec.rb +6 -9
- data/spec/unit/sql_helper_spec.rb +32 -0
- data/spec/unit/table_spec.rb +2 -29
- data/spec/unit/unit_helper.rb +2 -5
- metadata +52 -7
- data/Gemfile +0 -3
data/lib/lhm/intersection.rb
CHANGED
@@ -1,11 +1,8 @@
|
|
1
|
-
#
|
2
|
-
#
|
3
|
-
# Schmidt
|
4
|
-
#
|
5
|
-
# Determine and format columns common to origin and destination.
|
6
|
-
#
|
1
|
+
# Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
2
|
+
# Schmidt
|
7
3
|
|
8
4
|
module Lhm
|
5
|
+
# Determine and format columns common to origin and destination.
|
9
6
|
class Intersection
|
10
7
|
def initialize(origin, destination)
|
11
8
|
@origin = origin
|
@@ -39,4 +36,3 @@ module Lhm
|
|
39
36
|
end
|
40
37
|
end
|
41
38
|
end
|
42
|
-
|
data/lib/lhm/invoker.rb
CHANGED
@@ -1,21 +1,17 @@
|
|
1
|
-
#
|
2
|
-
#
|
3
|
-
# Schmidt
|
4
|
-
#
|
5
|
-
# Copies an origin table to an altered destination table. Live activity is
|
6
|
-
# synchronized into the destination table using triggers.
|
7
|
-
#
|
8
|
-
# Once the origin and destination tables have converged, origin is archived
|
9
|
-
# and replaced by destination.
|
10
|
-
#
|
1
|
+
# Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
2
|
+
# Schmidt
|
11
3
|
|
12
4
|
require 'lhm/chunker'
|
13
5
|
require 'lhm/entangler'
|
14
6
|
require 'lhm/locked_switcher'
|
15
|
-
require 'lhm/migration'
|
16
7
|
require 'lhm/migrator'
|
17
8
|
|
18
9
|
module Lhm
|
10
|
+
# Copies an origin table to an altered destination table. Live activity is
|
11
|
+
# synchronized into the destination table using triggers.
|
12
|
+
#
|
13
|
+
# Once the origin and destination tables have converged, origin is archived
|
14
|
+
# and replaced by destination.
|
19
15
|
class Invoker
|
20
16
|
attr_reader :migrator
|
21
17
|
|
@@ -27,11 +23,10 @@ module Lhm
|
|
27
23
|
def run(chunk_options = {})
|
28
24
|
migration = @migrator.run
|
29
25
|
|
30
|
-
Entangler.new(migration, @connection).run do
|
31
|
-
Chunker.new(migration,
|
26
|
+
Entangler.new(migration, @connection).run do
|
27
|
+
Chunker.new(migration, @connection, chunk_options).run
|
32
28
|
LockedSwitcher.new(migration, @connection).run
|
33
29
|
end
|
34
30
|
end
|
35
31
|
end
|
36
32
|
end
|
37
|
-
|
data/lib/lhm/locked_switcher.rb
CHANGED
@@ -1,28 +1,30 @@
|
|
1
|
-
#
|
2
|
-
#
|
3
|
-
# Schmidt
|
4
|
-
#
|
5
|
-
# Switches origin with destination table with a write lock. Use this as a safe
|
6
|
-
# alternative to rename, which can cause slave inconsistencies:
|
7
|
-
#
|
8
|
-
# http://bugs.mysql.com/bug.php?id=39675
|
9
|
-
#
|
10
|
-
# LockedSwitcher adopts the Facebook strategy, with the following caveat:
|
11
|
-
#
|
12
|
-
# "Since alter table causes an implicit commit in innodb, innodb locks get
|
13
|
-
# released after the first alter table. So any transaction that sneaks in
|
14
|
-
# after the first alter table and before the second alter table gets
|
15
|
-
# a 'table not found' error. The second alter table is expected to be very
|
16
|
-
# fast though because copytable is not visible to other transactions and so
|
17
|
-
# there is no need to wait."
|
18
|
-
#
|
1
|
+
# Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
2
|
+
# Schmidt
|
19
3
|
|
20
4
|
require 'lhm/command'
|
21
5
|
require 'lhm/migration'
|
6
|
+
require 'lhm/sql_helper'
|
22
7
|
|
23
8
|
module Lhm
|
9
|
+
# Switches origin with destination table with a write lock. Use this as
|
10
|
+
# a safe alternative to rename, which can cause slave inconsistencies:
|
11
|
+
#
|
12
|
+
# http://bugs.mysql.com/bug.php?id=39675
|
13
|
+
#
|
14
|
+
# LockedSwitcher adopts the Facebook strategy, with the following caveat:
|
15
|
+
#
|
16
|
+
# "Since alter table causes an implicit commit in innodb, innodb locks get
|
17
|
+
# released after the first alter table. So any transaction that sneaks in
|
18
|
+
# after the first alter table and before the second alter table gets
|
19
|
+
# a 'table not found' error. The second alter table is expected to be very
|
20
|
+
# fast though because copytable is not visible to other transactions and so
|
21
|
+
# there is no need to wait."
|
22
|
+
#
|
24
23
|
class LockedSwitcher
|
25
24
|
include Command
|
25
|
+
include SqlHelper
|
26
|
+
|
27
|
+
attr_reader :connection
|
26
28
|
|
27
29
|
def initialize(migration, connection = nil)
|
28
30
|
@migration = migration
|
@@ -51,29 +53,23 @@ module Lhm
|
|
51
53
|
"set session autocommit = 0",
|
52
54
|
yield,
|
53
55
|
"set session autocommit = @lhm_auto_commit"
|
54
|
-
|
55
56
|
].flatten
|
56
57
|
end
|
57
58
|
|
58
|
-
#
|
59
|
-
# Command interface
|
60
|
-
#
|
61
|
-
|
62
59
|
def validate
|
63
60
|
unless table?(@origin.name) && table?(@destination.name)
|
64
61
|
error "`#{ @origin.name }` and `#{ @destination.name }` must exist"
|
65
62
|
end
|
66
63
|
end
|
67
64
|
|
65
|
+
private
|
66
|
+
|
68
67
|
def revert
|
69
68
|
sql "unlock tables"
|
70
69
|
end
|
71
70
|
|
72
|
-
private
|
73
|
-
|
74
71
|
def execute
|
75
72
|
sql statements
|
76
73
|
end
|
77
74
|
end
|
78
75
|
end
|
79
|
-
|
data/lib/lhm/migration.rb
CHANGED
@@ -1,9 +1,6 @@
|
|
1
|
-
#
|
2
|
-
#
|
3
|
-
# Schmidt
|
4
|
-
#
|
1
|
+
# Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
2
|
+
# Schmidt
|
5
3
|
|
6
|
-
require 'lhm/table'
|
7
4
|
require 'lhm/intersection'
|
8
5
|
|
9
6
|
module Lhm
|
@@ -24,11 +21,8 @@ module Lhm
|
|
24
21
|
Intersection.new(@origin, @destination)
|
25
22
|
end
|
26
23
|
|
27
|
-
private
|
28
|
-
|
29
24
|
def startstamp
|
30
25
|
@start.strftime "%Y_%m_%d_%H_%M_%S_#{ "%03d" % (@start.usec / 1000) }"
|
31
26
|
end
|
32
27
|
end
|
33
28
|
end
|
34
|
-
|
data/lib/lhm/migrator.rb
CHANGED
@@ -1,19 +1,19 @@
|
|
1
|
-
#
|
2
|
-
#
|
3
|
-
# Schmidt
|
4
|
-
#
|
5
|
-
# Copies existing schema and applies changes using alter on the empty table.
|
6
|
-
# `run` returns a Migration which can be used for the remaining process.
|
7
|
-
#
|
1
|
+
# Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
2
|
+
# Schmidt
|
8
3
|
|
9
4
|
require 'lhm/command'
|
10
5
|
require 'lhm/migration'
|
6
|
+
require 'lhm/sql_helper'
|
7
|
+
require 'lhm/table'
|
11
8
|
|
12
9
|
module Lhm
|
10
|
+
# Copies existing schema and applies changes using alter on the empty table.
|
11
|
+
# `run` returns a Migration which can be used for the remaining process.
|
13
12
|
class Migrator
|
14
13
|
include Command
|
14
|
+
include SqlHelper
|
15
15
|
|
16
|
-
attr_reader :name, :statements
|
16
|
+
attr_reader :name, :statements, :connection
|
17
17
|
|
18
18
|
def initialize(table, connection = nil)
|
19
19
|
@connection = connection
|
@@ -22,78 +22,103 @@ module Lhm
|
|
22
22
|
@statements = []
|
23
23
|
end
|
24
24
|
|
25
|
+
# Alter a table with a custom statement
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
#
|
29
|
+
# Lhm.change_table(:users) do |m|
|
30
|
+
# m.ddl("ALTER TABLE #{m.name} ADD COLUMN age INT(11)")
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# @param [String] statement SQL alter statement
|
34
|
+
# @note
|
35
|
+
#
|
36
|
+
# Don't write the table name directly into the statement. Use the #name
|
37
|
+
# getter instead, because the alter statement will be executed against a
|
38
|
+
# temporary table.
|
39
|
+
#
|
25
40
|
def ddl(statement)
|
26
41
|
statements << statement
|
27
42
|
end
|
28
43
|
|
44
|
+
# Add a column to a table
|
29
45
|
#
|
30
|
-
#
|
46
|
+
# @example
|
31
47
|
#
|
32
|
-
#
|
33
|
-
#
|
48
|
+
# Lhm.change_table(:users) do |m|
|
49
|
+
# m.add_column(:comment, "VARCHAR(12) DEFAULT '0'")
|
34
50
|
# end
|
35
51
|
#
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
52
|
+
# @param [String] name Name of the column to add
|
53
|
+
# @param [String] definition Valid SQL column definition
|
54
|
+
def add_column(name, definition)
|
55
|
+
ddl("alter table `%s` add column `%s` %s" % [@name, name, definition])
|
40
56
|
end
|
41
57
|
|
58
|
+
# Remove a column from a table
|
42
59
|
#
|
43
|
-
#
|
60
|
+
# @example
|
44
61
|
#
|
45
|
-
#
|
46
|
-
#
|
62
|
+
# Lhm.change_table(:users) do |m|
|
63
|
+
# m.remove_column(:comment)
|
47
64
|
# end
|
48
65
|
#
|
49
|
-
|
66
|
+
# @param [String] name Name of the column to delete
|
50
67
|
def remove_column(name)
|
51
|
-
ddl
|
52
|
-
statements << ddl.strip
|
68
|
+
ddl("alter table `%s` drop `%s`" % [@name, name])
|
53
69
|
end
|
54
70
|
|
71
|
+
# Add an index to a table
|
55
72
|
#
|
56
|
-
#
|
73
|
+
# @example
|
57
74
|
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
#
|
75
|
+
# Lhm.change_table(:users) do |m|
|
76
|
+
# m.add_index(:comment)
|
77
|
+
# m.add_index([:username, :created_at])
|
78
|
+
# m.add_index("comment(10)")
|
79
|
+
# end
|
61
80
|
#
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
81
|
+
# @param [String, Symbol, Array<String, Symbol>] columns
|
82
|
+
# A column name given as String or Symbol. An Array of Strings or Symbols
|
83
|
+
# for compound indexes. It's possible to pass a length limit.
|
84
|
+
def add_index(columns)
|
85
|
+
ddl(index_ddl(columns))
|
66
86
|
end
|
67
87
|
|
88
|
+
# Add a unique index to a table
|
68
89
|
#
|
69
|
-
#
|
90
|
+
# @example
|
70
91
|
#
|
71
|
-
#
|
72
|
-
#
|
73
|
-
#
|
92
|
+
# Lhm.change_table(:users) do |m|
|
93
|
+
# m.add_unique_index(:comment)
|
94
|
+
# m.add_unique_index([:username, :created_at])
|
95
|
+
# m.add_unique_index("comment(10)")
|
96
|
+
# end
|
74
97
|
#
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
98
|
+
# @param [String, Symbol, Array<String, Symbol>] columns
|
99
|
+
# A column name given as String or Symbol. An Array of Strings or Symbols
|
100
|
+
# for compound indexes. It's possible to pass a length limit.
|
101
|
+
def add_unique_index(columns)
|
102
|
+
ddl(index_ddl(columns, :unique))
|
79
103
|
end
|
80
104
|
|
81
|
-
#
|
82
105
|
# Remove an index from a table
|
83
106
|
#
|
84
|
-
#
|
85
|
-
#
|
107
|
+
# @example
|
108
|
+
#
|
109
|
+
# Lhm.change_table(:users) do |m|
|
110
|
+
# m.remove_index(:comment)
|
111
|
+
# m.remove_index([:username, :created_at])
|
86
112
|
# end
|
87
113
|
#
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
114
|
+
# @param [String, Symbol, Array<String, Symbol>] columns
|
115
|
+
# A column name given as String or Symbol. An Array of Strings or Symbols
|
116
|
+
# for compound indexes.
|
117
|
+
def remove_index(columns)
|
118
|
+
ddl("drop index `%s` on `%s`" % [idx_name(@origin.name, columns), @name])
|
92
119
|
end
|
93
120
|
|
94
|
-
|
95
|
-
# Command implementation
|
96
|
-
#
|
121
|
+
private
|
97
122
|
|
98
123
|
def validate
|
99
124
|
unless table?(@origin.name)
|
@@ -111,8 +136,6 @@ module Lhm
|
|
111
136
|
end
|
112
137
|
end
|
113
138
|
|
114
|
-
private
|
115
|
-
|
116
139
|
def execute
|
117
140
|
destination_create
|
118
141
|
sql(@statements)
|
@@ -130,13 +153,10 @@ module Lhm
|
|
130
153
|
Table.parse(@origin.destination_name, connection)
|
131
154
|
end
|
132
155
|
|
133
|
-
def
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
def idx_parts(cols)
|
138
|
-
[@origin.idx_name(cols), idx_spec(cols)]
|
156
|
+
def index_ddl(cols, unique = nil)
|
157
|
+
type = unique ? "unique index" : "index"
|
158
|
+
parts = [type, idx_name(@origin.name, cols), @name, idx_spec(cols)]
|
159
|
+
"create %s `%s` on `%s` (%s)" % parts
|
139
160
|
end
|
140
161
|
end
|
141
162
|
end
|
142
|
-
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
2
|
+
# Schmidt
|
3
|
+
|
4
|
+
module Lhm
|
5
|
+
module SqlHelper
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def idx_name(table_name, cols)
|
9
|
+
column_names = column_definition(cols).map(&:first)
|
10
|
+
"index_#{ table_name }_on_#{ column_names.join("_and_") }"
|
11
|
+
end
|
12
|
+
|
13
|
+
def idx_spec(cols)
|
14
|
+
column_definition(cols).map do |name, length|
|
15
|
+
"`#{name}`#{length}"
|
16
|
+
end.join(', ')
|
17
|
+
end
|
18
|
+
|
19
|
+
def table?(table_name)
|
20
|
+
connection.table_exists?(table_name)
|
21
|
+
end
|
22
|
+
|
23
|
+
def sql(statements)
|
24
|
+
[statements].flatten.each { |statement| connection.execute(statement) }
|
25
|
+
rescue ActiveRecord::StatementInvalid, Mysql::Error => e
|
26
|
+
error e.message
|
27
|
+
end
|
28
|
+
|
29
|
+
def update(statements)
|
30
|
+
[statements].flatten.inject(0) do |memo, statement|
|
31
|
+
memo += connection.update(statement)
|
32
|
+
end
|
33
|
+
rescue ActiveRecord::StatementInvalid, Mysql::Error => e
|
34
|
+
error e.message
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def column_definition(cols)
|
40
|
+
Array(cols).map do |column|
|
41
|
+
column.to_s.match(/`?([^\(]+)`?(\([^\)]+\))?/).captures
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/lhm/table.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
|
-
#
|
2
|
-
#
|
3
|
-
# Schmidt
|
4
|
-
#
|
1
|
+
# Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
|
2
|
+
# Schmidt
|
5
3
|
|
6
4
|
module Lhm
|
7
5
|
class Table
|
@@ -23,11 +21,6 @@ module Lhm
|
|
23
21
|
"lhmn_#{ @name }"
|
24
22
|
end
|
25
23
|
|
26
|
-
def idx_name(cols)
|
27
|
-
column_part = Array(cols).map { |c| c.to_s.sub(/\(.*/, "") }.join("_and_")
|
28
|
-
"index_#{ @name }_on_#{ column_part }"
|
29
|
-
end
|
30
|
-
|
31
24
|
def self.parse(table_name, connection)
|
32
25
|
sql = "show create table `#{ table_name }`"
|
33
26
|
ddl = connection.execute(sql).fetch_row.last
|
@@ -85,4 +78,3 @@ module Lhm
|
|
85
78
|
end
|
86
79
|
end
|
87
80
|
end
|
88
|
-
|
data/lib/lhm/version.rb
ADDED
data/spec/README.md
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
Preparing for master slave integration tests
|
2
|
+
--------------------------------------------
|
3
|
+
|
4
|
+
You can set the integration specs up to run against a master slave setup by
|
5
|
+
running the included `setup-cluster` script.
|
6
|
+
|
7
|
+
# set up instances
|
8
|
+
|
9
|
+
spec/config/setup-cluster
|
10
|
+
|
11
|
+
# start instances
|
12
|
+
|
13
|
+
basedir=/opt/lhm-cluster
|
14
|
+
mysqld --defaults-file="$basedir/master/my.cnf"
|
15
|
+
mysqld --defaults-file="$basedir/slave/my.cnf"
|
16
|
+
|
17
|
+
# run the grants
|
18
|
+
|
19
|
+
spec/config/grants
|
20
|
+
|
21
|
+
# run specs
|
22
|
+
|
23
|
+
To run specs in slave mode, set the SLAVE=1 when running tests:
|
24
|
+
|
25
|
+
MASTER_SLAVE=1 rake specs
|
26
|
+
|