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