schoefmax-multi_db 0.1.2 → 0.1.3

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.
data/README.rdoc CHANGED
@@ -1,6 +1,6 @@
1
1
  = multi_db
2
2
 
3
- =====-- This Plugin was inspired by Rick Olson's "masochism"-Plugin
3
+ =====-- This GEM was inspired by Rick Olson's "masochism"-Plugin
4
4
 
5
5
  multi_db uses a connection proxy, which sends read queries to slave databases,
6
6
  and all write queries to the master database (Read/Write Split).
@@ -11,7 +11,7 @@ master database.
11
11
  === Caveats
12
12
 
13
13
  * works with activerecord 2.1 and 2.2, but is not threadsafe (yet)
14
- * when using Passenger or lightspeed you will probably need to introduce a before_filter which checks if the proxy is setup (see the discussion in the masochism readme: http://github.com/technoweenie/masochism/tree/master)
14
+ * when using Passenger or lightspeed you will probably need to introduce a before_filter which checks if the proxy is setup (see the discussion in the masochism readme: http://github.com/technoweenie/masochism)
15
15
 
16
16
  === Install
17
17
 
@@ -79,7 +79,20 @@ Just add this to your controller:
79
79
 
80
80
  In your environment.rb or an initializer, add:
81
81
 
82
- MultiDb::ConnectionProxy.master_models = ['PaymentTransaction', ...]
82
+ MultiDb::ConnectionProxy.master_models = ['CGI::Session::ActiveRecordStore', 'PaymentTransaction', ...]
83
+
84
+ === Making one slave database sticky during a request
85
+
86
+ This can be useful to leverage database level query caching as all queries will
87
+ be sent to the same slave database during one web request.
88
+
89
+ To enable, add this to your environment.rb just before <tt>MultiDb::ConnectionProxy.setup!</tt>:
90
+
91
+ MultiDb::ConnectionProxy.sticky_slave = true
92
+
93
+ And add this to your ApplicationController:
94
+
95
+ after_filter { ActiveRecord::Base.connection_proxy.next_reader! }
83
96
 
84
97
  === Usage outside of Rails
85
98
 
@@ -142,11 +155,30 @@ Note that the configurations hash should contain strings as keys instead of symb
142
155
  <tt>with_master(&block)</tt>
143
156
  * All proxied methods are dynamically generated for better performance
144
157
 
158
+ === See also
159
+
160
+ ==== Masochism
161
+
162
+ The original plugin:
163
+
164
+ * http://github.com/technoweenie/masochism
165
+
166
+ ==== DataFabric
167
+
168
+ A solution by FiveRuns, also based on masochism but without the "nested with_master"-issue,
169
+ threadsafe and allows sharding of data.
170
+
171
+ * http://github.com/fiveruns/data_fabric
172
+
145
173
  === Contributors
146
174
 
147
175
  * Matt Conway http://github.com/wr0ngway
148
176
  * Matthias Marshall http://github.com/webops
149
177
 
178
+ === Ideas
179
+
180
+ See: http://github.com/schoefmax/multi_db/wikis/home
181
+
150
182
  === Running specs
151
183
 
152
184
  If you haven't already, install the rspec gem, then create an empty database
@@ -4,24 +4,23 @@ module MultiDb
4
4
  base.alias_method_chain :reload, :master
5
5
 
6
6
  class << base
7
- def connection_proxy=(proxy)
8
- @@connection_proxy = proxy
9
- end
10
7
 
8
+ cattr_accessor :connection_proxy
9
+
11
10
  # hijack the original method
12
11
  def connection
13
12
  if ConnectionProxy.master_models.include?(self.to_s)
14
13
  self.retrieve_connection
15
14
  else
16
- @@connection_proxy
15
+ self.connection_proxy
17
16
  end
18
17
  end
19
18
  end
20
-
19
+
21
20
  end
22
21
 
23
22
  def reload_with_master(*args, &block)
24
- connection.with_master { reload_without_master }
23
+ connection_proxy.with_master { reload_without_master }
25
24
  end
26
25
  end
27
26
  end
@@ -22,14 +22,21 @@ module MultiDb
22
22
  #
23
23
  # Example:
24
24
  #
25
- # MultiDb::ConnectionProxy.master_models = %w[PaymentTransactions]
25
+ # MultiDb::ConnectionProxy.master_models = ['CGI::Session::ActiveRecordStore']
26
26
  attr_accessor :master_models
27
27
 
28
+ # decides if we should switch to the next reader automatically.
29
+ # If set to false, an after|before_filter in the ApplicationController
30
+ # has to do this.
31
+ # This will not affect failover if a master is unavailable.
32
+ attr_accessor :sticky_slave
33
+
28
34
  # Replaces the connection of ActiveRecord::Base with a proxy and
29
35
  # establishes the connections to the slaves.
30
36
  def setup!
31
37
  self.master_models ||= []
32
38
  self.environment ||= (defined?(RAILS_ENV) ? RAILS_ENV : 'development')
39
+ self.sticky_slave ||= false
33
40
 
34
41
  master = ActiveRecord::Base
35
42
  slaves = init_slaves
@@ -97,7 +104,17 @@ module MultiDb
97
104
  create_delegation_method!(method)
98
105
  end
99
106
  end
100
-
107
+
108
+ # Switches to the next slave database for read operations.
109
+ # Fails over to the master database if all slaves are unavailable.
110
+ def next_reader!
111
+ return if @with_master > 0 # don't if in with_master block
112
+ @current = @slaves.next
113
+ rescue Scheduler::NoMoreItems
114
+ logger.warn "[MULTIDB] All slaves are blacklisted. Reading from master"
115
+ @current = @master
116
+ end
117
+
101
118
  protected
102
119
 
103
120
  def get_connection(db_class)
@@ -107,10 +124,10 @@ module MultiDb
107
124
  def create_delegation_method!(method)
108
125
  self.instance_eval %Q{
109
126
  def #{method}(*args, &block)
110
- #{'next_reader!' unless unsafe?(method)}
127
+ #{'next_reader!' unless self.class.sticky_slave || unsafe?(method)}
111
128
  #{target_method(method)}(:#{method}, *args, &block)
112
129
  end
113
- }
130
+ }, __FILE__, __LINE__
114
131
  end
115
132
 
116
133
  def target_method(method)
@@ -138,14 +155,6 @@ module MultiDb
138
155
  retry
139
156
  end
140
157
 
141
- def next_reader!
142
- return if @with_master > 0 # don't if in with_master block
143
- @current = @slaves.next
144
- rescue Scheduler::NoMoreItems
145
- logger.warn "[MULTIDB] All slaves are blacklisted. Reading from master"
146
- @current = @master
147
- end
148
-
149
158
  def reconnect_master!
150
159
  get_connection(@master).reconnect!
151
160
  @reconnect = false
@@ -2,7 +2,7 @@ module MultiDb
2
2
  # Implements the methods expected by the QueryCache module
3
3
  module QueryCacheCompat
4
4
  def select_all(*a, &b)
5
- next_reader!
5
+ next_reader! unless ConnectionProxy.sticky_slave
6
6
  send_to_current(:select_all, *a, &b)
7
7
  end
8
8
  def columns(*a, &b)
data/multi_db.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{multi_db}
5
- s.version = "0.1.2"
5
+ s.version = "0.1.3"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Maximilian Sch\303\266fmann"]
@@ -18,11 +18,14 @@ describe MultiDb::ConnectionProxy do
18
18
 
19
19
  before(:each) do
20
20
  MultiDb::ConnectionProxy.setup!
21
- @proxy = ActiveRecord::Base.connection
21
+ @proxy = ActiveRecord::Base.connection_proxy
22
+ @master = @proxy.master.retrieve_connection
23
+ @slave1 = MultiDb::SlaveDatabase1.retrieve_connection
24
+ @slave2 = MultiDb::SlaveDatabase2.retrieve_connection
22
25
  end
23
26
 
24
27
  it 'AR::B#connection should return an instance of MultiDb::ConnectionProxy' do
25
- ActiveRecord::Base.connection.should be_a(MultiDb::ConnectionProxy)
28
+ ActiveRecord::Base.connection.should be_kind_of(MultiDb::ConnectionProxy)
26
29
  end
27
30
 
28
31
  it "should generate classes for each entry in the database.yml" do
@@ -47,7 +50,7 @@ describe MultiDb::ConnectionProxy do
47
50
  end
48
51
 
49
52
  it 'should perform transactions on the master' do
50
- @proxy.master.retrieve_connection.should_receive(:select_all).exactly(1) # makes sure the first one goes to a slave
53
+ @master.should_receive(:select_all).exactly(1) # makes sure the first one goes to a slave
51
54
  @proxy.select_all(@sql)
52
55
  ActiveRecord::Base.transaction do
53
56
  @proxy.select_all(@sql)
@@ -55,25 +58,25 @@ describe MultiDb::ConnectionProxy do
55
58
  end
56
59
 
57
60
  it 'should switch to the next reader on selects' do
58
- MultiDb::SlaveDatabase1.retrieve_connection.should_receive(:select_one).twice
59
- MultiDb::SlaveDatabase2.retrieve_connection.should_receive(:select_one).twice
61
+ @slave1.should_receive(:select_one).exactly(2)
62
+ @slave2.should_receive(:select_one).exactly(2)
60
63
  4.times { @proxy.select_one(@sql) }
61
64
  end
62
65
 
63
66
  it 'should not switch to the next reader when whithin a with_master-block' do
64
- @proxy.master.retrieve_connection.should_receive(:select_one).twice
65
- MultiDb::SlaveDatabase1.retrieve_connection.should_not_receive(:select_one)
66
- MultiDb::SlaveDatabase2.retrieve_connection.should_not_receive(:select_one)
67
+ @master.should_receive(:select_one).twice
68
+ @slave1.should_not_receive(:select_one)
69
+ @slave2.should_not_receive(:select_one)
67
70
  @proxy.with_master do
68
71
  2.times { @proxy.select_one(@sql) }
69
72
  end
70
73
  end
71
-
74
+
72
75
  it 'should send dangerous methods to the master' do
73
76
  meths = [:insert, :update, :delete, :execute]
74
77
  meths.each do |meth|
75
- MultiDb::SlaveDatabase1.retrieve_connection.stub!(meth).and_raise(RuntimeError)
76
- @proxy.master.retrieve_connection.should_receive(meth).and_return(true)
78
+ @slave1.stub!(meth).and_raise(RuntimeError)
79
+ @master.should_receive(meth).and_return(true)
77
80
  @proxy.send(meth, @sql)
78
81
  end
79
82
  end
@@ -87,9 +90,9 @@ describe MultiDb::ConnectionProxy do
87
90
  it 'should cache queries using select_all' do
88
91
  ActiveRecord::Base.cache do
89
92
  # next_reader will be called and switch to the SlaveDatabase2
90
- MultiDb::SlaveDatabase2.retrieve_connection.should_receive(:select_all).exactly(1)
91
- MultiDb::SlaveDatabase1.retrieve_connection.should_not_receive(:select_all)
92
- @proxy.master.retrieve_connection.should_not_receive(:select_all)
93
+ @slave2.should_receive(:select_all).exactly(1)
94
+ @slave1.should_not_receive(:select_all)
95
+ @master.should_not_receive(:select_all)
93
96
  3.times { @proxy.select_all(@sql) }
94
97
  end
95
98
  end
@@ -98,10 +101,10 @@ describe MultiDb::ConnectionProxy do
98
101
  ActiveRecord::Base.cache do
99
102
  meths = [:insert, :update, :delete]
100
103
  meths.each do |meth|
101
- @proxy.master.retrieve_connection.should_receive(meth).and_return(true)
104
+ @master.should_receive(meth).and_return(true)
102
105
  end
103
- MultiDb::SlaveDatabase2.retrieve_connection.should_receive(:select_all).twice
104
- MultiDb::SlaveDatabase1.retrieve_connection.should_receive(:select_all).once
106
+ @slave2.should_receive(:select_all).twice
107
+ @slave1.should_receive(:select_all).once
105
108
  3.times do |i|
106
109
  @proxy.select_all(@sql)
107
110
  @proxy.send(meths[i])
@@ -110,31 +113,52 @@ describe MultiDb::ConnectionProxy do
110
113
  end
111
114
 
112
115
  it 'should retry the next slave when one fails and finally fall back to the master' do
113
- MultiDb::SlaveDatabase1.retrieve_connection.should_receive(:select_all).once.and_raise(RuntimeError)
114
- MultiDb::SlaveDatabase2.retrieve_connection.should_receive(:select_all).once.and_raise(RuntimeError)
115
- @proxy.master.retrieve_connection.should_receive(:select_all).and_return(true)
116
+ @slave1.should_receive(:select_all).once.and_raise(RuntimeError)
117
+ @slave2.should_receive(:select_all).once.and_raise(RuntimeError)
118
+ @master.should_receive(:select_all).and_return(true)
116
119
  @proxy.select_all(@sql)
117
120
  end
118
121
 
119
122
  it 'should try to reconnect the master connection after the master has failed' do
120
- @proxy.master.retrieve_connection.should_receive(:update).and_raise(RuntimeError)
123
+ @master.should_receive(:update).and_raise(RuntimeError)
121
124
  lambda { @proxy.update(@sql) }.should raise_error
122
- @proxy.master.retrieve_connection.should_receive(:reconnect!).and_return(true)
123
- @proxy.master.retrieve_connection.should_receive(:insert).and_return(1)
125
+ @master.should_receive(:reconnect!).and_return(true)
126
+ @master.should_receive(:insert).and_return(1)
124
127
  @proxy.insert(@sql)
125
128
  end
126
129
 
127
130
  it 'should always use the master database for models configured as master models' do
128
- MultiDb::SlaveDatabase2.retrieve_connection.should_receive(:select_all).once.and_return([])
131
+ @slave2.should_receive(:select_all).once.and_return([])
129
132
  MasterModel.connection.should == @proxy
130
133
  MasterModel.first
131
134
 
132
135
  MultiDb::ConnectionProxy.master_models = ['MasterModel']
133
- MasterModel.connection.should == @proxy.master.retrieve_connection
134
- MultiDb::SlaveDatabase1.retrieve_connection.should_not_receive(:select_all)
136
+ MasterModel.connection.should == @master
137
+ @slave1.should_not_receive(:select_all)
135
138
  MasterModel.retrieve_connection.should_receive(:select_all).once.and_return([])
136
139
  MasterModel.first
137
140
  end
138
141
 
142
+ describe 'with sticky_slave ' do
143
+
144
+ before { MultiDb::ConnectionProxy.sticky_slave = true }
145
+ after { MultiDb::ConnectionProxy.sticky_slave = false }
146
+
147
+ it 'should not switch to the next reader when #sticky_master is true' do
148
+ @slave1.should_receive(:select_all).exactly(3)
149
+ @slave2.should_receive(:select_all).exactly(0)
150
+ 3.times { @proxy.select_all(@sql) }
151
+ end
152
+
153
+ it '#next_reader! should switch to the next slave' do
154
+ @slave1.should_receive(:select_one).exactly(3)
155
+ @slave2.should_receive(:select_one).exactly(7)
156
+ 3.times { @proxy.select_one(@sql) }
157
+ @proxy.next_reader!
158
+ 7.times { @proxy.select_one(@sql) }
159
+ end
160
+
161
+ end
162
+
139
163
  end
140
164
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: schoefmax-multi_db
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - "Maximilian Sch\xC3\xB6fmann"