ar-octopus 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.mkdn +49 -0
- data/Rakefile +106 -0
- data/VERSION +1 -0
- data/doc/api.textile +98 -0
- data/doc/features.textile +74 -0
- data/doc/libraries.textile +70 -0
- data/doc/shards.yml +76 -0
- data/lib/octopus.rb +30 -0
- data/lib/octopus/controller.rb +2 -0
- data/lib/octopus/migration.rb +41 -0
- data/lib/octopus/model.rb +57 -0
- data/lib/octopus/proxy.rb +195 -0
- data/spec/config/shards.yml +76 -0
- data/spec/database_connection.rb +6 -0
- data/spec/database_models.rb +23 -0
- data/spec/migrations/1_create_users_on_master.rb +9 -0
- data/spec/migrations/2_create_users_on_canada.rb +11 -0
- data/spec/migrations/3_create_users_on_both_shards.rb +11 -0
- data/spec/migrations/4_create_users_on_shards_of_a_group.rb +11 -0
- data/spec/migrations/5_create_users_on_multiples_groups.rb +11 -0
- data/spec/migrations/6_raise_exception_with_invalid_shard_name.rb +11 -0
- data/spec/migrations/7_raise_exception_with_invalid_multiple_shard_names.rb +11 -0
- data/spec/migrations/8_raise_exception_with_invalid_group_name.rb +11 -0
- data/spec/migrations/9_raise_exception_with_multiple_invalid_group_names.rb +11 -0
- data/spec/octopus/controller_spec.rb +7 -0
- data/spec/octopus/migration_spec.rb +79 -0
- data/spec/octopus/model_spec.rb +122 -0
- data/spec/octopus/octopus_spec.rb +15 -0
- data/spec/octopus/proxy_spec.rb +112 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +27 -0
- metadata +123 -0
@@ -0,0 +1,79 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Octopus::Migration do
|
4
|
+
it "should run just in the master shard" do
|
5
|
+
ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT, 1)
|
6
|
+
|
7
|
+
User.using(:master).find_by_name("Master").should_not be_nil
|
8
|
+
User.using(:canada).find_by_name("Master").should be_nil
|
9
|
+
|
10
|
+
ActiveRecord::Migrator.run(:down, MIGRATIONS_ROOT, 1)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should run on specific shard" do
|
14
|
+
ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT, 2)
|
15
|
+
|
16
|
+
User.using(:master).find_by_name("Sharding").should be_nil
|
17
|
+
User.using(:canada).find_by_name("Sharding").should_not be_nil
|
18
|
+
|
19
|
+
ActiveRecord::Migrator.run(:down, MIGRATIONS_ROOT, 2)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should run on specifieds shards" do
|
23
|
+
ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT, 3)
|
24
|
+
|
25
|
+
User.using(:brazil).find_by_name("Both").should_not be_nil
|
26
|
+
User.using(:canada).find_by_name("Both").should_not be_nil
|
27
|
+
|
28
|
+
ActiveRecord::Migrator.run(:down, MIGRATIONS_ROOT, 3)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should run on specified group" do
|
32
|
+
ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT, 4)
|
33
|
+
|
34
|
+
User.using(:canada).find_by_name("Group").should_not be_nil
|
35
|
+
User.using(:brazil).find_by_name("Group").should_not be_nil
|
36
|
+
User.using(:russia).find_by_name("Group").should_not be_nil
|
37
|
+
|
38
|
+
ActiveRecord::Migrator.run(:down, MIGRATIONS_ROOT, 4)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should run on multiples groups" do
|
42
|
+
ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT, 5)
|
43
|
+
|
44
|
+
User.using(:canada).where(:name => "MultipleGroup").all.size.should == 2
|
45
|
+
User.using(:brazil).where(:name => "MultipleGroup").all.size.should == 2
|
46
|
+
User.using(:russia).where(:name => "MultipleGroup").all.size.should == 2
|
47
|
+
|
48
|
+
ActiveRecord::Migrator.run(:down, MIGRATIONS_ROOT, 5)
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "should raise a exception when" do
|
52
|
+
it "you specify a invalid shard name" do
|
53
|
+
lambda { ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT, 6) }.should raise_error("Nonexistent Shard Name: amazing_shard")
|
54
|
+
end
|
55
|
+
|
56
|
+
it "you specify a invalid shard name, even if you have multiple shards, and one of them are right" do
|
57
|
+
lambda { ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT, 7) }.should raise_error("Nonexistent Shard Name: invalid_shard")
|
58
|
+
end
|
59
|
+
|
60
|
+
it "you specify a invalid group name" do
|
61
|
+
lambda { ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT, 8) }.should raise_error("Nonexistent Group Name: invalid_group")
|
62
|
+
end
|
63
|
+
|
64
|
+
it "you specify a invalid group name, even if you have multiple groups, and one of them are right" do
|
65
|
+
lambda { ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT, 9) }.should raise_error("Nonexistent Group Name: invalid_group")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "when using replication" do
|
70
|
+
it "should run writes on master when you use replication" do
|
71
|
+
pending()
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should run in all shards, master or another shards" do
|
75
|
+
pending()
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Octopus::Model do
|
4
|
+
describe "#using method" do
|
5
|
+
it "should return self after calling the #using method" do
|
6
|
+
User.using(:canada).should == User
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should allow selecting the shards on scope" do
|
10
|
+
User.using(:canada).create!(:name => 'oi')
|
11
|
+
User.using(:canada).count.should == 1
|
12
|
+
User.count.should == 0
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should allow scoping dynamically" do
|
16
|
+
User.using(:canada).using(:master).using(:canada).create!(:name => 'oi')
|
17
|
+
User.using(:canada).using(:master).count.should == 0
|
18
|
+
User.using(:master).using(:canada).count.should == 1
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should clean the current_shard after executing the current query" do
|
22
|
+
User.using(:canada).create!(:name => "oi")
|
23
|
+
User.count.should == 0
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should support both groups and alone shards" do
|
27
|
+
User.using(:alone_shard).create!(:name => "Alone")
|
28
|
+
User.using(:alone_shard).count.should == 1
|
29
|
+
User.using(:canada).count.should == 0
|
30
|
+
User.using(:brazil).count.should == 0
|
31
|
+
User.count.should == 0
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "passing a block" do
|
35
|
+
it "should allow queries be executed inside the block, ponting to a specific shard" do
|
36
|
+
User.using(:canada) do
|
37
|
+
User.create(:name => "oi")
|
38
|
+
end
|
39
|
+
|
40
|
+
User.using(:canada).count.should == 1
|
41
|
+
User.using(:master).count.should == 0
|
42
|
+
User.count.should == 0
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should allow execute queries inside a model" do
|
46
|
+
u = User.new
|
47
|
+
u.awesome_queries()
|
48
|
+
User.using(:canada).count.should == 1
|
49
|
+
User.count.should == 0
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "when you have a relationship" do
|
54
|
+
it "should find all models in the specified shard" do
|
55
|
+
pending()
|
56
|
+
# brazil_client = Client.using(:brazil).create!(:name => "Brazil Client")
|
57
|
+
# master_client = Client.create!(:name => "Master Client")
|
58
|
+
#
|
59
|
+
# item_brazil = Item.using(:brazil).create!(:name => "Brazil Item", :client => brazil_client)
|
60
|
+
# item_master = Item.create!(:name => "Master Item", :client => master_client)
|
61
|
+
# c = Client.using(:brazil).find_by_name("Brazil Client")
|
62
|
+
# Client.using(:master).create!(:name => "teste")
|
63
|
+
# c.items.should == [item_brazil]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe "raising errors" do
|
68
|
+
it "should raise a error when you specify a shard that doesn't exist" do
|
69
|
+
lambda { User.using(:crazy_shard) }.should raise_error("Nonexistent Shard Name: crazy_shard")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "using a postgresql shard" do
|
75
|
+
after(:each) do
|
76
|
+
User.using(:postgresql_shard).delete_all
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should update the Arel Engine" do
|
80
|
+
User.using(:postgresql_shard).arel_engine.connection.adapter_name.should == "PostgreSQL"
|
81
|
+
User.using(:alone_shard).arel_engine.connection.adapter_name.should == "MySQL"
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should works with writes and reads" do
|
85
|
+
pending()
|
86
|
+
#u = User.using(:postgresql_shard).create!(:name => "PostgreSQL User")
|
87
|
+
# #User.using(:postgresql_shard).arel_table.columns.should == ""
|
88
|
+
# User.using(:postgresql_shard).scoped.should == ""
|
89
|
+
# User.using(:alone_shard).find(:all).should == []
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe "#replicated_model method" do
|
94
|
+
before(:each) do
|
95
|
+
Octopus.stub!(:env).and_return("production_replicated")
|
96
|
+
@proxy = Octopus::Proxy.new(Octopus.config())
|
97
|
+
#TODO - This is ugly, but is better than mocking
|
98
|
+
ActiveRecord::Base.class_eval("@@connection_proxy = nil")
|
99
|
+
#TODO - This is ugly, but is better than mocking
|
100
|
+
end
|
101
|
+
|
102
|
+
after(:each) do
|
103
|
+
#TODO - One little Kitten dies each time this code is executed.
|
104
|
+
ActiveRecord::Base.class_eval("@@connection_proxy = nil")
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should be replicated" do
|
108
|
+
ActiveRecord::Base.connection_proxy.replicated.should be_true
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should mark the Cat model as replicated" do
|
112
|
+
Cat.all.should == []
|
113
|
+
ActiveRecord::Base.connection_proxy.replicated_models.first.should == "Cat"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
describe "#sharded_by method" do
|
118
|
+
it "should send all queries to the specify shard" do
|
119
|
+
pending()
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Octopus do
|
4
|
+
describe "#config method" do
|
5
|
+
it "should load shards.yml file to start working" do
|
6
|
+
Octopus.config().should be_kind_of(Hash)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "#directory method" do
|
11
|
+
it "should return the directory that contains the shards.yml file" do
|
12
|
+
Octopus.directory().should == File.expand_path(File.dirname(__FILE__) + "/../")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Octopus::Proxy do
|
4
|
+
before(:each) do
|
5
|
+
@proxy = Octopus::Proxy.new(Octopus.config())
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "creating a new instance" do
|
9
|
+
it "should initialize all shards and groups" do
|
10
|
+
@proxy.shards.keys.to_set.should == [:postgresql_shard, :alone_shard, :aug2011, :canada, :brazil, :aug2009, :russia, :aug2010, :master].to_set
|
11
|
+
@proxy.groups.should == {:country_shards=>[:canada, :brazil, :russia], :history_shards=>[:aug2009, :aug2010, :aug2011]}
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should initialize the block attribute as false" do
|
15
|
+
@proxy.block.should be_false
|
16
|
+
end
|
17
|
+
it "should initialize replicated attribute as false" do
|
18
|
+
@proxy.replicated.should be_false
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "should raise error if you have duplicated shard names" do
|
22
|
+
before(:each) do
|
23
|
+
Octopus.stub!(:env).and_return("production_raise_error")
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should raise the error" do
|
27
|
+
lambda { Octopus::Proxy.new(Octopus.config()) }.should raise_error("You have duplicated shard names!")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "returning the correct connection" do
|
33
|
+
describe "should return the shard name" do
|
34
|
+
it "when current_shard is empty" do
|
35
|
+
@proxy.shard_name.should == :master
|
36
|
+
end
|
37
|
+
|
38
|
+
it "when current_shard is a single shard" do
|
39
|
+
@proxy.current_shard = :canada
|
40
|
+
@proxy.shard_name.should == :canada
|
41
|
+
end
|
42
|
+
|
43
|
+
it "when current_shard is more than one shard" do
|
44
|
+
@proxy.current_shard = [:russia, :brazil]
|
45
|
+
@proxy.shard_name.should == :russia
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "should return the connection based on shard_name" do
|
50
|
+
it "when current_shard is empty" do
|
51
|
+
@proxy.select_connection().should == @proxy.shards[:master].connection()
|
52
|
+
end
|
53
|
+
|
54
|
+
it "when current_shard is a single shard" do
|
55
|
+
@proxy.current_shard = :canada
|
56
|
+
@proxy.select_connection().should == @proxy.shards[:canada].connection()
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "when the database is replicated" do
|
62
|
+
before(:each) do
|
63
|
+
Octopus.stub!(:env).and_return("production_replicated")
|
64
|
+
@proxy = Octopus::Proxy.new(Octopus.config())
|
65
|
+
#TODO - THIS IS SO UGLY
|
66
|
+
ActiveRecord::Base.class_eval("@@connection_proxy = nil")
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should have the replicated attribute as true" do
|
70
|
+
@proxy.replicated.should be_true
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should initialize the list of shards" do
|
74
|
+
@proxy.slaves_list.should == ["slave1", "slave2", "slave3", "slave4"]
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should send all writes queries to master" do
|
78
|
+
u = User.create!(:name => "Replicated")
|
79
|
+
|
80
|
+
[:slave4, :slave1, :slave2, :slave3].each do |sym|
|
81
|
+
User.using(sym).count.should == 0
|
82
|
+
end
|
83
|
+
|
84
|
+
User.using(:master).count.should == 1
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should send read queries to slaves, using a round robin algorithm" do
|
88
|
+
u = Cat.create!(:name => "master")
|
89
|
+
c = Client.create!(:name => "client_master")
|
90
|
+
|
91
|
+
[:slave4, :slave1, :slave2, :slave3].each do |sym|
|
92
|
+
Cat.using(sym).create!(:name => "Replicated_#{sym}")
|
93
|
+
end
|
94
|
+
|
95
|
+
Client.find(:first).should_not be_nil
|
96
|
+
Cat.find(:first).name.should == "Replicated_slave1"
|
97
|
+
Client.find(:first).should_not be_nil
|
98
|
+
Cat.find(:first).name.should == "Replicated_slave2"
|
99
|
+
Client.find(:first).should_not be_nil
|
100
|
+
Cat.find(:first).name.should == "Replicated_slave3"
|
101
|
+
Client.find(:first).should_not be_nil
|
102
|
+
Cat.find(:first).name.should == "Replicated_slave4"
|
103
|
+
Client.find(:first).should_not be_nil
|
104
|
+
Cat.find(:first).name.should == "Replicated_slave1"
|
105
|
+
Client.find(:first).should_not be_nil
|
106
|
+
|
107
|
+
[:slave4, :slave1, :slave2, :slave3].each do |sym|
|
108
|
+
Cat.using(sym).find_by_name("master").should be_false
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|