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.
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
+