ar-octopus 0.8.1 → 0.8.2
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.
- checksums.yaml +8 -8
- data/README.mkdn +80 -55
- data/lib/octopus.rb +20 -6
- data/lib/octopus/{rails3/abstract_adapter.rb → abstract_adapter.rb} +1 -4
- data/lib/octopus/association.rb +5 -99
- data/lib/octopus/association_shard_tracking.rb +105 -0
- data/lib/octopus/collection_association.rb +9 -0
- data/lib/octopus/collection_proxy.rb +14 -0
- data/lib/octopus/has_and_belongs_to_many_association.rb +2 -12
- data/lib/octopus/load_balancing.rb +3 -0
- data/lib/octopus/load_balancing/round_robin.rb +15 -0
- data/lib/octopus/{rails3/log_subscriber.rb → log_subscriber.rb} +0 -0
- data/lib/octopus/model.rb +74 -85
- data/lib/octopus/{rails3/persistence.rb → persistence.rb} +0 -0
- data/lib/octopus/proxy.rb +166 -29
- data/lib/octopus/relation_proxy.rb +39 -0
- data/lib/octopus/scope_proxy.rb +7 -10
- data/lib/octopus/shard_tracking.rb +45 -0
- data/lib/octopus/shard_tracking/attribute.rb +24 -0
- data/lib/octopus/shard_tracking/dynamic.rb +7 -0
- data/lib/octopus/singular_association.rb +7 -0
- data/lib/octopus/slave_group.rb +11 -0
- data/lib/octopus/version.rb +1 -1
- data/spec/config/shards.yml +53 -0
- data/spec/octopus/{association_spec.rb → association_shard_tracking_spec.rb} +1 -1
- data/spec/octopus/collection_proxy_spec.rb +15 -0
- data/spec/octopus/model_spec.rb +2 -2
- data/spec/octopus/octopus_spec.rb +34 -0
- data/spec/octopus/relation_proxy_spec.rb +77 -0
- data/spec/octopus/replicated_slave_grouped_spec.rb +64 -0
- data/spec/octopus/sharded_replicated_slave_grouped_spec.rb +55 -0
- data/spec/support/octopus_helper.rb +1 -0
- metadata +26 -9
- data/lib/octopus/association_collection.rb +0 -49
- data/lib/octopus/rails3/singular_association.rb +0 -34
@@ -0,0 +1,39 @@
|
|
1
|
+
module Octopus
|
2
|
+
class RelationProxy
|
3
|
+
include Octopus::ShardTracking::Attribute
|
4
|
+
|
5
|
+
attr_accessor :ar_relation
|
6
|
+
|
7
|
+
def initialize(shard, ar_relation)
|
8
|
+
@current_shard = shard
|
9
|
+
@ar_relation = ar_relation
|
10
|
+
end
|
11
|
+
|
12
|
+
def method_missing(method, *args, &block)
|
13
|
+
run_on_shard { @ar_relation.send(method, *args, &block) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def respond_to?(*args)
|
17
|
+
super || @ar_relation.respond_to?(*args)
|
18
|
+
end
|
19
|
+
|
20
|
+
# these methods are not normally sent to method_missing
|
21
|
+
def inspect
|
22
|
+
method_missing(:inspect)
|
23
|
+
end
|
24
|
+
|
25
|
+
def as_json(options = nil)
|
26
|
+
method_missing(:as_json, options)
|
27
|
+
end
|
28
|
+
|
29
|
+
def ==(other)
|
30
|
+
case other
|
31
|
+
when Octopus::RelationProxy
|
32
|
+
method_missing(:==, other.ar_relation)
|
33
|
+
else
|
34
|
+
method_missing(:==, other)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
alias :eql? :==
|
38
|
+
end
|
39
|
+
end
|
data/lib/octopus/scope_proxy.rb
CHANGED
@@ -1,33 +1,30 @@
|
|
1
1
|
class Octopus::ScopeProxy
|
2
|
-
|
2
|
+
include Octopus::ShardTracking::Attribute
|
3
|
+
attr_accessor :klass
|
3
4
|
|
4
5
|
def initialize(shard, klass)
|
5
|
-
@
|
6
|
+
@current_shard = shard
|
6
7
|
@klass = klass
|
7
8
|
end
|
8
9
|
|
9
10
|
def using(shard)
|
10
11
|
raise "Nonexistent Shard Name: #{shard}" if @klass.connection.instance_variable_get(:@shards)[shard].nil?
|
11
|
-
@
|
12
|
+
@current_shard = shard
|
12
13
|
return self
|
13
14
|
end
|
14
15
|
|
15
16
|
# Transaction Method send all queries to a specified shard.
|
16
17
|
def transaction(options = {}, &block)
|
17
|
-
@klass.
|
18
|
-
@klass = @klass.connection().transaction(options, &block)
|
19
|
-
end
|
18
|
+
run_on_shard { @klass = klass.transaction(options, &block) }
|
20
19
|
end
|
21
20
|
|
22
21
|
def connection
|
23
|
-
@klass.connection().current_shard = @
|
22
|
+
@klass.connection().current_shard = @current_shard
|
24
23
|
@klass.connection()
|
25
24
|
end
|
26
25
|
|
27
26
|
def method_missing(method, *args, &block)
|
28
|
-
result = @klass.
|
29
|
-
@klass.send(method, *args, &block)
|
30
|
-
end
|
27
|
+
result = run_on_shard { @klass.send(method, *args, &block) }
|
31
28
|
|
32
29
|
if result.respond_to?(:scoped)
|
33
30
|
@klass = result
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Octopus::ShardTracking
|
2
|
+
def self.included(base)
|
3
|
+
base.extend(ClassMethods)
|
4
|
+
end
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
# If the class which includes this module responds to the class
|
8
|
+
# method sharded_methods, then automagically alias_method_chain
|
9
|
+
# a sharding-friendly version of each of those methods into existence
|
10
|
+
def sharded_methods(*methods)
|
11
|
+
methods.each { |m| create_sharded_method(m) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_sharded_method(name)
|
15
|
+
name.to_s =~ /([^!?]+)([!?])?/
|
16
|
+
method, punctuation = [ $1, $2 ]
|
17
|
+
with = :"#{method}_with_octopus#{punctuation}"
|
18
|
+
without = :"#{method}_without_octopus#{punctuation}"
|
19
|
+
define_method with do |*args, &block|
|
20
|
+
run_on_shard { send(without, *args, &block) }
|
21
|
+
end
|
22
|
+
alias_method_chain name.to_sym, :octopus
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Adds run_on_shard method, but does not implement current_shard method
|
27
|
+
def run_on_shard(&block)
|
28
|
+
cs = current_shard
|
29
|
+
if !!cs
|
30
|
+
r = ActiveRecord::Base.connection_proxy.run_queries_on_shard(current_shard, &block)
|
31
|
+
# Use a case statement to avoid any path through ActiveRecord::Delegation's
|
32
|
+
# respond_to? code. We want to avoid the respond_to? code because it can have
|
33
|
+
# the side effect of causing a call to load_target
|
34
|
+
# return r
|
35
|
+
case r
|
36
|
+
when ActiveRecord::Relation
|
37
|
+
Octopus::RelationProxy.new(cs, r)
|
38
|
+
else
|
39
|
+
r
|
40
|
+
end
|
41
|
+
else
|
42
|
+
yield
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# Adds current_shard as an attribute; provide a default
|
2
|
+
# implementation of set_current_shard which considers
|
3
|
+
# only the current ActiveRecord::Base.connection_proxy
|
4
|
+
module Octopus::ShardTracking::Attribute
|
5
|
+
def self.included(base)
|
6
|
+
base.send(:include, Octopus::ShardTracking)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
base.track_current_shard_as_attribute
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def track_current_shard_as_attribute
|
13
|
+
attr_accessor :current_shard
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def set_current_shard
|
18
|
+
return unless Octopus.enabled?
|
19
|
+
|
20
|
+
if ActiveRecord::Base.connection_proxy.block
|
21
|
+
self.current_shard = ActiveRecord::Base.connection_proxy.current_shard
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/octopus/version.rb
CHANGED
data/spec/config/shards.yml
CHANGED
@@ -142,6 +142,59 @@ not_entire_sharded:
|
|
142
142
|
database: octopus_shard_5
|
143
143
|
<<: *mysql
|
144
144
|
|
145
|
+
sharded_replicated_slave_grouped:
|
146
|
+
replicated: true
|
147
|
+
fully_replicated: true
|
148
|
+
shards:
|
149
|
+
russia:
|
150
|
+
database: octopus_shard_1
|
151
|
+
<<: *mysql
|
152
|
+
slaves1:
|
153
|
+
russia_slave11:
|
154
|
+
database: octopus_shard_1
|
155
|
+
<<: *mysql
|
156
|
+
russia_slave21:
|
157
|
+
database: octopus_shard_2
|
158
|
+
<<: *mysql
|
159
|
+
slaves2:
|
160
|
+
russia_slave21:
|
161
|
+
database: octopus_shard_3
|
162
|
+
<<: *mysql
|
163
|
+
russia_slave22:
|
164
|
+
database: octopus_shard_1
|
165
|
+
<<: *mysql
|
166
|
+
europe:
|
167
|
+
database: octopus_shard_2
|
168
|
+
<<: *mysql
|
169
|
+
slaves1:
|
170
|
+
europe_slave11:
|
171
|
+
database: octopus_shard_3
|
172
|
+
<<: *mysql
|
173
|
+
slaves2:
|
174
|
+
europe_slave21:
|
175
|
+
database: octopus_shard_1
|
176
|
+
<<: *mysql
|
177
|
+
|
178
|
+
replicated_slave_grouped:
|
179
|
+
replicated: true
|
180
|
+
fully_replicated: true
|
181
|
+
shards:
|
182
|
+
slaves1:
|
183
|
+
slave11:
|
184
|
+
database: octopus_shard_2
|
185
|
+
<<: *mysql
|
186
|
+
slaves2:
|
187
|
+
slave21:
|
188
|
+
database: octopus_shard_1
|
189
|
+
<<: *mysql
|
190
|
+
slaves3:
|
191
|
+
slave31:
|
192
|
+
database: octopus_shard_1
|
193
|
+
<<: *mysql
|
194
|
+
slave32:
|
195
|
+
database: octopus_shard_2
|
196
|
+
<<: *mysql
|
197
|
+
|
145
198
|
modify_config:
|
146
199
|
replicated: true
|
147
200
|
shards:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
|
3
|
-
describe Octopus::
|
3
|
+
describe Octopus::AssociationShardTracking, :shards => [:brazil, :master, :canada] do
|
4
4
|
describe "when you have a 1 x 1 relationship" do
|
5
5
|
before(:each) do
|
6
6
|
@computer_brazil = Computer.using(:brazil).create!(:name => "Computer Brazil")
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Octopus::CollectionProxy do
|
4
|
+
describe "method dispatch" do
|
5
|
+
before :each do
|
6
|
+
@client = Client.using(:canada).create!
|
7
|
+
@client.items << Item.using(:canada).create!
|
8
|
+
end
|
9
|
+
|
10
|
+
it "computes the size of the collection without loading it" do
|
11
|
+
@client.items.size.should eq(1)
|
12
|
+
@client.items.should_not be_loaded
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/spec/octopus/model_spec.rb
CHANGED
@@ -361,14 +361,14 @@ describe Octopus::Model do
|
|
361
361
|
@user = User.using(:brazil).create!(:name => "User1")
|
362
362
|
User.using(:brazil).update_all({:updated_at => Time.now - 3.months}, {:id => @user.id})
|
363
363
|
@user.touch
|
364
|
-
@user.reload.updated_at.to_date.should eq(
|
364
|
+
@user.reload.updated_at.in_time_zone('GMT').to_date.should eq(Time.now.in_time_zone('GMT').to_date)
|
365
365
|
end
|
366
366
|
|
367
367
|
it "updates passed in attribute name" do
|
368
368
|
@user = User.using(:brazil).create!(:name => "User1")
|
369
369
|
User.using(:brazil).update_all({:created_at => Time.now - 3.months}, {:id => @user.id})
|
370
370
|
@user.touch(:created_at)
|
371
|
-
@user.reload.created_at.to_date.should eq(
|
371
|
+
@user.reload.created_at.in_time_zone('GMT').to_date.should eq(Time.now.in_time_zone('GMT').to_date)
|
372
372
|
end
|
373
373
|
end
|
374
374
|
|
@@ -87,4 +87,38 @@ describe Octopus, :shards => [] do
|
|
87
87
|
Octopus.should_not be_enabled
|
88
88
|
end
|
89
89
|
end
|
90
|
+
|
91
|
+
describe "#fully_replicated" do
|
92
|
+
before do
|
93
|
+
OctopusHelper.using_environment :production_replicated do
|
94
|
+
OctopusHelper.clean_all_shards([:slave1, :slave2, :slave3, :slave4])
|
95
|
+
4.times { |i| User.using(:"slave#{i+1}").create!(:name => "Slave User") }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
it "sends queries to slaves" do
|
100
|
+
OctopusHelper.using_environment :production_replicated do
|
101
|
+
User.count.should eq(0)
|
102
|
+
4.times do |i|
|
103
|
+
Octopus.fully_replicated do
|
104
|
+
User.count.should eq(1)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
it "allows nesting" do
|
111
|
+
OctopusHelper.using_environment :production_replicated do
|
112
|
+
Octopus.fully_replicated do
|
113
|
+
User.count.should eq(1)
|
114
|
+
|
115
|
+
Octopus.fully_replicated do
|
116
|
+
User.count.should eq(1)
|
117
|
+
end
|
118
|
+
|
119
|
+
User.count.should eq(1)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
90
124
|
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Octopus::RelationProxy do
|
4
|
+
describe "shard tracking" do
|
5
|
+
before :each do
|
6
|
+
@client = Client.using(:canada).create!
|
7
|
+
@client.items << Item.using(:canada).create!
|
8
|
+
@relation = @client.items
|
9
|
+
end
|
10
|
+
|
11
|
+
it "remembers the shard on which a relation was created" do
|
12
|
+
@relation.current_shard.should eq(:canada)
|
13
|
+
end
|
14
|
+
|
15
|
+
context "when comparing to other Relation objects" do
|
16
|
+
before :each do
|
17
|
+
@relation.reset
|
18
|
+
end
|
19
|
+
|
20
|
+
it "is equal to its clone" do
|
21
|
+
@relation.should eq(@relation.clone)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
if Octopus.rails4?
|
26
|
+
context "under Rails 4" do
|
27
|
+
it "is an Octopus::RelationProxy" do
|
28
|
+
@relation.class.should eq(Octopus::RelationProxy)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should be able to return its ActiveRecord::Relation" do
|
32
|
+
@relation.ar_relation.is_a?(ActiveRecord::Relation).should be_true
|
33
|
+
end
|
34
|
+
|
35
|
+
it "is equal to an identically-defined, but different, RelationProxy" do
|
36
|
+
i = @client.items
|
37
|
+
@relation.should eq(i)
|
38
|
+
@relation.object_id.should_not eq(i.object_id)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "is equal to its own underlying ActiveRecord::Relation" do
|
42
|
+
@relation.should eq(@relation.ar_relation)
|
43
|
+
@relation.ar_relation.should eq(@relation)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "when no explicit shard context is provided" do
|
49
|
+
it "uses the correct shard" do
|
50
|
+
@relation.count.should eq(1)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "lazily evaluates on the correct shard" do
|
54
|
+
# Do something to force Client.connection_proxy.current_shard to change
|
55
|
+
other_count = Client.using(:brazil).count
|
56
|
+
@relation.select(:client_id).count.should == 1
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context "when an explicit, but different, shard context is provided" do
|
61
|
+
it "uses the correct shard" do
|
62
|
+
Item.using(:brazil).count.should eq(0)
|
63
|
+
clients_on_brazil = Client.using(:brazil).all
|
64
|
+
Client.using(:brazil) do
|
65
|
+
@relation.count.should eq(1)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
it "lazily evaluates on the correct shard" do
|
70
|
+
Item.using(:brazil).count.should eq(0)
|
71
|
+
Client.using(:brazil) do
|
72
|
+
@relation.select(:client_id).count.should == 1
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "when the database is replicated and has slave groups" do
|
4
|
+
|
5
|
+
it "should pick the slave group based on current_slave_grup when you have a replicated model" do
|
6
|
+
|
7
|
+
OctopusHelper.using_environment :replicated_slave_grouped do
|
8
|
+
# The following two calls of `create!` both creates cats in :master(The database `octopus_shard_1`)
|
9
|
+
# which is configured through RAILS_ENV and database.yml
|
10
|
+
Cat.create!(:name => "Thiago1")
|
11
|
+
Cat.create!(:name => "Thiago2")
|
12
|
+
|
13
|
+
# See "replicated_slave_grouped" defined in shards.yml
|
14
|
+
# We have:
|
15
|
+
# The database `octopus_shard_1` as :slave21 which is a member of the slave group :slaves2, and as :master
|
16
|
+
# The databse `octopus_shard_2` as :slave11 which is a member of the slave group :slaves1
|
17
|
+
# When a select-count query is sent to `octopus_shard_1`, it should return 2 because we have create two cats in :master .
|
18
|
+
# When a select-count query is sent to `octopus_shard_2`, it should return 0.
|
19
|
+
|
20
|
+
# The query goes to `octopus_shard_1`
|
21
|
+
Cat.using(:master).count.should == 2
|
22
|
+
# The query goes to `octopus_shard_1`
|
23
|
+
Cat.count.should == 2
|
24
|
+
# The query goes to `octopus_shard_2`
|
25
|
+
Cat.using(slave_group: :slaves1).count.should == 0
|
26
|
+
# The query goes to `octopus_shard_1`
|
27
|
+
Cat.using(slave_group: :slaves2).count.should == 2
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should distribute queries between slaves in a slave group in round-robin" do
|
32
|
+
OctopusHelper.using_environment :replicated_slave_grouped do
|
33
|
+
# The query goes to :master(`octopus_shard_1`)
|
34
|
+
Cat.create!(:name => "Thiago1")
|
35
|
+
# The query goes to :master(`octopus_shard_1`)
|
36
|
+
Cat.create!(:name => "Thiago2")
|
37
|
+
|
38
|
+
# The query goes to :slave32(`octopus_shard_2`)
|
39
|
+
Cat.using(slave_group: :slaves3).count.should == 0
|
40
|
+
# The query goes to :slave31(`octopus_shard_1`)
|
41
|
+
Cat.using(slave_group: :slaves3).count.should == 2
|
42
|
+
# The query goes to :slave32(`octopus_shard_2`)
|
43
|
+
Cat.using(slave_group: :slaves3).count.should == 0
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should make queries to master when slave groups are configured but not selected" do
|
48
|
+
OctopusHelper.using_environment :replicated_slave_grouped do
|
49
|
+
# All the queries go to :master(`octopus_shard_1`)
|
50
|
+
|
51
|
+
Cat.create!(:name => "Thiago1")
|
52
|
+
Cat.create!(:name => "Thiago2")
|
53
|
+
|
54
|
+
# In `database.yml` and `shards.yml`, we have configured 1 master and 4 slaves.
|
55
|
+
# So we can ensure Octopus is not distributing queries between them
|
56
|
+
# by asserting 1 + 4 = 5 queries go to :master(`octopus_shard_1`)
|
57
|
+
Cat.count.should == 2
|
58
|
+
Cat.count.should == 2
|
59
|
+
Cat.count.should == 2
|
60
|
+
Cat.count.should == 2
|
61
|
+
Cat.count.should == 2
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|