multi_db 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +236 -0
- data/lib/multi_db.rb +6 -0
- data/lib/multi_db/active_record_extensions.rb +55 -0
- data/lib/multi_db/connection_proxy.rb +189 -0
- data/lib/multi_db/observer_extensions.rb +18 -0
- data/lib/multi_db/query_cache_compat.rb +21 -0
- data/lib/multi_db/scheduler.rb +41 -0
- data/multi_db.gemspec +36 -0
- data/spec/config/database.yml +23 -0
- data/spec/connection_proxy_spec.rb +229 -0
- data/spec/scheduler_spec.rb +58 -0
- data/spec/spec_helper.rb +11 -0
- metadata +92 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT license:
|
2
|
+
Copyright (c) 2008 Maximilian Schöfmann
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
6
|
+
in the Software without restriction, including without limitation the rights
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
9
|
+
furnished to do so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in
|
12
|
+
all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,236 @@
|
|
1
|
+
= multi_db
|
2
|
+
|
3
|
+
=====-- This GEM was inspired by Rick Olson's "masochism"-Plugin
|
4
|
+
|
5
|
+
multi_db uses a connection proxy, which sends read queries to slave databases,
|
6
|
+
and all write queries to the master database (Read/Write Split).
|
7
|
+
Within transactions, while executing ActiveRecord Observers and
|
8
|
+
within "with_master" blocks (see below), even read queries are sent to the
|
9
|
+
master database.
|
10
|
+
|
11
|
+
=== Important changes in 0.2.0
|
12
|
+
|
13
|
+
* As of this version, <tt>ActiveRecord::Base.connection</tt> does not return the
|
14
|
+
connection proxy by default anymore (therefore the jump to 0.2.0). Only models
|
15
|
+
inheriting from AR::B return the proxy, unless they are defined as master_models
|
16
|
+
(see below). If you want to access the connection proxy from AR::B directly,
|
17
|
+
use <tt>ActiveRecord::Base.connection_proxy</tt>.
|
18
|
+
|
19
|
+
* This version is the first attempt for thread-safety of this gem. <em>There might
|
20
|
+
still be some threading issues left!</em>. So please test your apps thoroughly
|
21
|
+
and report any issues you might encounter.
|
22
|
+
|
23
|
+
* <tt>CGI::Session::ActiveRecordStore::Session</tt> is now automatically registered
|
24
|
+
as a master model.
|
25
|
+
|
26
|
+
=== Caveats
|
27
|
+
|
28
|
+
* works only with activerecord 2.1, 2.2 and 2.3
|
29
|
+
|
30
|
+
=== Install
|
31
|
+
|
32
|
+
gem sources --add http://gems.github.com # only if you haven't already added github
|
33
|
+
gem install schoefmax-multi_db
|
34
|
+
|
35
|
+
When using Rails, add this to your environment.rb:
|
36
|
+
|
37
|
+
config.gem 'schoefmax-multi_db', :lib => 'multi_db', :source => 'http://gems.github.com'
|
38
|
+
|
39
|
+
=== Setup
|
40
|
+
|
41
|
+
In your database.yml, add sections for the slaves, e.g.:
|
42
|
+
|
43
|
+
production: # that would be the master
|
44
|
+
adapter: mysql
|
45
|
+
database: myapp_production
|
46
|
+
username: root
|
47
|
+
password:
|
48
|
+
host: localhost
|
49
|
+
|
50
|
+
production_slave_database: # that would be a slave
|
51
|
+
adapter: mysql
|
52
|
+
database: myapp_production
|
53
|
+
username: root
|
54
|
+
password:
|
55
|
+
host: 10.0.0.2
|
56
|
+
|
57
|
+
production_slave_database_2: # another slave
|
58
|
+
...
|
59
|
+
production_slave_database_in_india: # yet another one
|
60
|
+
...
|
61
|
+
|
62
|
+
*NOTE*: multi_db identifies slave databases by looking for entries of the form
|
63
|
+
"<tt><environment>_slave_database<_optional_name></tt>". As a (useless) side effect you
|
64
|
+
get abstract classes named <tt>MultiDb::SlaveDatabaseInIndia</tt> etc.
|
65
|
+
The advantage of specifying the slaves explicitly, instead of the master, is that
|
66
|
+
you can use the same configuration file for scripts that don't use multi_db.
|
67
|
+
Also, when you decide to disable multi_db for some reason, you don't have to
|
68
|
+
swap hosts in your <tt>database.yml</tt> from master to slave (which is easy to forget...).
|
69
|
+
|
70
|
+
To enable the proxy globally, add this to your environment.rb, or some file in
|
71
|
+
config/initializers:
|
72
|
+
|
73
|
+
MultiDb::ConnectionProxy.setup!
|
74
|
+
|
75
|
+
If you only want to enable it for specific environments, add this to
|
76
|
+
the corresponding file in config/environments:
|
77
|
+
|
78
|
+
config.after_initialize do
|
79
|
+
MultiDb::ConnectionProxy.setup!
|
80
|
+
end
|
81
|
+
|
82
|
+
In the development and test environments, you can use identical configurations
|
83
|
+
for master and slave connections. This can help you finding (some of the) issues
|
84
|
+
your application might have with a replicated database setup without actually having
|
85
|
+
one on your development machine.
|
86
|
+
|
87
|
+
=== Using with Phusion Passenger
|
88
|
+
|
89
|
+
With Passengers smart spawning method, child processes forked by the ApplicationSpawner
|
90
|
+
won't have the connection proxy set up properly.
|
91
|
+
|
92
|
+
To make it work, add this to your <tt>environment.rb</tt> or an initializer script
|
93
|
+
(e.g. <tt>config/initializers/connection_proxy.rb</tt>):
|
94
|
+
|
95
|
+
if defined?(PhusionPassenger)
|
96
|
+
PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
97
|
+
if forked
|
98
|
+
# ... set MultiDb configuration options, if any ...
|
99
|
+
MultiDb::ConnectionProxy.setup!
|
100
|
+
end
|
101
|
+
end
|
102
|
+
else # not using passenger (e.g. development/testing)
|
103
|
+
# ... set MultiDb configuration options, if any ...
|
104
|
+
MultiDb::ConnectionProxy.setup!
|
105
|
+
end
|
106
|
+
|
107
|
+
Thanks to Nathan Esquenazi for testing this.
|
108
|
+
|
109
|
+
=== Forcing the master for certain actions
|
110
|
+
|
111
|
+
Just add this to your controller:
|
112
|
+
|
113
|
+
around_filter(:only => :foo_action) { |c,a| ActiveRecord::Base.connection_proxy.with_master { a.call } }
|
114
|
+
|
115
|
+
=== Forcing the master for certain models
|
116
|
+
|
117
|
+
In your environment.rb or an initializer, add this *before* the call to <tt>setup!</tt>:
|
118
|
+
|
119
|
+
MultiDb::ConnectionProxy.master_models = ['CGI::Session::ActiveRecordStore::Session', 'PaymentTransaction', ...]
|
120
|
+
MultiDb::ConnectionProxy.setup!
|
121
|
+
|
122
|
+
*NOTE*: You cannot safely add more master_models after calling <tt>setup!</tt>.
|
123
|
+
|
124
|
+
=== Making one slave database sticky during a request
|
125
|
+
|
126
|
+
This can be useful to leverage database level query caching as all queries will
|
127
|
+
be sent to the same slave database during one web request.
|
128
|
+
|
129
|
+
To enable, add this to your environment.rb just before <tt>MultiDb::ConnectionProxy.setup!</tt>:
|
130
|
+
|
131
|
+
MultiDb::ConnectionProxy.sticky_slave = true
|
132
|
+
|
133
|
+
And add this to your ApplicationController:
|
134
|
+
|
135
|
+
after_filter { ActiveRecord::Base.connection_proxy.next_reader! }
|
136
|
+
|
137
|
+
*NOTE*: It's not possible to toggle this mode in a running process, as the dynamically
|
138
|
+
generated methods will have the initially defined "stickyness" built in.
|
139
|
+
|
140
|
+
=== Usage outside of Rails
|
141
|
+
|
142
|
+
You can use multi_db together with other framworks or in standalone scripts.
|
143
|
+
Example:
|
144
|
+
|
145
|
+
require 'rubygems'
|
146
|
+
require 'active_record'
|
147
|
+
require 'multi_db'
|
148
|
+
|
149
|
+
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
150
|
+
ActiveRecord::Base.configurations = {
|
151
|
+
'development' => {
|
152
|
+
'adapter' => 'mysql',
|
153
|
+
'host' => 'localhost',
|
154
|
+
'username' => 'root',
|
155
|
+
'database' => 'multi_db_test'
|
156
|
+
},
|
157
|
+
'development_slave_database' => {
|
158
|
+
'adapter' => 'mysql',
|
159
|
+
'host' => 'localhost',
|
160
|
+
'username' => 'root',
|
161
|
+
'database' => 'multi_db_test'
|
162
|
+
}
|
163
|
+
}
|
164
|
+
ActiveRecord::Base.establish_connection :development
|
165
|
+
MultiDb::ConnectionProxy.setup!
|
166
|
+
|
167
|
+
class MyModel < ActiveRecord::Base
|
168
|
+
# ...
|
169
|
+
end
|
170
|
+
|
171
|
+
# ...
|
172
|
+
|
173
|
+
Note that the configurations hash should contain strings as keys instead of symbols.
|
174
|
+
|
175
|
+
=== Differences to "masochism":
|
176
|
+
|
177
|
+
* Supports multiple slave databases (round robin)
|
178
|
+
* It sends everything except "select ..." queries to the master, instead of
|
179
|
+
sending only specific things to the master and anything "else" to the slave.
|
180
|
+
This avoids accidential writes to the master when there are API changes in
|
181
|
+
ActiveRecord which haven't been picked up by multi_db yet.
|
182
|
+
Note that this behaviour will also always send helper methods like "+quote+" or
|
183
|
+
"<tt>add_limit!</tt>" to the master connection object, which doesn't add any
|
184
|
+
more load on the master, as these methods don't communicate with the db server
|
185
|
+
itself.
|
186
|
+
* It uses its own query cache as the slave's cache isn't emptied when there are
|
187
|
+
changes on the master
|
188
|
+
* It supports immediate failover for slave connections
|
189
|
+
* It will wait some time before trying to query a failed slave database again
|
190
|
+
* It supports nesting "with_master"-blocks, without unexpectedly switching you
|
191
|
+
back to the slave again
|
192
|
+
* It schedules a reconnect of the master connection if statements fail there.
|
193
|
+
This might help with HA setups using virtual IPs (a test setup would be nice
|
194
|
+
to verify this)
|
195
|
+
* You specify slave databases in the configuration instead of specifying an extra
|
196
|
+
master database. This makes disabling or removing multi_db less dangerous
|
197
|
+
(Update: Recent versions of masochism support this, too).
|
198
|
+
* There are no <tt>set_to_master!</tt> and <tt>set_to_slave!</tt> methods, just
|
199
|
+
<tt>with_master(&block)</tt>
|
200
|
+
* All proxied methods are dynamically generated for better performance
|
201
|
+
|
202
|
+
=== See also
|
203
|
+
|
204
|
+
==== Masochism
|
205
|
+
|
206
|
+
The original plugin:
|
207
|
+
|
208
|
+
* http://github.com/technoweenie/masochism
|
209
|
+
|
210
|
+
==== DataFabric
|
211
|
+
|
212
|
+
A solution by FiveRuns, also based on masochism but without the "nested with_master"-issue,
|
213
|
+
threadsafe and allows sharding of data.
|
214
|
+
|
215
|
+
* http://github.com/fiveruns/data_fabric
|
216
|
+
|
217
|
+
=== Contributors
|
218
|
+
|
219
|
+
* Matt Conway http://github.com/wr0ngway
|
220
|
+
* Matthias Marshall http://github.com/webops
|
221
|
+
|
222
|
+
=== Ideas
|
223
|
+
|
224
|
+
See: http://github.com/schoefmax/multi_db/wikis/home
|
225
|
+
|
226
|
+
=== Running specs
|
227
|
+
|
228
|
+
If you haven't already, install the rspec gem, then create an empty database
|
229
|
+
called "multi_db_test" (you might want to tweak the spec/config/database.yml).
|
230
|
+
From the plugin directory, run:
|
231
|
+
|
232
|
+
spec spec
|
233
|
+
|
234
|
+
|
235
|
+
Copyright (c) 2008, Max Schoefmann <max (a) pragmatic-it de>
|
236
|
+
Released under the MIT license
|
data/lib/multi_db.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
module MultiDb
|
2
|
+
module ActiveRecordExtensions
|
3
|
+
def self.included(base)
|
4
|
+
base.send :include, InstanceMethods
|
5
|
+
base.send :extend, ClassMethods
|
6
|
+
base.alias_method_chain :reload, :master
|
7
|
+
base.cattr_accessor :connection_proxy
|
8
|
+
# handle subclasses which were defined by the framework or plugins
|
9
|
+
base.send(:subclasses).each do |child|
|
10
|
+
child.hijack_connection
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module InstanceMethods
|
15
|
+
def reload_with_master(*args, &block)
|
16
|
+
self.connection_proxy.with_master { reload_without_master }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
# Make sure transactions always switch to the master
|
22
|
+
def transaction(&block)
|
23
|
+
if self.connection.kind_of?(ConnectionProxy)
|
24
|
+
super
|
25
|
+
else
|
26
|
+
self.connection_proxy.with_master { super }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# make caching always use the ConnectionProxy
|
31
|
+
def cache(&block)
|
32
|
+
if ActiveRecord::Base.configurations.blank?
|
33
|
+
yield
|
34
|
+
else
|
35
|
+
self.connection_proxy.cache(&block)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def inherited(child)
|
40
|
+
super
|
41
|
+
child.hijack_connection
|
42
|
+
end
|
43
|
+
|
44
|
+
def hijack_connection
|
45
|
+
return if ConnectionProxy.master_models.include?(self.to_s)
|
46
|
+
logger.info "[MULTIDB] hijacking connection for #{self.to_s}"
|
47
|
+
class << self
|
48
|
+
def connection
|
49
|
+
self.connection_proxy
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
module MultiDb
|
2
|
+
class ConnectionProxy
|
3
|
+
include QueryCacheCompat
|
4
|
+
include ActiveRecord::ConnectionAdapters::QueryCache
|
5
|
+
extend ThreadLocalAccessors
|
6
|
+
|
7
|
+
# Safe methods are those that should either go to the slave ONLY or go
|
8
|
+
# to the current active connection.
|
9
|
+
SAFE_METHODS = [ :select_all, :select_one, :select_value, :select_values,
|
10
|
+
:select_rows, :select, :verify!, :raw_connection, :active?, :reconnect!,
|
11
|
+
:disconnect!, :reset_runtime, :log, :log_info ]
|
12
|
+
|
13
|
+
if ActiveRecord.const_defined?(:SessionStore) # >= Rails 2.3
|
14
|
+
DEFAULT_MASTER_MODELS = ['ActiveRecord::SessionStore::Session']
|
15
|
+
else # =< Rails 2.3
|
16
|
+
DEFAULT_MASTER_MODELS = ['CGI::Session::ActiveRecordStore::Session']
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_accessor :master
|
20
|
+
tlattr_accessor :master_depth, :current, true
|
21
|
+
|
22
|
+
class << self
|
23
|
+
|
24
|
+
# defaults to RAILS_ENV if multi_db is used with Rails
|
25
|
+
# defaults to 'development' when used outside Rails
|
26
|
+
attr_accessor :environment
|
27
|
+
|
28
|
+
# a list of models that should always go directly to the master
|
29
|
+
#
|
30
|
+
# Example:
|
31
|
+
#
|
32
|
+
# MultiDb::ConnectionProxy.master_models = ['MySessionStore', 'PaymentTransaction']
|
33
|
+
attr_accessor :master_models
|
34
|
+
|
35
|
+
# decides if we should switch to the next reader automatically.
|
36
|
+
# If set to false, an after|before_filter in the ApplicationController
|
37
|
+
# has to do this.
|
38
|
+
# This will not affect failover if a master is unavailable.
|
39
|
+
attr_accessor :sticky_slave
|
40
|
+
|
41
|
+
# Replaces the connection of ActiveRecord::Base with a proxy and
|
42
|
+
# establishes the connections to the slaves.
|
43
|
+
def setup!
|
44
|
+
self.master_models ||= DEFAULT_MASTER_MODELS
|
45
|
+
self.environment ||= (defined?(RAILS_ENV) ? RAILS_ENV : 'development')
|
46
|
+
self.sticky_slave ||= false
|
47
|
+
|
48
|
+
master = ActiveRecord::Base
|
49
|
+
slaves = init_slaves
|
50
|
+
raise "No slaves databases defined for environment: #{self.environment}" if slaves.empty?
|
51
|
+
master.send :include, MultiDb::ActiveRecordExtensions
|
52
|
+
ActiveRecord::Observer.send :include, MultiDb::ObserverExtensions
|
53
|
+
master.connection_proxy = new(master, slaves)
|
54
|
+
master.logger.info("** multi_db with master and #{slaves.length} slave#{"s" if slaves.length > 1} loaded.")
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
# Slave entries in the database.yml must be named like this
|
60
|
+
# development_slave_database:
|
61
|
+
# or
|
62
|
+
# development_slave_database1:
|
63
|
+
# or
|
64
|
+
# production_slave_database_someserver:
|
65
|
+
# These would be available later as MultiDb::SlaveDatabaseSomeserver
|
66
|
+
def init_slaves
|
67
|
+
returning([]) do |slaves|
|
68
|
+
ActiveRecord::Base.configurations.keys.each do |name|
|
69
|
+
if name.to_s =~ /#{self.environment}_(slave_database.*)/
|
70
|
+
MultiDb.module_eval %Q{
|
71
|
+
class #{$1.camelize} < ActiveRecord::Base
|
72
|
+
self.abstract_class = true
|
73
|
+
establish_connection :#{name}
|
74
|
+
end
|
75
|
+
}, __FILE__, __LINE__
|
76
|
+
slaves << "MultiDb::#{$1.camelize}".constantize
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private :new
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
def initialize(master, slaves)
|
87
|
+
@slaves = Scheduler.new(slaves)
|
88
|
+
@master = master
|
89
|
+
@reconnect = false
|
90
|
+
self.current = @slaves.current
|
91
|
+
self.master_depth = 0
|
92
|
+
end
|
93
|
+
|
94
|
+
def slave
|
95
|
+
@slaves.current
|
96
|
+
end
|
97
|
+
|
98
|
+
def with_master
|
99
|
+
self.current = @master
|
100
|
+
self.master_depth += 1
|
101
|
+
yield
|
102
|
+
ensure
|
103
|
+
self.master_depth -= 1
|
104
|
+
self.current = slave if master_depth.zero?
|
105
|
+
end
|
106
|
+
|
107
|
+
def transaction(start_db_transaction = true, &block)
|
108
|
+
with_master { @master.retrieve_connection.transaction(start_db_transaction, &block) }
|
109
|
+
end
|
110
|
+
|
111
|
+
# Calls the method on master/slave and dynamically creates a new
|
112
|
+
# method on success to speed up subsequent calls
|
113
|
+
def method_missing(method, *args, &block)
|
114
|
+
returning(send(target_method(method), method, *args, &block)) do
|
115
|
+
create_delegation_method!(method)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Switches to the next slave database for read operations.
|
120
|
+
# Fails over to the master database if all slaves are unavailable.
|
121
|
+
def next_reader!
|
122
|
+
return unless master_depth.zero? # don't if in with_master block
|
123
|
+
self.current = @slaves.next
|
124
|
+
rescue Scheduler::NoMoreItems
|
125
|
+
logger.warn "[MULTIDB] All slaves are blacklisted. Reading from master"
|
126
|
+
self.current = @master
|
127
|
+
end
|
128
|
+
|
129
|
+
protected
|
130
|
+
|
131
|
+
def create_delegation_method!(method)
|
132
|
+
self.instance_eval %Q{
|
133
|
+
def #{method}(*args, &block)
|
134
|
+
#{'next_reader!' unless self.class.sticky_slave || unsafe?(method)}
|
135
|
+
#{target_method(method)}(:#{method}, *args, &block)
|
136
|
+
end
|
137
|
+
}, __FILE__, __LINE__
|
138
|
+
end
|
139
|
+
|
140
|
+
def target_method(method)
|
141
|
+
unsafe?(method) ? :send_to_master : :send_to_current
|
142
|
+
end
|
143
|
+
|
144
|
+
def send_to_master(method, *args, &block)
|
145
|
+
reconnect_master! if @reconnect
|
146
|
+
@master.retrieve_connection.send(method, *args, &block)
|
147
|
+
rescue => e
|
148
|
+
raise_master_error(e)
|
149
|
+
end
|
150
|
+
|
151
|
+
def send_to_current(method, *args, &block)
|
152
|
+
reconnect_master! if @reconnect && master?
|
153
|
+
current.retrieve_connection.send(method, *args, &block)
|
154
|
+
rescue NotImplementedError, NoMethodError
|
155
|
+
raise
|
156
|
+
rescue => e # TODO don't rescue everything
|
157
|
+
raise_master_error(e) if master?
|
158
|
+
logger.warn "[MULTIDB] Error reading from slave database"
|
159
|
+
logger.error %(#{e.message}\n#{e.backtrace.join("\n")})
|
160
|
+
@slaves.blacklist!(current)
|
161
|
+
next_reader!
|
162
|
+
retry
|
163
|
+
end
|
164
|
+
|
165
|
+
def reconnect_master!
|
166
|
+
@master.retrieve_connection.reconnect!
|
167
|
+
@reconnect = false
|
168
|
+
end
|
169
|
+
|
170
|
+
def raise_master_error(error)
|
171
|
+
logger.fatal "[MULTIDB] Error accessing master database. Scheduling reconnect"
|
172
|
+
@reconnect = true
|
173
|
+
raise error
|
174
|
+
end
|
175
|
+
|
176
|
+
def unsafe?(method)
|
177
|
+
!SAFE_METHODS.include?(method)
|
178
|
+
end
|
179
|
+
|
180
|
+
def master?
|
181
|
+
current == @master
|
182
|
+
end
|
183
|
+
|
184
|
+
def logger
|
185
|
+
ActiveRecord::Base.logger
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module MultiDb
|
2
|
+
module ObserverExtensions
|
3
|
+
def self.included(base)
|
4
|
+
base.alias_method_chain :update, :masterdb
|
5
|
+
end
|
6
|
+
|
7
|
+
# Send observed_method(object) if the method exists.
|
8
|
+
def update_with_masterdb(observed_method, object) #:nodoc:
|
9
|
+
if object.class.connection.respond_to?(:with_master)
|
10
|
+
object.class.connection.with_master do
|
11
|
+
update_without_masterdb(observed_method, object)
|
12
|
+
end
|
13
|
+
else
|
14
|
+
update_without_masterdb(observed_method, object)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module MultiDb
|
2
|
+
# Implements the methods expected by the QueryCache module
|
3
|
+
module QueryCacheCompat
|
4
|
+
def select_all(*a, &b)
|
5
|
+
next_reader! unless ConnectionProxy.sticky_slave
|
6
|
+
send_to_current(:select_all, *a, &b)
|
7
|
+
end
|
8
|
+
def columns(*a, &b)
|
9
|
+
send_to_current(:columns, *a, &b)
|
10
|
+
end
|
11
|
+
def insert(*a, &b)
|
12
|
+
send_to_master(:insert, *a, &b)
|
13
|
+
end
|
14
|
+
def update(*a, &b)
|
15
|
+
send_to_master(:update, *a, &b)
|
16
|
+
end
|
17
|
+
def delete(*a, &b)
|
18
|
+
send_to_master(:delete, *a, &b)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module MultiDb
|
2
|
+
class Scheduler
|
3
|
+
class NoMoreItems < Exception; end
|
4
|
+
extend ThreadLocalAccessors
|
5
|
+
|
6
|
+
attr :items
|
7
|
+
delegate :[], :[]=, :to => :items
|
8
|
+
tlattr_accessor :current_index, true
|
9
|
+
|
10
|
+
def initialize(items, blacklist_timeout = 1.minute)
|
11
|
+
@n = items.length
|
12
|
+
@items = items
|
13
|
+
@blacklist = Array.new(@n, Time.at(0))
|
14
|
+
@blacklist_timeout = blacklist_timeout
|
15
|
+
self.current_index = 0
|
16
|
+
end
|
17
|
+
|
18
|
+
def blacklist!(item)
|
19
|
+
@blacklist[@items.index(item)] = Time.now
|
20
|
+
end
|
21
|
+
|
22
|
+
def current
|
23
|
+
@items[current_index]
|
24
|
+
end
|
25
|
+
|
26
|
+
def next
|
27
|
+
previous = current_index
|
28
|
+
until(@blacklist[next_index!] < Time.now - @blacklist_timeout) do
|
29
|
+
raise NoMoreItems, 'All items are blacklisted' if current_index == previous
|
30
|
+
end
|
31
|
+
current
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
def next_index!
|
37
|
+
self.current_index = (current_index + 1) % @n
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
data/multi_db.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{multi_db}
|
5
|
+
s.version = "0.2.1"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Maximilian Sch\303\266fmann"]
|
9
|
+
s.date = %q{2009-03-11}
|
10
|
+
s.description = "Connection proxy for ActiveRecord for single master / multiple slave database deployments"
|
11
|
+
s.email = "max@pragmatic-it.de"
|
12
|
+
s.extra_rdoc_files = ["LICENSE", "README.rdoc"]
|
13
|
+
s.files = ["lib/multi_db.rb", "lib/multi_db/active_record_extensions.rb", "lib/multi_db/connection_proxy.rb", "lib/multi_db/observer_extensions.rb", "lib/multi_db/query_cache_compat.rb", "lib/multi_db/scheduler.rb", "LICENSE", "README.rdoc", "spec/config/database.yml", "spec/connection_proxy_spec.rb", "spec/scheduler_spec.rb", "spec/spec_helper.rb", "multi_db.gemspec"]
|
14
|
+
s.has_rdoc = true
|
15
|
+
s.homepage = "http://github.com/schoefmax/multi_db"
|
16
|
+
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "multi_db", "--main", "README.rdoc"]
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
s.rubyforge_project = "multi_db"
|
19
|
+
s.rubygems_version = %q{1.3.1}
|
20
|
+
s.summary = "Connection proxy for ActiveRecord for single master / multiple slave database deployments"
|
21
|
+
|
22
|
+
if s.respond_to? :specification_version then
|
23
|
+
s.specification_version = 2
|
24
|
+
|
25
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
26
|
+
s.add_runtime_dependency('activerecord', [">= 2.1.0"])
|
27
|
+
s.add_runtime_dependency('tlattr_accessors', [">= 0.0.3"])
|
28
|
+
else
|
29
|
+
s.add_dependency('activerecord', [">= 2.1.0"])
|
30
|
+
s.add_dependency('tlattr_accessors', [">= 0.0.3"])
|
31
|
+
end
|
32
|
+
else
|
33
|
+
s.add_dependency('activerecord', [">= 2.1.0"])
|
34
|
+
s.add_dependency('tlattr_accessors', [">= 0.0.3"])
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
test:
|
2
|
+
adapter: mysql
|
3
|
+
database: multi_db_test
|
4
|
+
username: root
|
5
|
+
password:
|
6
|
+
host: 127.0.0.1
|
7
|
+
pool: 5
|
8
|
+
|
9
|
+
test_slave_database_1:
|
10
|
+
adapter: mysql
|
11
|
+
database: multi_db_test
|
12
|
+
username: root
|
13
|
+
password:
|
14
|
+
host: 127.0.0.1
|
15
|
+
pool: 5
|
16
|
+
|
17
|
+
test_slave_database_2:
|
18
|
+
adapter: mysql
|
19
|
+
database: multi_db_test
|
20
|
+
username: root
|
21
|
+
password:
|
22
|
+
host: 127.0.0.1
|
23
|
+
pool: 5
|
@@ -0,0 +1,229 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
require MULTI_DB_SPEC_DIR + '/../lib/multi_db/query_cache_compat'
|
3
|
+
require MULTI_DB_SPEC_DIR + '/../lib/multi_db/active_record_extensions'
|
4
|
+
require MULTI_DB_SPEC_DIR + '/../lib/multi_db/observer_extensions'
|
5
|
+
require MULTI_DB_SPEC_DIR + '/../lib/multi_db/scheduler'
|
6
|
+
require MULTI_DB_SPEC_DIR + '/../lib/multi_db/connection_proxy'
|
7
|
+
|
8
|
+
RAILS_ROOT = MULTI_DB_SPEC_DIR
|
9
|
+
|
10
|
+
describe MultiDb::ConnectionProxy do
|
11
|
+
|
12
|
+
before(:all) do
|
13
|
+
ActiveRecord::Base.configurations = MULTI_DB_SPEC_CONFIG
|
14
|
+
ActiveRecord::Base.establish_connection :test
|
15
|
+
ActiveRecord::Migration.verbose = false
|
16
|
+
ActiveRecord::Migration.create_table(:master_models, :force => true) {}
|
17
|
+
class MasterModel < ActiveRecord::Base; end
|
18
|
+
ActiveRecord::Migration.create_table(:foo_models, :force => true) {|t| t.string :bar}
|
19
|
+
class FooModel < ActiveRecord::Base; end
|
20
|
+
@sql = 'SELECT 1 + 1 FROM DUAL'
|
21
|
+
end
|
22
|
+
|
23
|
+
before(:each) do
|
24
|
+
MultiDb::ConnectionProxy.master_models = ['MasterModel']
|
25
|
+
MultiDb::ConnectionProxy.setup!
|
26
|
+
@proxy = ActiveRecord::Base.connection_proxy
|
27
|
+
@master = @proxy.master.retrieve_connection
|
28
|
+
@slave1 = MultiDb::SlaveDatabase1.retrieve_connection
|
29
|
+
@slave2 = MultiDb::SlaveDatabase2.retrieve_connection
|
30
|
+
end
|
31
|
+
|
32
|
+
after(:each) do
|
33
|
+
ActiveRecord::Base.send :alias_method, :reload, :reload_without_master
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'AR::B should respond to #connection_proxy' do
|
37
|
+
ActiveRecord::Base.connection_proxy.should be_kind_of(MultiDb::ConnectionProxy)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'FooModel#connection should return an instance of MultiDb::ConnectionProxy' do
|
41
|
+
FooModel.connection.should be_kind_of(MultiDb::ConnectionProxy)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'MasterModel#connection should not return an instance of MultiDb::ConnectionProxy' do
|
45
|
+
MasterModel.connection.should_not be_kind_of(MultiDb::ConnectionProxy)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should generate classes for each entry in the database.yml" do
|
49
|
+
defined?(MultiDb::SlaveDatabase1).should_not be_nil
|
50
|
+
defined?(MultiDb::SlaveDatabase2).should_not be_nil
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should handle nested with_master-blocks correctly' do
|
54
|
+
@proxy.current.should_not == @proxy.master
|
55
|
+
@proxy.with_master do
|
56
|
+
@proxy.current.should == @proxy.master
|
57
|
+
@proxy.with_master do
|
58
|
+
@proxy.current.should == @proxy.master
|
59
|
+
@proxy.with_master do
|
60
|
+
@proxy.current.should == @proxy.master
|
61
|
+
end
|
62
|
+
@proxy.current.should == @proxy.master
|
63
|
+
end
|
64
|
+
@proxy.current.should == @proxy.master
|
65
|
+
end
|
66
|
+
@proxy.current.should_not == @proxy.master
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'should perform transactions on the master' do
|
70
|
+
@master.should_receive(:select_all).exactly(1) # makes sure the first one goes to a slave
|
71
|
+
@proxy.select_all(@sql)
|
72
|
+
ActiveRecord::Base.transaction do
|
73
|
+
@proxy.select_all(@sql)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'should switch to the next reader on selects' do
|
78
|
+
@slave1.should_receive(:select_one).exactly(2)
|
79
|
+
@slave2.should_receive(:select_one).exactly(2)
|
80
|
+
4.times { @proxy.select_one(@sql) }
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'should not switch to the next reader when whithin a with_master-block' do
|
84
|
+
@master.should_receive(:select_one).twice
|
85
|
+
@slave1.should_not_receive(:select_one)
|
86
|
+
@slave2.should_not_receive(:select_one)
|
87
|
+
@proxy.with_master do
|
88
|
+
2.times { @proxy.select_one(@sql) }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'should send dangerous methods to the master' do
|
93
|
+
meths = [:insert, :update, :delete, :execute]
|
94
|
+
meths.each do |meth|
|
95
|
+
@slave1.stub!(meth).and_raise(RuntimeError)
|
96
|
+
@master.should_receive(meth).and_return(true)
|
97
|
+
@proxy.send(meth, @sql)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'should dynamically generate safe methods' do
|
102
|
+
@proxy.should_not respond_to(:select_value)
|
103
|
+
@proxy.select_value(@sql)
|
104
|
+
@proxy.should respond_to(:select_value)
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'should cache queries using select_all' do
|
108
|
+
ActiveRecord::Base.cache do
|
109
|
+
# next_reader will be called and switch to the SlaveDatabase2
|
110
|
+
@slave2.should_receive(:select_all).exactly(1)
|
111
|
+
@slave1.should_not_receive(:select_all)
|
112
|
+
@master.should_not_receive(:select_all)
|
113
|
+
3.times { @proxy.select_all(@sql) }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'should invalidate the cache on insert, delete and update' do
|
118
|
+
ActiveRecord::Base.cache do
|
119
|
+
meths = [:insert, :update, :delete]
|
120
|
+
meths.each do |meth|
|
121
|
+
@master.should_receive(meth).and_return(true)
|
122
|
+
end
|
123
|
+
@slave2.should_receive(:select_all).twice
|
124
|
+
@slave1.should_receive(:select_all).once
|
125
|
+
3.times do |i|
|
126
|
+
@proxy.select_all(@sql)
|
127
|
+
@proxy.send(meths[i])
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'should retry the next slave when one fails and finally fall back to the master' do
|
133
|
+
@slave1.should_receive(:select_all).once.and_raise(RuntimeError)
|
134
|
+
@slave2.should_receive(:select_all).once.and_raise(RuntimeError)
|
135
|
+
@master.should_receive(:select_all).and_return(true)
|
136
|
+
@proxy.select_all(@sql)
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'should try to reconnect the master connection after the master has failed' do
|
140
|
+
@master.should_receive(:update).and_raise(RuntimeError)
|
141
|
+
lambda { @proxy.update(@sql) }.should raise_error
|
142
|
+
@master.should_receive(:reconnect!).and_return(true)
|
143
|
+
@master.should_receive(:insert).and_return(1)
|
144
|
+
@proxy.insert(@sql)
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'should reload models from the master' do
|
148
|
+
foo = FooModel.create!(:bar => 'baz')
|
149
|
+
foo.bar = "not_saved"
|
150
|
+
@slave1.should_not_receive(:select_all)
|
151
|
+
@slave2.should_not_receive(:select_all)
|
152
|
+
foo.reload
|
153
|
+
# we didn't stub @master#select_all here, check that we actually hit the db
|
154
|
+
foo.bar.should == 'baz'
|
155
|
+
end
|
156
|
+
|
157
|
+
describe 'with sticky_slave ' do
|
158
|
+
|
159
|
+
before { MultiDb::ConnectionProxy.sticky_slave = true }
|
160
|
+
after { MultiDb::ConnectionProxy.sticky_slave = false }
|
161
|
+
|
162
|
+
it 'should not switch to the next reader automatically' do
|
163
|
+
@slave1.should_receive(:select_all).exactly(3)
|
164
|
+
@slave2.should_receive(:select_all).exactly(0)
|
165
|
+
3.times { @proxy.select_all(@sql) }
|
166
|
+
end
|
167
|
+
|
168
|
+
it '#next_reader! should switch to the next slave' do
|
169
|
+
@slave1.should_receive(:select_one).exactly(3)
|
170
|
+
@slave2.should_receive(:select_one).exactly(7)
|
171
|
+
3.times { @proxy.select_one(@sql) }
|
172
|
+
@proxy.next_reader!
|
173
|
+
7.times { @proxy.select_one(@sql) }
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
describe '(accessed from multiple threads)' do
|
179
|
+
# NOTE: We cannot put expectations on the connection objects itself
|
180
|
+
# for the threading specs, as connection pooling will cause
|
181
|
+
# different connections being returned for different threads.
|
182
|
+
|
183
|
+
it '#current and #next_reader! should be local to the thread' do
|
184
|
+
@proxy.current.should == MultiDb::SlaveDatabase1
|
185
|
+
@proxy.next_reader!.should == MultiDb::SlaveDatabase2
|
186
|
+
Thread.new do
|
187
|
+
@proxy.current.should == MultiDb::SlaveDatabase1
|
188
|
+
@proxy.next_reader!.should == MultiDb::SlaveDatabase2
|
189
|
+
@proxy.current.should == MultiDb::SlaveDatabase2
|
190
|
+
@proxy.next_reader!.should == MultiDb::SlaveDatabase1
|
191
|
+
@proxy.current.should == MultiDb::SlaveDatabase1
|
192
|
+
end
|
193
|
+
@proxy.current.should == MultiDb::SlaveDatabase2
|
194
|
+
end
|
195
|
+
|
196
|
+
it '#with_master should be local to the thread' do
|
197
|
+
@proxy.current.should_not == @proxy.master
|
198
|
+
@proxy.with_master do
|
199
|
+
@proxy.current.should == @proxy.master
|
200
|
+
Thread.new do
|
201
|
+
@proxy.current.should_not == @proxy.master
|
202
|
+
@proxy.with_master do
|
203
|
+
@proxy.current.should == @proxy.master
|
204
|
+
end
|
205
|
+
@proxy.current.should_not == @proxy.master
|
206
|
+
end
|
207
|
+
@proxy.current.should == @proxy.master
|
208
|
+
end
|
209
|
+
@proxy.current.should_not == @proxy.master
|
210
|
+
end
|
211
|
+
|
212
|
+
it 'should switch to the next reader even whithin with_master-block in different threads' do
|
213
|
+
# Because of connection pooling in AR 2.2, the second thread will cause
|
214
|
+
# a new connection being created behind the scenes. We therefore just test
|
215
|
+
# that these connections are beting retrieved for the right databases here.
|
216
|
+
@proxy.master.should_not_receive(:retrieve_connection).and_return(@master)
|
217
|
+
MultiDb::SlaveDatabase1.should_receive(:retrieve_connection).twice.and_return(@slave1)
|
218
|
+
MultiDb::SlaveDatabase2.should_receive(:retrieve_connection).once.and_return(@slave2)
|
219
|
+
@proxy.with_master do
|
220
|
+
Thread.new do
|
221
|
+
3.times { @proxy.select_one(@sql) }
|
222
|
+
end.join
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
end
|
227
|
+
|
228
|
+
end
|
229
|
+
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
require MULTI_DB_SPEC_DIR + '/../lib/multi_db/scheduler'
|
3
|
+
|
4
|
+
describe MultiDb::Scheduler do
|
5
|
+
|
6
|
+
before do
|
7
|
+
@items = [5, 7, 4, 8]
|
8
|
+
@scheduler = MultiDb::Scheduler.new(@items.clone)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should return items in a round robin fashion" do
|
12
|
+
first = @items.shift
|
13
|
+
@scheduler.current.should == first
|
14
|
+
@items.each do |item|
|
15
|
+
@scheduler.next.should == item
|
16
|
+
end
|
17
|
+
@scheduler.next.should == first
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should not return blacklisted items' do
|
21
|
+
@scheduler.blacklist!(4)
|
22
|
+
@items.size.times do
|
23
|
+
@scheduler.next.should_not == 4
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should raise NoMoreItems if all are blacklisted' do
|
28
|
+
@items.each do |item|
|
29
|
+
@scheduler.blacklist!(item)
|
30
|
+
end
|
31
|
+
lambda {
|
32
|
+
@scheduler.next
|
33
|
+
}.should raise_error(MultiDb::Scheduler::NoMoreItems)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should unblacklist items automatically' do
|
37
|
+
@scheduler = MultiDb::Scheduler.new(@items.clone, 1.second)
|
38
|
+
@scheduler.blacklist!(7)
|
39
|
+
sleep(1)
|
40
|
+
@scheduler.next.should == 7
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '(accessed from multiple threads)' do
|
44
|
+
|
45
|
+
it '#current and #next should return the same item for the same thread' do
|
46
|
+
@scheduler.current.should == 5
|
47
|
+
@scheduler.next.should == 7
|
48
|
+
Thread.new do
|
49
|
+
@scheduler.current.should == 5
|
50
|
+
@scheduler.next.should == 7
|
51
|
+
end.join
|
52
|
+
@scheduler.next.should == 4
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'activerecord', '2.2.2'
|
3
|
+
%w[tlattr_accessors active_record yaml erb spec].each {|lib| require lib}
|
4
|
+
|
5
|
+
RAILS_ENV = ENV['RAILS_ENV'] = 'test'
|
6
|
+
|
7
|
+
MULTI_DB_SPEC_DIR = File.dirname(__FILE__)
|
8
|
+
MULTI_DB_SPEC_CONFIG = YAML::load(File.open(MULTI_DB_SPEC_DIR + '/config/database.yml'))
|
9
|
+
|
10
|
+
ActiveRecord::Base.logger = Logger.new(MULTI_DB_SPEC_DIR + "/debug.log")
|
11
|
+
ActiveRecord::Base.configurations = MULTI_DB_SPEC_CONFIG
|
metadata
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: multi_db
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- "Maximilian Sch\xC3\xB6fmann"
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-03-11 00:00:00 +01:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: activerecord
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 2.1.0
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: tlattr_accessors
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.0.3
|
34
|
+
version:
|
35
|
+
description: Connection proxy for ActiveRecord for single master / multiple slave database deployments
|
36
|
+
email: max@pragmatic-it.de
|
37
|
+
executables: []
|
38
|
+
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files:
|
42
|
+
- LICENSE
|
43
|
+
- README.rdoc
|
44
|
+
files:
|
45
|
+
- lib/multi_db.rb
|
46
|
+
- lib/multi_db/active_record_extensions.rb
|
47
|
+
- lib/multi_db/connection_proxy.rb
|
48
|
+
- lib/multi_db/observer_extensions.rb
|
49
|
+
- lib/multi_db/query_cache_compat.rb
|
50
|
+
- lib/multi_db/scheduler.rb
|
51
|
+
- LICENSE
|
52
|
+
- README.rdoc
|
53
|
+
- spec/config/database.yml
|
54
|
+
- spec/connection_proxy_spec.rb
|
55
|
+
- spec/scheduler_spec.rb
|
56
|
+
- spec/spec_helper.rb
|
57
|
+
- multi_db.gemspec
|
58
|
+
has_rdoc: true
|
59
|
+
homepage: http://github.com/schoefmax/multi_db
|
60
|
+
licenses: []
|
61
|
+
|
62
|
+
post_install_message:
|
63
|
+
rdoc_options:
|
64
|
+
- --line-numbers
|
65
|
+
- --inline-source
|
66
|
+
- --title
|
67
|
+
- multi_db
|
68
|
+
- --main
|
69
|
+
- README.rdoc
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: "0"
|
77
|
+
version:
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: "1.2"
|
83
|
+
version:
|
84
|
+
requirements: []
|
85
|
+
|
86
|
+
rubyforge_project: multi_db
|
87
|
+
rubygems_version: 1.3.5
|
88
|
+
signing_key:
|
89
|
+
specification_version: 2
|
90
|
+
summary: Connection proxy for ActiveRecord for single master / multiple slave database deployments
|
91
|
+
test_files: []
|
92
|
+
|