lhm 1.0.0.rc2 → 1.0.0.rc3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/.config +3 -0
  2. data/.gitignore +1 -0
  3. data/.travis.yml +9 -3
  4. data/CHANGELOG.md +10 -0
  5. data/README.md +28 -24
  6. data/Rakefile +2 -1
  7. data/gemfiles/ar-2.3.gemfile +4 -0
  8. data/gemfiles/ar-3.1.gemfile +4 -0
  9. data/lhm.gemspec +1 -1
  10. data/lib/lhm.rb +30 -8
  11. data/lib/lhm/chunker.rb +53 -34
  12. data/lib/lhm/command.rb +15 -42
  13. data/lib/lhm/entangler.rb +13 -20
  14. data/lib/lhm/intersection.rb +3 -7
  15. data/lib/lhm/invoker.rb +9 -14
  16. data/lib/lhm/locked_switcher.rb +22 -26
  17. data/lib/lhm/migration.rb +2 -8
  18. data/lib/lhm/migrator.rb +76 -56
  19. data/lib/lhm/sql_helper.rb +45 -0
  20. data/lib/lhm/table.rb +2 -10
  21. data/lib/lhm/version.rb +6 -0
  22. data/spec/README.md +26 -0
  23. data/spec/bootstrap.rb +2 -5
  24. data/spec/config/.config +3 -0
  25. data/spec/config/clobber +36 -0
  26. data/spec/config/grants +25 -0
  27. data/spec/config/setup-cluster +61 -0
  28. data/spec/fixtures/destination.ddl +0 -1
  29. data/spec/fixtures/origin.ddl +0 -1
  30. data/spec/fixtures/users.ddl +1 -1
  31. data/spec/integration/chunker_spec.rb +10 -9
  32. data/spec/integration/entangler_spec.rb +16 -10
  33. data/spec/integration/integration_helper.rb +43 -12
  34. data/spec/integration/lhm_spec.rb +66 -42
  35. data/spec/integration/locked_switcher_spec.rb +11 -10
  36. data/spec/unit/chunker_spec.rb +50 -18
  37. data/spec/unit/entangler_spec.rb +2 -5
  38. data/spec/unit/intersection_spec.rb +2 -5
  39. data/spec/unit/locked_switcher_spec.rb +2 -5
  40. data/spec/unit/migration_spec.rb +2 -5
  41. data/spec/unit/migrator_spec.rb +6 -9
  42. data/spec/unit/sql_helper_spec.rb +32 -0
  43. data/spec/unit/table_spec.rb +2 -29
  44. data/spec/unit/unit_helper.rb +2 -5
  45. metadata +52 -7
  46. data/Gemfile +0 -3
@@ -1,11 +1,8 @@
1
- #
2
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
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
-
@@ -1,21 +1,17 @@
1
- #
2
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
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 |tangler|
31
- Chunker.new(migration, tangler.epoch, @connection, chunk_options).run
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
-
@@ -1,28 +1,30 @@
1
- #
2
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
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
-
@@ -1,9 +1,6 @@
1
- #
2
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
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
-
@@ -1,19 +1,19 @@
1
- #
2
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
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
- # Add a column to a table:
46
+ # @example
31
47
  #
32
- # hadron_change_table("users") do |t|
33
- # t.add_column(:logins, "INT(12) DEFAULT '0'")
48
+ # Lhm.change_table(:users) do |m|
49
+ # m.add_column(:comment, "VARCHAR(12) DEFAULT '0'")
34
50
  # end
35
51
  #
36
-
37
- def add_column(name, definition = "")
38
- ddl = "alter table `%s` add column `%s` %s" % [@name, name, definition]
39
- statements << ddl.strip
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
- # Remove a column from a table:
60
+ # @example
44
61
  #
45
- # hadron_change_table("users") do |t|
46
- # t.remove_column(:comment)
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 = "alter table `%s` drop `%s`" % [@name, name]
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
- # Add an index to a table:
73
+ # @example
57
74
  #
58
- # hadron_change_table("users") do |t|
59
- # t.add_index([:comment, :created_at])
60
- # end
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
- def add_index(cols)
64
- ddl = "create index `%s` on %s" % idx_parts(cols)
65
- statements << ddl.strip
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
- # Add a unique index to a table:
90
+ # @example
70
91
  #
71
- # hadron_change_table("users") do |t|
72
- # t.add_unique_index([:comment, :created_at])
73
- # end
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
- def add_unique_index(cols)
77
- ddl = "create unique index `%s` on %s" % idx_parts(cols)
78
- statements << ddl.strip
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
- # hadron_change_table("users") do |t|
85
- # t.remove_index(:username, :created_at)
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
- def remove_index(cols)
90
- ddl = "drop index `%s` on `%s`" % [@origin.idx_name(cols), @name]
91
- statements << ddl.strip
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 idx_spec(cols)
134
- "`#{ @name }` (#{ Array(cols).map(&:to_s).join(', ') })"
135
- end
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
@@ -1,7 +1,5 @@
1
- #
2
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
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
-
@@ -0,0 +1,6 @@
1
+ # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
+ # Schmidt
3
+
4
+ module Lhm
5
+ VERSION = "1.0.0.rc3"
6
+ end
@@ -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
+