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,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
  require 'minitest/spec'
7
5
  require 'minitest/autorun'
@@ -13,4 +11,3 @@ $spec = $project.join("spec")
13
11
  $fixtures = $spec.join("fixtures")
14
12
 
15
13
  $: << $project.join("lib").to_s
16
-
@@ -0,0 +1,3 @@
1
+ basedir=/opt/lhm-cluster
2
+ master_port=3306
3
+ slave_port=3307
@@ -0,0 +1,36 @@
1
+ #!/bin/sh
2
+
3
+ set -e
4
+ set -u
5
+
6
+ source .config
7
+
8
+ lhmkill() {
9
+ ps -ef | gsed -n "/[m]ysqld.*lhm-cluster/p" | awk '{ print $2 }' | xargs kill
10
+ sleep 5
11
+ }
12
+
13
+ echo stopping other running mysql instance
14
+ launchctl remove com.mysql.mysqld || { echo launchctl did not remove mysqld; }
15
+ mysqladmin shutdown || { echo mysqladmin did not shut down anything; }
16
+
17
+ echo killing lhm-cluster
18
+ lhmkill
19
+
20
+ echo removing $basedir
21
+ rm -rf "$basedir"
22
+
23
+ echo setting up cluster
24
+ spec/config/setup-cluster
25
+
26
+ echo staring instances
27
+ mysqld --defaults-file="$basedir/master/my.cnf" 2>&1 >$basedir/master/lhm.log &
28
+ mysqld --defaults-file="$basedir/slave/my.cnf" 2>&1 >$basedir/slave/lhm.log &
29
+ sleep 5
30
+
31
+ echo running grants
32
+ spec/config/grants
33
+
34
+ trap lhmkill SIGTERM SIGINT
35
+
36
+ wait
@@ -0,0 +1,25 @@
1
+ #!/bin/sh
2
+
3
+ source .config
4
+
5
+ master() { mysql --protocol=TCP -P $master_port -uroot; }
6
+ slave() { mysql --protocol=TCP -P $slave_port -uroot; }
7
+
8
+ # set up master
9
+
10
+ echo "create user 'slave'@'localhost' identified by 'slave'" | master
11
+ echo "grant replication slave on *.* to 'slave'@'localhost'" | master
12
+
13
+ # set up slave
14
+
15
+ echo "change master to master_user = 'slave', master_password = 'slave', master_port = 3306, master_host = 'localhost'" | slave
16
+ echo "start slave" | slave
17
+ echo "show slave status \G" | slave
18
+
19
+ # setup for test
20
+
21
+ echo "grant all privileges on *.* to ''@'localhost'" | master
22
+ echo "grant all privileges on *.* to ''@'localhost'" | slave
23
+
24
+ echo "create database lhm" | master
25
+ echo "create database lhm" | slave
@@ -0,0 +1,61 @@
1
+ #!/bin/sh
2
+
3
+ #
4
+ # Set up master slave cluster for lhm specs
5
+ #
6
+
7
+ set -e
8
+ set -u
9
+
10
+ source .config
11
+
12
+ #
13
+ # Main
14
+ #
15
+
16
+ mkdir -p "$basedir/master/data" "$basedir/slave/data"
17
+
18
+ cat <<-CNF > $basedir/master/my.cnf
19
+ [mysqld]
20
+ pid-file = $basedir/master/mysqld.pid
21
+ socket = $basedir/master/mysqld.sock
22
+ port = $master_port
23
+ log_output = FILE
24
+ log-error = $basedir/master/error.log
25
+ datadir = $basedir/master/data
26
+ log-bin = master-bin
27
+ log-bin-index = master-bin.index
28
+ server-id = 1
29
+ CNF
30
+
31
+ cat <<-CNF > $basedir/slave/my.cnf
32
+ [mysqld]
33
+ pid-file = $basedir/slave/mysqld.pid
34
+ socket = $basedir/slave/mysqld.sock
35
+ port = $slave_port
36
+ log_output = FILE
37
+ log-error = $basedir/slave/error.log
38
+ datadir = $basedir/slave/data
39
+ relay-log = slave-relay-bin
40
+ relay-log-index = slave-relay-bin.index
41
+ server-id = 2
42
+
43
+ # replication (optional filters)
44
+
45
+ replicate-do-table = lhm.users
46
+ replicate-do-table = lhm.lhmn_users
47
+ replicate-wild-do = lhm.lhma_%_users
48
+
49
+ replicate-do-table = lhm.origin
50
+ replicate-do-table = lhm.lhmn_origin
51
+ replicate-wild-do = lhm.lhma_%_origin
52
+
53
+ replicate-do-table = lhm.destination
54
+ replicate-do-table = lhm.lhmn_destination
55
+ replicate-wild-do = lhm.lhma_%_destination
56
+ CNF
57
+
58
+ # build system tables
59
+
60
+ mysql_install_db --datadir="$basedir/master/data"
61
+ mysql_install_db --datadir="$basedir/slave/data"
@@ -4,4 +4,3 @@ CREATE TABLE `destination` (
4
4
  `common` varchar(255) DEFAULT NULL,
5
5
  PRIMARY KEY (`id`)
6
6
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8
7
-
@@ -4,4 +4,3 @@ CREATE TABLE `origin` (
4
4
  `common` varchar(255) DEFAULT NULL,
5
5
  PRIMARY KEY (`id`)
6
6
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8
7
-
@@ -2,10 +2,10 @@ CREATE TABLE `users` (
2
2
  `id` int(11) NOT NULL AUTO_INCREMENT,
3
3
  `reference` int(11) DEFAULT NULL,
4
4
  `username` varchar(255) DEFAULT NULL,
5
+ `group` varchar(255) DEFAULT NULL,
5
6
  `created_at` datetime DEFAULT NULL,
6
7
  `comment` varchar(20) DEFAULT NULL,
7
8
  PRIMARY KEY (`id`),
8
9
  UNIQUE KEY `index_users_on_reference` (`reference`),
9
10
  KEY `index_users_on_username_and_created_at` (`username`,`created_at`)
10
11
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8
11
-
@@ -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
  require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
7
5
 
@@ -12,7 +10,7 @@ require 'lhm/migration'
12
10
  describe Lhm::Chunker do
13
11
  include IntegrationHelper
14
12
 
15
- before(:each) { connect! }
13
+ before(:each) { connect_master! }
16
14
 
17
15
  describe "copying" do
18
16
  before(:each) do
@@ -22,10 +20,13 @@ describe Lhm::Chunker do
22
20
  end
23
21
 
24
22
  it "should copy 23 rows from origin to destination" do
25
- 23.times { |n| execute("insert into origin set common = '#{ n }'") }
26
- Lhm::Chunker.new(@migration, limit = 23, connection).run
27
- count_all(@destination.name).must_equal(23)
23
+ 23.times { |n| execute("insert into origin set id = '#{ n * n + 23 }'") }
24
+
25
+ Lhm::Chunker.new(@migration, connection, { :stride => 100 }).run
26
+
27
+ slave do
28
+ count_all(@destination.name).must_equal(23)
29
+ end
28
30
  end
29
31
  end
30
32
  end
31
-
@@ -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
  require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
7
5
 
@@ -12,7 +10,7 @@ require 'lhm/entangler'
12
10
  describe Lhm::Entangler do
13
11
  include IntegrationHelper
14
12
 
15
- before(:each) { connect! }
13
+ before(:each) { connect_master! }
16
14
 
17
15
  describe "entanglement" do
18
16
  before(:each) do
@@ -27,7 +25,9 @@ describe Lhm::Entangler do
27
25
  execute("insert into origin (common) values ('inserted')")
28
26
  end
29
27
 
30
- count(:destination, "common", "inserted").must_equal(1)
28
+ slave do
29
+ count(:destination, "common", "inserted").must_equal(1)
30
+ end
31
31
  end
32
32
 
33
33
  it "should replay deletes from origin into destination" do
@@ -37,7 +37,9 @@ describe Lhm::Entangler do
37
37
  execute("delete from origin where common = 'inserted'")
38
38
  end
39
39
 
40
- count(:destination, "common", "inserted").must_equal(0)
40
+ slave do
41
+ count(:destination, "common", "inserted").must_equal(0)
42
+ end
41
43
  end
42
44
 
43
45
  it "should replay updates from origin into destination" do
@@ -46,15 +48,19 @@ describe Lhm::Entangler do
46
48
  execute("update origin set common = 'updated'")
47
49
  end
48
50
 
49
- count(:destination, "common", "updated").must_equal(1)
51
+ slave do
52
+ count(:destination, "common", "updated").must_equal(1)
53
+ end
50
54
  end
51
55
 
52
56
  it "should remove entanglement" do
53
57
  @entangler.run {}
54
58
 
55
59
  execute("insert into origin (common) values ('inserted')")
56
- count(:destination, "common", "inserted").must_equal(0)
60
+
61
+ slave do
62
+ count(:destination, "common", "inserted").must_equal(0)
63
+ end
57
64
  end
58
65
  end
59
66
  end
60
-
@@ -1,31 +1,36 @@
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
  require File.expand_path(File.dirname(__FILE__)) + "/../bootstrap"
7
5
 
8
6
  require 'active_record'
9
7
  require 'lhm/table'
8
+ require 'lhm/sql_helper'
10
9
 
11
10
  module IntegrationHelper
11
+ attr_accessor :connection
12
12
 
13
13
  #
14
14
  # Connectivity
15
15
  #
16
16
 
17
- def connect!
17
+ def connect_master!
18
+ @connection = connect!(3306)
19
+ end
20
+
21
+ def connect_slave!
22
+ @connection = connect!(3307)
23
+ end
24
+
25
+ def connect!(port)
18
26
  ActiveRecord::Base.establish_connection(
19
27
  :adapter => 'mysql',
28
+ :host => '127.0.0.1',
20
29
  :database => 'lhm',
21
30
  :username => '',
22
- :host => 'localhost'
31
+ :port => port
23
32
  )
24
33
 
25
- ActiveRecord::Migration.verbose = !!ENV["VERBOSE"]
26
- end
27
-
28
- def connection
29
34
  ActiveRecord::Base.connection
30
35
  end
31
36
 
@@ -41,6 +46,23 @@ module IntegrationHelper
41
46
  connection.execute(*args)
42
47
  end
43
48
 
49
+ def slave(&block)
50
+ if master_slave_mode?
51
+ connect_slave!
52
+
53
+ # need to wait for the slave to catch up. a better method would be to
54
+ # check the master binlog position and wait for the slave to catch up
55
+ # to that position.
56
+ sleep 1
57
+ end
58
+
59
+ yield block
60
+
61
+ if master_slave_mode?
62
+ connect_master!
63
+ end
64
+ end
65
+
44
66
  #
45
67
  # Test Data
46
68
  #
@@ -77,9 +99,18 @@ module IntegrationHelper
77
99
  select_value(query).to_i
78
100
  end
79
101
 
80
- def key?(table, cols, type = :non_unique)
102
+ def key?(table_name, cols, type = :non_unique)
81
103
  non_unique = type == :non_unique ? 1 : 0
82
- query = "show indexes in #{ table.name } where key_name = '#{ table.idx_name(cols) }' and non_unique = #{ non_unique }"
104
+ key_name = Lhm::SqlHelper.idx_name(table_name, cols)
105
+ query = "show indexes in #{ table_name } where key_name = '#{ key_name }' and non_unique = #{ non_unique }"
83
106
  !!select_value(query)
84
107
  end
108
+
109
+ #
110
+ # Environment
111
+ #
112
+
113
+ def master_slave_mode?
114
+ !!ENV["MASTER_SLAVE"]
115
+ end
85
116
  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
  require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
7
5
 
@@ -9,9 +7,8 @@ require 'lhm'
9
7
 
10
8
  describe Lhm do
11
9
  include IntegrationHelper
12
- include Lhm
13
10
 
14
- before(:each) { connect! }
11
+ before(:each) { connect_master! }
15
12
 
16
13
  describe "changes" do
17
14
  before(:each) do
@@ -19,67 +16,91 @@ describe Lhm do
19
16
  end
20
17
 
21
18
  it "should add a column" do
22
- hadron_change_table("users") do |t|
19
+ Lhm.change_table(:users) do |t|
23
20
  t.add_column(:logins, "INT(12) DEFAULT '0'")
24
21
  end
25
22
 
26
- table_read("users").columns["logins"].must_equal({
27
- :type => "int(12)",
28
- :metadata => "DEFAULT '0'"
29
- })
23
+ slave do
24
+ table_read(:users).columns["logins"].must_equal({
25
+ :type => "int(12)",
26
+ :metadata => "DEFAULT '0'"
27
+ })
28
+ end
30
29
  end
31
30
 
32
31
  it "should copy all rows" do
33
32
  23.times { |n| execute("insert into users set reference = '#{ n }'") }
34
33
 
35
- hadron_change_table("users") do |t|
34
+ Lhm.change_table(:users) do |t|
36
35
  t.add_column(:logins, "INT(12) DEFAULT '0'")
37
36
  end
38
37
 
39
- count_all("users").must_equal(23)
38
+ slave do
39
+ count_all(:users).must_equal(23)
40
+ end
40
41
  end
41
42
 
42
43
  it "should remove a column" do
43
- hadron_change_table("users") do |t|
44
+ Lhm.change_table(:users) do |t|
44
45
  t.remove_column(:comment)
45
46
  end
46
47
 
47
- table_read("users").columns["comment"].must_equal nil
48
+ slave do
49
+ table_read(:users).columns["comment"].must_equal nil
50
+ end
48
51
  end
49
52
 
50
53
  it "should add an index" do
51
- hadron_change_table("users") do |t|
54
+ Lhm.change_table(:users) do |t|
52
55
  t.add_index([:comment, :created_at])
53
56
  end
54
57
 
55
- key?(table_read("users"), ["comment", "created_at"]).must_equal(true)
58
+ slave do
59
+ key?(:users, [:comment, :created_at]).must_equal(true)
60
+ end
61
+ end
62
+
63
+ it "should add an index on a column with a reserved name" do
64
+ Lhm.change_table(:users) do |t|
65
+ t.add_index(:group)
66
+ end
67
+
68
+ slave do
69
+ key?(:users, :group).must_equal(true)
70
+ end
56
71
  end
57
72
 
58
73
  it "should add a unqiue index" do
59
- hadron_change_table("users") do |t|
74
+ Lhm.change_table(:users) do |t|
60
75
  t.add_unique_index(:comment)
61
76
  end
62
77
 
63
- key?(table_read(:users), :comment, :unique).must_equal(true)
78
+ slave do
79
+ key?(:users, :comment, :unique).must_equal(true)
80
+ end
64
81
  end
65
82
 
66
83
  it "should remove an index" do
67
- hadron_change_table("users") do |t|
84
+ Lhm.change_table(:users) do |t|
68
85
  t.remove_index([:username, :created_at])
69
86
  end
70
87
 
71
- key?(table_read("users"), ["username", "created_at"]).must_equal(false)
88
+ slave do
89
+ key?(:users, [:username, :created_at]).must_equal(false)
90
+ end
72
91
  end
73
92
 
74
93
  it "should apply a ddl statement" do
75
- hadron_change_table("users") do |t|
94
+ Lhm.change_table(:users) do |t|
76
95
  t.ddl("alter table %s add column flag tinyint(1)" % t.name)
77
96
  end
78
97
 
79
- table_read("users").columns["flag"].must_equal({
80
- :type => "tinyint(1)",
81
- :metadata => "DEFAULT NULL"
82
- })
98
+ slave do
99
+ table_read(:users).columns["flag"].must_equal({
100
+ :type => "tinyint(1)",
101
+ :metadata => "DEFAULT NULL"
102
+ })
103
+ end
83
104
  end
84
105
 
85
106
  describe "parallel" do
@@ -93,34 +114,37 @@ describe Lhm do
93
114
  end
94
115
  end
95
116
 
96
- hadron_change_table("users", :stride => 10, :throttle => 97) do |t|
117
+ Lhm.change_table(:users, :stride => 10, :throttle => 97) do |t|
97
118
  t.add_column(:parallel, "INT(10) DEFAULT '0'")
98
119
  end
99
120
 
100
121
  insert.join
101
122
 
102
- count_all("users").must_equal(60)
123
+ slave do
124
+ count_all(:users).must_equal(60)
125
+ end
103
126
  end
104
- end
105
127
 
106
- it "should perserve deletes during migration" do
107
- 50.times { |n| execute("insert into users set reference = '#{ n }'") }
128
+ it "should perserve deletes during migration" do
129
+ 50.times { |n| execute("insert into users set reference = '#{ n }'") }
108
130
 
109
- insert = Thread.new do
110
- 10.times do |n|
111
- execute("delete from users where id = '#{ n + 1 }'")
112
- sleep(0.17)
131
+ delete = Thread.new do
132
+ 10.times do |n|
133
+ execute("delete from users where id = '#{ n + 1 }'")
134
+ sleep(0.17)
135
+ end
113
136
  end
114
- end
115
137
 
116
- hadron_change_table("users", :stride => 10, :throttle => 97) do |t|
117
- t.add_column(:parallel, "INT(10) DEFAULT '0'")
118
- end
138
+ Lhm.change_table(:users, :stride => 10, :throttle => 97) do |t|
139
+ t.add_column(:parallel, "INT(10) DEFAULT '0'")
140
+ end
119
141
 
120
- insert.join
142
+ delete.join
121
143
 
122
- count_all("users").must_equal(40)
144
+ slave do
145
+ count_all(:users).must_equal(40)
146
+ end
147
+ end
123
148
  end
124
149
  end
125
150
  end
126
-