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
data/.config ADDED
@@ -0,0 +1,3 @@
1
+ basedir=/opt/lhm-cluster
2
+ master_port=3306
3
+ slave_port=3307
data/.gitignore CHANGED
@@ -1,5 +1,6 @@
1
1
  *.gem
2
2
  .bundle
3
3
  Gemfile.lock
4
+ gemfiles/*.lock
4
5
  pkg/*
5
6
  .rvmrc
@@ -1,9 +1,15 @@
1
+ before_script:
2
+ - "mysql -e 'create database lhm;'"
1
3
  rvm:
2
4
  - 1.8.7
3
- - 1.9.2
4
5
  - 1.9.3
5
- before_script:
6
- - "mysql -e 'create database lhm;'"
6
+ gemfile:
7
+ - gemfiles/ar-2.3.gemfile
8
+ - gemfiles/ar-3.1.gemfile
9
+ matrix:
10
+ exclude:
11
+ - rvm: 1.8.7
12
+ gemfile: gemfiles/ar-2.3.gemfile
7
13
  branches:
8
14
  only:
9
15
  - master
@@ -1,3 +1,13 @@
1
+ # 1.0.0.rc3 (January 19, 2012)
2
+
3
+ * Speedup migrations for tables with large minimum id
4
+ * Add a bit yard documentation
5
+ * Fix issues with index creation on reserved column names
6
+ * Improve error handling
7
+ * Add tests for replication
8
+ * Rename public API method from `hadron_change_table` to `change_table`
9
+ * Add tests for ActiveRecord 2.3 and 3.1 compatibility
10
+
1
11
  # 1.0.0.rc2 (January 18, 2012)
2
12
 
3
13
  * Speedup migrations for tables with large ids
data/README.md CHANGED
@@ -16,7 +16,7 @@ access becomes just as difficult.
16
16
 
17
17
  There are few things that can be done at the server or engine level. It is
18
18
  possible to change default values in an `ALTER TABLE` without locking the
19
- table. The InnoDB Plugin provides facilities for online index creation, which
19
+ table. The InnoDB Plugin provides facilities for online index creation, which
20
20
  is great if you are using this engine, but only solves half the problem.
21
21
 
22
22
  At SoundCloud we started having migration pains quite a while ago, and after
@@ -42,40 +42,44 @@ twitter solution [1], it does not require the presence of an indexed
42
42
 
43
43
  ## Usage
44
44
 
45
- After extending Lhm, `hadron_change_table` becomes available
46
- with the following methods:
45
+ You can invoke Lhm directly from a plain ruby file after connecting active
46
+ record to your mysql instance:
47
47
 
48
- class MigrateArbitrary < ActiveRecord::Migration
49
- extend Lhm
48
+ require 'lhm'
49
+
50
+ ActiveRecord::Base.establish_connection(
51
+ :adapter => 'mysql',
52
+ :host => '127.0.0.1',
53
+ :database => 'lhm'
54
+ )
55
+
56
+ Lhm.change_table(:users) do |m|
57
+ m.add_column(:arbitrary, "INT(12)")
58
+ m.add_index([:arbitrary, :created_at])
59
+ m.ddl("alter table %s add column flag tinyint(1)" % m.name)
60
+ end
61
+
62
+ To use Lhm from an ActiveRecord::Migration in a Rails project, add it to your
63
+ Gemfile, then invoke as follows:
64
+
65
+ class MigrateUsers < ActiveRecord::Migration
50
66
 
51
67
  def self.up
52
- hadron_change_table(:users) do |t|
53
- t.add_column(:arbitrary, "INT(12)")
54
- t.add_index([:arbitrary, :created_at])
55
- t.ddl("alter table %s add column flag tinyint(1)" % t.name)
68
+ Lhm.change_table(:users) do |m|
69
+ m.add_column(:arbitrary, "INT(12)")
70
+ m.add_index([:arbitrary, :created_at])
71
+ m.ddl("alter table %s add column flag tinyint(1)" % m.name)
56
72
  end
57
73
  end
58
74
 
59
75
  def self.down
60
- hadron_change_table(:users) do |t|
61
- t.remove_index([:arbitrary, :created_at])
62
- t.remove_column(:arbitrary)
76
+ Lhm.change_table(:users) do |m|
77
+ m.remove_index([:arbitrary, :created_at])
78
+ m.remove_column(:arbitrary)
63
79
  end
64
80
  end
65
81
  end
66
82
 
67
- ## Migration phases
68
-
69
- _TODO_
70
-
71
- ### When adding a column
72
-
73
- _TODO_
74
-
75
- ### When removing a column
76
-
77
- _TODO_
78
-
79
83
  ## Contributing
80
84
 
81
85
  We'll check out your contribution if you:
data/Rakefile CHANGED
@@ -15,5 +15,6 @@ Rake::TestTask.new("integration") do |t|
15
15
  t.verbose = true
16
16
  end
17
17
 
18
- task :default => [:unit, :integration]
18
+ task :specs => [:unit, :integration]
19
+ task :default => :specs
19
20
 
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "activerecord", "~> 2.3.14"
4
+ gemspec :path=>"../"
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "activerecord", "~> 3.1.3"
4
+ gemspec :path=>"../"
@@ -3,7 +3,7 @@
3
3
  lib = File.expand_path('../lib', __FILE__)
4
4
  $:.unshift(lib) unless $:.include?(lib)
5
5
 
6
- require 'lhm'
6
+ require 'lhm/version'
7
7
 
8
8
  Gem::Specification.new do |s|
9
9
  s.name = "lhm"
data/lib/lhm.rb CHANGED
@@ -1,20 +1,42 @@
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
 
4
+ require 'active_record'
6
5
  require 'lhm/table'
7
6
  require 'lhm/invoker'
8
- require 'lhm/migration'
7
+ require 'lhm/version'
9
8
 
9
+ # Large hadron migrator - online schema change tool
10
+ #
11
+ # @example
12
+ #
13
+ # Lhm.change_table(:users) do |m|
14
+ # m.add_column(:arbitrary, "INT(12)")
15
+ # m.add_index([:arbitrary, :created_at])
16
+ # m.ddl("alter table %s add column flag tinyint(1)" % m.name)
17
+ # end
18
+ #
10
19
  module Lhm
11
- VERSION = "1.0.0.rc2"
20
+ extend self
12
21
 
13
- def hadron_change_table(table_name, chunk_options = {}, &block)
22
+ # Alters a table with the changes described in the block
23
+ #
24
+ # @param [String, Symbol] table_name Name of the table
25
+ # @param [Hash] chunk_options Optional options to alter the chunk behavior
26
+ # @option chunk_options [Fixnum] :stride
27
+ # Size of a chunk (defaults to: 40,000)
28
+ # @option chunk_options [Fixnum] :throttle
29
+ # Time to wait between chunks in milliseconds (defaults to: 100)
30
+ # @yield [Migrator] Yielded Migrator object records the changes
31
+ # @return [Boolean] Returns true if the migration finishs
32
+ # @raise [Error] Raises Lhm::Error in case of a error and aborts the migration
33
+ def change_table(table_name, chunk_options = {}, &block)
34
+ connection = ActiveRecord::Base.connection
14
35
  origin = Table.parse(table_name, connection)
15
36
  invoker = Invoker.new(origin, connection)
16
37
  block.call(invoker.migrator)
17
38
  invoker.run(chunk_options)
39
+
40
+ true
18
41
  end
19
42
  end
20
-
@@ -1,64 +1,88 @@
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/migration'
7
4
  require 'lhm/command'
5
+ require 'lhm/sql_helper'
8
6
 
9
7
  module Lhm
10
8
  class Chunker
11
9
  include Command
10
+ include SqlHelper
11
+
12
+ attr_reader :connection
12
13
 
13
- #
14
14
  # Copy from origin to destination in chunks of size `stride`. Sleeps for
15
15
  # `throttle` milliseconds between each stride.
16
- #
17
-
18
- def initialize(migration, limit = 1, connection = nil, options = {})
16
+ def initialize(migration, connection = nil, options = {})
17
+ @migration = migration
18
+ @connection = connection
19
19
  @stride = options[:stride] || 40_000
20
20
  @throttle = options[:throttle] || 100
21
- @limit = limit
22
- @connection = connection
23
- @migration = migration
21
+ @start = options[:start] || select_start
22
+ @limit = options[:limit] || select_limit
24
23
  end
25
24
 
26
- #
27
- # Copies chunks of size `stride`, starting from id 1 up to id `limit`.
28
- #
29
-
30
- def up_to(limit)
31
- traversable_chunks_up_to(limit).times do |n|
32
- yield(bottom(n + 1), top(n + 1, limit))
25
+ # Copies chunks of size `stride`, starting from `start` up to id `limit`.
26
+ def up_to(&block)
27
+ 1.upto(traversable_chunks_size) do |n|
28
+ yield(bottom(n), top(n))
33
29
  end
34
30
  end
35
31
 
36
- def traversable_chunks_up_to(limit)
37
- (limit / @stride.to_f).ceil
32
+ def traversable_chunks_size
33
+ @limit && @start ? ((@limit - @start + 1) / @stride.to_f).ceil : 0
38
34
  end
39
35
 
40
36
  def bottom(chunk)
41
- (chunk - 1) * @stride + 1
37
+ (chunk - 1) * @stride + @start
42
38
  end
43
39
 
44
- def top(chunk, limit)
45
- [chunk * @stride, limit].min
40
+ def top(chunk)
41
+ [chunk * @stride + @start - 1, @limit].min
46
42
  end
47
43
 
48
44
  def copy(lowest, highest)
49
- "insert ignore into `#{ @migration.destination.name }` (#{ cols.joined }) " +
50
- "select #{ cols.joined } from `#{ @migration.origin.name }` " +
45
+ "insert ignore into `#{ destination_name }` (#{ columns }) " +
46
+ "select #{ columns } from `#{ origin_name }` " +
51
47
  "where `id` between #{ lowest } and #{ highest }"
52
48
  end
53
49
 
50
+ def select_start
51
+ start = connection.select_value("select min(id) from #{ origin_name }")
52
+ start ? start.to_i : nil
53
+ end
54
+
55
+ def select_limit
56
+ limit = connection.select_value("select max(id) from #{ origin_name }")
57
+ limit ? limit.to_i : nil
58
+ end
59
+
60
+ def throttle_seconds
61
+ @throttle / 1000.0
62
+ end
63
+
54
64
  private
55
65
 
56
- def cols
57
- @cols ||= @migration.intersection
66
+ def destination_name
67
+ @migration.destination.name
68
+ end
69
+
70
+ def origin_name
71
+ @migration.origin.name
72
+ end
73
+
74
+ def columns
75
+ @columns ||= @migration.intersection.joined
76
+ end
77
+
78
+ def validate
79
+ if @start && @limit && @start > @limit
80
+ error("impossible chunk options (limit must be greater than start)")
81
+ end
58
82
  end
59
83
 
60
84
  def execute
61
- up_to(@limit) do |lowest, highest|
85
+ up_to do |lowest, highest|
62
86
  affected_rows = update(copy(lowest, highest))
63
87
 
64
88
  if affected_rows > 0
@@ -68,10 +92,5 @@ module Lhm
68
92
  print "."
69
93
  end
70
94
  end
71
-
72
- def throttle_seconds
73
- @throttle / 1000.0
74
- end
75
95
  end
76
96
  end
77
-
@@ -1,24 +1,11 @@
1
- #
2
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
3
- # Schmidt
4
- #
5
- # Apply a change to the database.
6
- #
1
+ # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
+ # Schmidt
7
3
 
8
4
  module Lhm
9
- module Command
10
- def self.included(base)
11
- base.send :attr_reader, :connection
12
- end
13
-
14
- #
15
- # Command Interface
16
- #
17
-
18
- def validate; end
19
-
20
- def revert; end
5
+ class Error < StandardError
6
+ end
21
7
 
8
+ module Command
22
9
  def run(&block)
23
10
  validate
24
11
 
@@ -29,45 +16,31 @@ module Lhm
29
16
  else
30
17
  execute
31
18
  end
19
+ rescue
20
+ revert
21
+ raise
32
22
  end
33
23
 
34
24
  private
35
25
 
36
- def execute
37
- raise NotImplementedError.new(self.class.name)
26
+ def validate
38
27
  end
39
28
 
40
- def before
41
- raise NotImplementedError.new(self.class.name)
29
+ def revert
42
30
  end
43
31
 
44
- def after
32
+ def execute
45
33
  raise NotImplementedError.new(self.class.name)
46
34
  end
47
35
 
48
- def table?(table_name)
49
- @connection.table_exists?(table_name)
50
- end
51
-
52
- def error(msg)
53
- raise Exception.new("#{ self.class }: #{ msg }")
36
+ def before
54
37
  end
55
38
 
56
- def sql(statements)
57
- [statements].flatten.each { |statement| @connection.execute(statement) }
58
- rescue ActiveRecord::StatementInvalid, Mysql::Error => e
59
- revert
60
- error e.message
39
+ def after
61
40
  end
62
41
 
63
- def update(statements)
64
- [statements].flatten.inject(0) do |memo, statement|
65
- memo += @connection.update(statement)
66
- end
67
- rescue ActiveRecord::StatementInvalid, Mysql::Error => e
68
- revert
69
- error e.message
42
+ def error(msg)
43
+ raise Error.new(msg)
70
44
  end
71
45
  end
72
46
  end
73
-
@@ -1,19 +1,18 @@
1
- #
2
- # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
3
- # Schmidt
4
- #
5
- # Creates entanglement between two tables. All creates, updates and deletes
6
- # to origin will be repeated on the the destination table.
7
- #
1
+ # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
+ # Schmidt
8
3
 
9
4
  require 'lhm/command'
5
+ require 'lhm/sql_helper'
10
6
 
11
7
  module Lhm
12
8
  class Entangler
13
9
  include Command
10
+ include SqlHelper
14
11
 
15
- attr_reader :epoch
12
+ attr_reader :connection
16
13
 
14
+ # Creates entanglement between two tables. All creates, updates and deletes
15
+ # to origin will be repeated on the destination table.
17
16
  def initialize(migration, connection = nil)
18
17
  @common = migration.intersection
19
18
  @origin = migration.origin
@@ -23,9 +22,9 @@ module Lhm
23
22
 
24
23
  def entangle
25
24
  [
26
- create_trigger_del,
27
- create_trigger_ins,
28
- create_trigger_upd
25
+ create_delete_trigger,
26
+ create_insert_trigger,
27
+ create_update_trigger
29
28
  ]
30
29
  end
31
30
 
@@ -37,7 +36,7 @@ module Lhm
37
36
  ]
38
37
  end
39
38
 
40
- def create_trigger_ins
39
+ def create_insert_trigger
41
40
  strip %Q{
42
41
  create trigger `#{ trigger(:ins) }`
43
42
  after insert on `#{ @origin.name }` for each row
@@ -46,7 +45,7 @@ module Lhm
46
45
  }
47
46
  end
48
47
 
49
- def create_trigger_upd
48
+ def create_update_trigger
50
49
  strip %Q{
51
50
  create trigger `#{ trigger(:upd) }`
52
51
  after update on `#{ @origin.name }` for each row
@@ -55,7 +54,7 @@ module Lhm
55
54
  }
56
55
  end
57
56
 
58
- def create_trigger_del
57
+ def create_delete_trigger
59
58
  strip %Q{
60
59
  create trigger `#{ trigger(:del) }`
61
60
  after delete on `#{ @origin.name }` for each row
@@ -68,10 +67,6 @@ module Lhm
68
67
  "lhmt_#{ type }_#{ @origin.name }"
69
68
  end
70
69
 
71
- #
72
- # Command implementation
73
- #
74
-
75
70
  def validate
76
71
  unless table?(@origin.name)
77
72
  error("#{ @origin.name } does not exist")
@@ -84,7 +79,6 @@ module Lhm
84
79
 
85
80
  def before
86
81
  sql(entangle)
87
- @epoch = connection.select_value("select max(id) from #{ @origin.name }").to_i
88
82
  end
89
83
 
90
84
  def after
@@ -102,4 +96,3 @@ module Lhm
102
96
  end
103
97
  end
104
98
  end
105
-