ar-octopus 0.8.1 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +8 -8
  2. data/README.mkdn +80 -55
  3. data/lib/octopus.rb +20 -6
  4. data/lib/octopus/{rails3/abstract_adapter.rb → abstract_adapter.rb} +1 -4
  5. data/lib/octopus/association.rb +5 -99
  6. data/lib/octopus/association_shard_tracking.rb +105 -0
  7. data/lib/octopus/collection_association.rb +9 -0
  8. data/lib/octopus/collection_proxy.rb +14 -0
  9. data/lib/octopus/has_and_belongs_to_many_association.rb +2 -12
  10. data/lib/octopus/load_balancing.rb +3 -0
  11. data/lib/octopus/load_balancing/round_robin.rb +15 -0
  12. data/lib/octopus/{rails3/log_subscriber.rb → log_subscriber.rb} +0 -0
  13. data/lib/octopus/model.rb +74 -85
  14. data/lib/octopus/{rails3/persistence.rb → persistence.rb} +0 -0
  15. data/lib/octopus/proxy.rb +166 -29
  16. data/lib/octopus/relation_proxy.rb +39 -0
  17. data/lib/octopus/scope_proxy.rb +7 -10
  18. data/lib/octopus/shard_tracking.rb +45 -0
  19. data/lib/octopus/shard_tracking/attribute.rb +24 -0
  20. data/lib/octopus/shard_tracking/dynamic.rb +7 -0
  21. data/lib/octopus/singular_association.rb +7 -0
  22. data/lib/octopus/slave_group.rb +11 -0
  23. data/lib/octopus/version.rb +1 -1
  24. data/spec/config/shards.yml +53 -0
  25. data/spec/octopus/{association_spec.rb → association_shard_tracking_spec.rb} +1 -1
  26. data/spec/octopus/collection_proxy_spec.rb +15 -0
  27. data/spec/octopus/model_spec.rb +2 -2
  28. data/spec/octopus/octopus_spec.rb +34 -0
  29. data/spec/octopus/relation_proxy_spec.rb +77 -0
  30. data/spec/octopus/replicated_slave_grouped_spec.rb +64 -0
  31. data/spec/octopus/sharded_replicated_slave_grouped_spec.rb +55 -0
  32. data/spec/support/octopus_helper.rb +1 -0
  33. metadata +26 -9
  34. data/lib/octopus/association_collection.rb +0 -49
  35. 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
@@ -1,33 +1,30 @@
1
1
  class Octopus::ScopeProxy
2
- attr_accessor :shard, :klass
2
+ include Octopus::ShardTracking::Attribute
3
+ attr_accessor :klass
3
4
 
4
5
  def initialize(shard, klass)
5
- @shard = shard
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
- @shard = shard
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.connection.run_queries_on_shard(@shard) do
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 = @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.connection.run_queries_on_shard(@shard) do
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
@@ -0,0 +1,7 @@
1
+ require 'octopus/shard_tracking'
2
+
3
+ module Octopus::ShardTracking::Dynamic
4
+ def self.included(base)
5
+ base.send(:include, Octopus::ShardTracking)
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Octopus::SingularAssociation
2
+ def self.included(base)
3
+ base.sharded_methods :reader, :writer, :create, :create!, :build
4
+ end
5
+ end
6
+
7
+ ActiveRecord::Associations::SingularAssociation.send(:include, Octopus::SingularAssociation)
@@ -0,0 +1,11 @@
1
+ class Octopus::SlaveGroup
2
+ def initialize(slaves)
3
+ slaves = HashWithIndifferentAccess.new(slaves)
4
+ slaves_list = slaves.values
5
+ @load_balancer = Octopus::LoadBalancing::RoundRobin.new(slaves_list)
6
+ end
7
+
8
+ def next
9
+ @load_balancer.next
10
+ end
11
+ end
@@ -1,3 +1,3 @@
1
1
  module Octopus
2
- VERSION = '0.8.1'
2
+ VERSION = '0.8.2'
3
3
  end
@@ -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::Association, :shards => [:brazil, :master, :canada] do
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
@@ -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(Date.today)
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(Date.today)
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