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,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
-