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 +35 -3
- data/lib/multi_db/active_record_extensions.rb +5 -6
- data/lib/multi_db/connection_proxy.rb +21 -12
- data/lib/multi_db/query_cache_compat.rb +1 -1
- data/multi_db.gemspec +1 -1
- data/spec/connection_proxy_spec.rb +50 -26
- metadata +1 -1
data/README.rdoc
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
= multi_db
|
|
2
2
|
|
|
3
|
-
=====-- This
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
data/multi_db.gemspec
CHANGED
|
@@ -18,11 +18,14 @@ describe MultiDb::ConnectionProxy do
|
|
|
18
18
|
|
|
19
19
|
before(:each) do
|
|
20
20
|
MultiDb::ConnectionProxy.setup!
|
|
21
|
-
@proxy = ActiveRecord::Base.
|
|
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
|
|
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
|
-
@
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
@
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
76
|
-
@
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
@
|
|
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
|
-
@
|
|
104
|
+
@master.should_receive(meth).and_return(true)
|
|
102
105
|
end
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
@
|
|
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
|
-
@
|
|
123
|
+
@master.should_receive(:update).and_raise(RuntimeError)
|
|
121
124
|
lambda { @proxy.update(@sql) }.should raise_error
|
|
122
|
-
@
|
|
123
|
-
@
|
|
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
|
-
|
|
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 == @
|
|
134
|
-
|
|
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
|
|