glebpom-multi_db 0.2.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.
- data/LICENSE +20 -0
- data/README.rdoc +238 -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 +186 -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,238 @@
|
|
1
|
+
= multi_db
|
2
|
+
|
3
|
+
IMPORANT: version with some hacks for compatibility with ultrasphinx. Used in qik.com
|
4
|
+
|
5
|
+
=====-- This GEM was inspired by Rick Olson's "masochism"-Plugin
|
6
|
+
|
7
|
+
multi_db uses a connection proxy, which sends read queries to slave databases,
|
8
|
+
and all write queries to the master database (Read/Write Split).
|
9
|
+
Within transactions, while executing ActiveRecord Observers and
|
10
|
+
within "with_master" blocks (see below), even read queries are sent to the
|
11
|
+
master database.
|
12
|
+
|
13
|
+
=== Important changes in 0.2.0
|
14
|
+
|
15
|
+
* As of this version, <tt>ActiveRecord::Base.connection</tt> does not return the
|
16
|
+
connection proxy by default anymore (therefore the jump to 0.2.0). Only models
|
17
|
+
inheriting from AR::B return the proxy, unless they are defined as master_models
|
18
|
+
(see below). If you want to access the connection proxy from AR::B directly,
|
19
|
+
use <tt>ActiveRecord::Base.connection_proxy</tt>.
|
20
|
+
|
21
|
+
* This version is the first attempt for thread-safety of this gem. <em>There might
|
22
|
+
still be some threading issues left!</em>. So please test your apps thoroughly
|
23
|
+
and report any issues you might encounter.
|
24
|
+
|
25
|
+
* <tt>CGI::Session::ActiveRecordStore::Session</tt> is now automatically registered
|
26
|
+
as a master model.
|
27
|
+
|
28
|
+
=== Caveats
|
29
|
+
|
30
|
+
* works only with activerecord 2.1, 2.2 and 2.3
|
31
|
+
|
32
|
+
=== Install
|
33
|
+
|
34
|
+
gem sources --add http://gems.github.com # only if you haven't already added github
|
35
|
+
gem install glebpom-multi_db
|
36
|
+
|
37
|
+
When using Rails, add this to your environment.rb:
|
38
|
+
|
39
|
+
config.gem 'glebpom-multi_db', :lib => 'multi_db', :source => 'http://gems.github.com'
|
40
|
+
|
41
|
+
=== Setup
|
42
|
+
|
43
|
+
In your database.yml, add sections for the slaves, e.g.:
|
44
|
+
|
45
|
+
production: # that would be the master
|
46
|
+
adapter: mysql
|
47
|
+
database: myapp_production
|
48
|
+
username: root
|
49
|
+
password:
|
50
|
+
host: localhost
|
51
|
+
|
52
|
+
production_slave_database: # that would be a slave
|
53
|
+
adapter: mysql
|
54
|
+
database: myapp_production
|
55
|
+
username: root
|
56
|
+
password:
|
57
|
+
host: 10.0.0.2
|
58
|
+
|
59
|
+
production_slave_database_2: # another slave
|
60
|
+
...
|
61
|
+
production_slave_database_in_india: # yet another one
|
62
|
+
...
|
63
|
+
|
64
|
+
*NOTE*: multi_db identifies slave databases by looking for entries of the form
|
65
|
+
"<tt><environment>_slave_database<_optional_name></tt>". As a (useless) side effect you
|
66
|
+
get abstract classes named <tt>MultiDb::SlaveDatabaseInIndia</tt> etc.
|
67
|
+
The advantage of specifying the slaves explicitly, instead of the master, is that
|
68
|
+
you can use the same configuration file for scripts that don't use multi_db.
|
69
|
+
Also, when you decide to disable multi_db for some reason, you don't have to
|
70
|
+
swap hosts in your <tt>database.yml</tt> from master to slave (which is easy to forget...).
|
71
|
+
|
72
|
+
To enable the proxy globally, add this to your environment.rb, or some file in
|
73
|
+
config/initializers:
|
74
|
+
|
75
|
+
MultiDb::ConnectionProxy.setup!
|
76
|
+
|
77
|
+
If you only want to enable it for specific environments, add this to
|
78
|
+
the corresponding file in config/environments:
|
79
|
+
|
80
|
+
config.after_initialize do
|
81
|
+
MultiDb::ConnectionProxy.setup!
|
82
|
+
end
|
83
|
+
|
84
|
+
In the development and test environments, you can use identical configurations
|
85
|
+
for master and slave connections. This can help you finding (some of the) issues
|
86
|
+
your application might have with a replicated database setup without actually having
|
87
|
+
one on your development machine.
|
88
|
+
|
89
|
+
=== Using with Phusion Passenger
|
90
|
+
|
91
|
+
With Passengers smart spawning method, child processes forked by the ApplicationSpawner
|
92
|
+
won't have the connection proxy set up properly.
|
93
|
+
|
94
|
+
To make it work, add this to your <tt>environment.rb</tt> or an initializer script
|
95
|
+
(e.g. <tt>config/initializers/connection_proxy.rb</tt>):
|
96
|
+
|
97
|
+
if defined?(PhusionPassenger)
|
98
|
+
PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
99
|
+
if forked
|
100
|
+
# ... set MultiDb configuration options, if any ...
|
101
|
+
MultiDb::ConnectionProxy.setup!
|
102
|
+
end
|
103
|
+
end
|
104
|
+
else # not using passenger (e.g. development/testing)
|
105
|
+
# ... set MultiDb configuration options, if any ...
|
106
|
+
MultiDb::ConnectionProxy.setup!
|
107
|
+
end
|
108
|
+
|
109
|
+
Thanks to Nathan Esquenazi for testing this.
|
110
|
+
|
111
|
+
=== Forcing the master for certain actions
|
112
|
+
|
113
|
+
Just add this to your controller:
|
114
|
+
|
115
|
+
around_filter(:only => :foo_action) { |c,a| ActiveRecord::Base.connection_proxy.with_master { a.call } }
|
116
|
+
|
117
|
+
=== Forcing the master for certain models
|
118
|
+
|
119
|
+
In your environment.rb or an initializer, add this *before* the call to <tt>setup!</tt>:
|
120
|
+
|
121
|
+
MultiDb::ConnectionProxy.master_models = ['CGI::Session::ActiveRecordStore::Session', 'PaymentTransaction', ...]
|
122
|
+
MultiDb::ConnectionProxy.setup!
|
123
|
+
|
124
|
+
*NOTE*: You cannot safely add more master_models after calling <tt>setup!</tt>.
|
125
|
+
|
126
|
+
=== Making one slave database sticky during a request
|
127
|
+
|
128
|
+
This can be useful to leverage database level query caching as all queries will
|
129
|
+
be sent to the same slave database during one web request.
|
130
|
+
|
131
|
+
To enable, add this to your environment.rb just before <tt>MultiDb::ConnectionProxy.setup!</tt>:
|
132
|
+
|
133
|
+
MultiDb::ConnectionProxy.sticky_slave = true
|
134
|
+
|
135
|
+
And add this to your ApplicationController:
|
136
|
+
|
137
|
+
after_filter { ActiveRecord::Base.connection_proxy.next_reader! }
|
138
|
+
|
139
|
+
*NOTE*: It's not possible to toggle this mode in a running process, as the dynamically
|
140
|
+
generated methods will have the initially defined "stickyness" built in.
|
141
|
+
|
142
|
+
=== Usage outside of Rails
|
143
|
+
|
144
|
+
You can use multi_db together with other framworks or in standalone scripts.
|
145
|
+
Example:
|
146
|
+
|
147
|
+
require 'rubygems'
|
148
|
+
require 'active_record'
|
149
|
+
require 'multi_db'
|
150
|
+
|
151
|
+
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
152
|
+
ActiveRecord::Base.configurations = {
|
153
|
+
'development' => {
|
154
|
+
'adapter' => 'mysql',
|
155
|
+
'host' => 'localhost',
|
156
|
+
'username' => 'root',
|
157
|
+
'database' => 'multi_db_test'
|
158
|
+
},
|
159
|
+
'development_slave_database' => {
|
160
|
+
'adapter' => 'mysql',
|
161
|
+
'host' => 'localhost',
|
162
|
+
'username' => 'root',
|
163
|
+
'database' => 'multi_db_test'
|
164
|
+
}
|
165
|
+
}
|
166
|
+
ActiveRecord::Base.establish_connection :development
|
167
|
+
MultiDb::ConnectionProxy.setup!
|
168
|
+
|
169
|
+
class MyModel < ActiveRecord::Base
|
170
|
+
# ...
|
171
|
+
end
|
172
|
+
|
173
|
+
# ...
|
174
|
+
|
175
|
+
Note that the configurations hash should contain strings as keys instead of symbols.
|
176
|
+
|
177
|
+
=== Differences to "masochism":
|
178
|
+
|
179
|
+
* Supports multiple slave databases (round robin)
|
180
|
+
* It sends everything except "select ..." queries to the master, instead of
|
181
|
+
sending only specific things to the master and anything "else" to the slave.
|
182
|
+
This avoids accidential writes to the master when there are API changes in
|
183
|
+
ActiveRecord which haven't been picked up by multi_db yet.
|
184
|
+
Note that this behaviour will also always send helper methods like "+quote+" or
|
185
|
+
"<tt>add_limit!</tt>" to the master connection object, which doesn't add any
|
186
|
+
more load on the master, as these methods don't communicate with the db server
|
187
|
+
itself.
|
188
|
+
* It uses its own query cache as the slave's cache isn't emptied when there are
|
189
|
+
changes on the master
|
190
|
+
* It supports immediate failover for slave connections
|
191
|
+
* It will wait some time before trying to query a failed slave database again
|
192
|
+
* It supports nesting "with_master"-blocks, without unexpectedly switching you
|
193
|
+
back to the slave again
|
194
|
+
* It schedules a reconnect of the master connection if statements fail there.
|
195
|
+
This might help with HA setups using virtual IPs (a test setup would be nice
|
196
|
+
to verify this)
|
197
|
+
* You specify slave databases in the configuration instead of specifying an extra
|
198
|
+
master database. This makes disabling or removing multi_db less dangerous
|
199
|
+
(Update: Recent versions of masochism support this, too).
|
200
|
+
* There are no <tt>set_to_master!</tt> and <tt>set_to_slave!</tt> methods, just
|
201
|
+
<tt>with_master(&block)</tt>
|
202
|
+
* All proxied methods are dynamically generated for better performance
|
203
|
+
|
204
|
+
=== See also
|
205
|
+
|
206
|
+
==== Masochism
|
207
|
+
|
208
|
+
The original plugin:
|
209
|
+
|
210
|
+
* http://github.com/technoweenie/masochism
|
211
|
+
|
212
|
+
==== DataFabric
|
213
|
+
|
214
|
+
A solution by FiveRuns, also based on masochism but without the "nested with_master"-issue,
|
215
|
+
threadsafe and allows sharding of data.
|
216
|
+
|
217
|
+
* http://github.com/fiveruns/data_fabric
|
218
|
+
|
219
|
+
=== Contributors
|
220
|
+
|
221
|
+
* Matt Conway http://github.com/wr0ngway
|
222
|
+
* Matthias Marshall http://github.com/webops
|
223
|
+
|
224
|
+
=== Ideas
|
225
|
+
|
226
|
+
See: http://github.com/schoefmax/multi_db/wikis/home
|
227
|
+
|
228
|
+
=== Running specs
|
229
|
+
|
230
|
+
If you haven't already, install the rspec gem, then create an empty database
|
231
|
+
called "multi_db_test" (you might want to tweak the spec/config/database.yml).
|
232
|
+
From the plugin directory, run:
|
233
|
+
|
234
|
+
spec spec
|
235
|
+
|
236
|
+
|
237
|
+
Copyright (c) 2008, Max Schoefmann <max (a) pragmatic-it de>
|
238
|
+
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,186 @@
|
|
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
|
+
DEFAULT_MASTER_MODELS = ['CGI::Session::ActiveRecordStore::Session']
|
14
|
+
|
15
|
+
attr_accessor :master
|
16
|
+
tlattr_accessor :master_depth, :current, true
|
17
|
+
|
18
|
+
class << self
|
19
|
+
|
20
|
+
# defaults to RAILS_ENV if multi_db is used with Rails
|
21
|
+
# defaults to 'development' when used outside Rails
|
22
|
+
attr_accessor :environment
|
23
|
+
|
24
|
+
# a list of models that should always go directly to the master
|
25
|
+
#
|
26
|
+
# Example:
|
27
|
+
#
|
28
|
+
# MultiDb::ConnectionProxy.master_models = ['MySessionStore', 'PaymentTransaction']
|
29
|
+
attr_accessor :master_models
|
30
|
+
|
31
|
+
# decides if we should switch to the next reader automatically.
|
32
|
+
# If set to false, an after|before_filter in the ApplicationController
|
33
|
+
# has to do this.
|
34
|
+
# This will not affect failover if a master is unavailable.
|
35
|
+
attr_accessor :sticky_slave
|
36
|
+
|
37
|
+
# Replaces the connection of ActiveRecord::Base with a proxy and
|
38
|
+
# establishes the connections to the slaves.
|
39
|
+
def setup!
|
40
|
+
self.master_models ||= DEFAULT_MASTER_MODELS
|
41
|
+
self.environment ||= (defined?(RAILS_ENV) ? RAILS_ENV : 'development')
|
42
|
+
self.sticky_slave ||= false
|
43
|
+
|
44
|
+
master = ActiveRecord::Base
|
45
|
+
slaves = init_slaves
|
46
|
+
raise "No slaves databases defined for environment: #{self.environment}" if slaves.empty?
|
47
|
+
master.send :include, MultiDb::ActiveRecordExtensions
|
48
|
+
ActiveRecord::Observer.send :include, MultiDb::ObserverExtensions
|
49
|
+
master.connection_proxy = new(master, slaves)
|
50
|
+
master.logger.info("** multi_db with master and #{slaves.length} slave#{"s" if slaves.length > 1} loaded.")
|
51
|
+
end
|
52
|
+
|
53
|
+
protected
|
54
|
+
|
55
|
+
# Slave entries in the database.yml must be named like this
|
56
|
+
# development_slave_database:
|
57
|
+
# or
|
58
|
+
# development_slave_database1:
|
59
|
+
# or
|
60
|
+
# production_slave_database_someserver:
|
61
|
+
# These would be available later as MultiDb::SlaveDatabaseSomeserver
|
62
|
+
def init_slaves
|
63
|
+
returning([]) do |slaves|
|
64
|
+
ActiveRecord::Base.configurations.keys.each do |name|
|
65
|
+
if name.to_s =~ /#{self.environment}_(slave_database.*)/
|
66
|
+
MultiDb.module_eval %Q{
|
67
|
+
class #{$1.camelize} < ActiveRecord::Base
|
68
|
+
self.abstract_class = true
|
69
|
+
establish_connection :#{name}
|
70
|
+
end
|
71
|
+
}, __FILE__, __LINE__
|
72
|
+
slaves << "MultiDb::#{$1.camelize}".constantize
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private :new
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
def initialize(master, slaves)
|
83
|
+
@slaves = Scheduler.new(slaves)
|
84
|
+
@master = master
|
85
|
+
@reconnect = false
|
86
|
+
@config = @master.connection.instance_variable_get("@config")
|
87
|
+
self.current = @slaves.current
|
88
|
+
self.master_depth = 0
|
89
|
+
end
|
90
|
+
|
91
|
+
def slave
|
92
|
+
@slaves.current
|
93
|
+
end
|
94
|
+
|
95
|
+
def with_master
|
96
|
+
self.current = @master
|
97
|
+
self.master_depth += 1
|
98
|
+
yield
|
99
|
+
ensure
|
100
|
+
self.master_depth -= 1
|
101
|
+
self.current = slave if master_depth.zero?
|
102
|
+
end
|
103
|
+
|
104
|
+
def transaction(start_db_transaction = true, &block)
|
105
|
+
with_master { @master.retrieve_connection.transaction(start_db_transaction, &block) }
|
106
|
+
end
|
107
|
+
|
108
|
+
# Calls the method on master/slave and dynamically creates a new
|
109
|
+
# method on success to speed up subsequent calls
|
110
|
+
def method_missing(method, *args, &block)
|
111
|
+
returning(send(target_method(method), method, *args, &block)) do
|
112
|
+
create_delegation_method!(method)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Switches to the next slave database for read operations.
|
117
|
+
# Fails over to the master database if all slaves are unavailable.
|
118
|
+
def next_reader!
|
119
|
+
return unless master_depth.zero? # don't if in with_master block
|
120
|
+
self.current = @slaves.next
|
121
|
+
rescue Scheduler::NoMoreItems
|
122
|
+
logger.warn "[MULTIDB] All slaves are blacklisted. Reading from master"
|
123
|
+
self.current = @master
|
124
|
+
end
|
125
|
+
|
126
|
+
protected
|
127
|
+
|
128
|
+
def create_delegation_method!(method)
|
129
|
+
self.instance_eval %Q{
|
130
|
+
def #{method}(*args, &block)
|
131
|
+
#{'next_reader!' unless self.class.sticky_slave || unsafe?(method)}
|
132
|
+
#{target_method(method)}(:#{method}, *args, &block)
|
133
|
+
end
|
134
|
+
}, __FILE__, __LINE__
|
135
|
+
end
|
136
|
+
|
137
|
+
def target_method(method)
|
138
|
+
unsafe?(method) ? :send_to_master : :send_to_current
|
139
|
+
end
|
140
|
+
|
141
|
+
def send_to_master(method, *args, &block)
|
142
|
+
reconnect_master! if @reconnect
|
143
|
+
@master.retrieve_connection.send(method, *args, &block)
|
144
|
+
rescue => e
|
145
|
+
raise_master_error(e)
|
146
|
+
end
|
147
|
+
|
148
|
+
def send_to_current(method, *args, &block)
|
149
|
+
reconnect_master! if @reconnect && master?
|
150
|
+
current.retrieve_connection.send(method, *args, &block)
|
151
|
+
rescue NotImplementedError, NoMethodError
|
152
|
+
raise
|
153
|
+
rescue => e # TODO don't rescue everything
|
154
|
+
raise_master_error(e) if master?
|
155
|
+
logger.warn "[MULTIDB] Error reading from slave database"
|
156
|
+
logger.error %(#{e.message}\n#{e.backtrace.join("\n")})
|
157
|
+
@slaves.blacklist!(current)
|
158
|
+
next_reader!
|
159
|
+
retry
|
160
|
+
end
|
161
|
+
|
162
|
+
def reconnect_master!
|
163
|
+
@master.retrieve_connection.reconnect!
|
164
|
+
@reconnect = false
|
165
|
+
end
|
166
|
+
|
167
|
+
def raise_master_error(error)
|
168
|
+
logger.fatal "[MULTIDB] Error accessing master database. Scheduling reconnect"
|
169
|
+
@reconnect = true
|
170
|
+
raise error
|
171
|
+
end
|
172
|
+
|
173
|
+
def unsafe?(method)
|
174
|
+
!SAFE_METHODS.include?(method)
|
175
|
+
end
|
176
|
+
|
177
|
+
def master?
|
178
|
+
current == @master
|
179
|
+
end
|
180
|
+
|
181
|
+
def logger
|
182
|
+
ActiveRecord::Base.logger
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
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.2"
|
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", "Gleb Pomykalov"]
|
9
|
+
s.date = %q{2009-09-22}
|
10
|
+
s.description = "Connection proxy for ActiveRecord for single master / multiple slave database deployments"
|
11
|
+
s.email = "gleb.pomykalov@qik.com"
|
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/glebpom/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('schoefmax-tlattr_accessors', [">= 0.0.3"])
|
28
|
+
else
|
29
|
+
s.add_dependency('activerecord', [">= 2.1.0"])
|
30
|
+
s.add_dependency('schoefmax-tlattr_accessors', [">= 0.0.3"])
|
31
|
+
end
|
32
|
+
else
|
33
|
+
s.add_dependency('activerecord', [">= 2.1.0"])
|
34
|
+
s.add_dependency('schoefmax-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: glebpom-multi_db
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- "Maximilian Sch\xC3\xB6fmann"
|
8
|
+
- Gleb Pomykalov
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2009-09-22 00:00:00 -07:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: activerecord
|
18
|
+
type: :runtime
|
19
|
+
version_requirement:
|
20
|
+
version_requirements: !ruby/object:Gem::Requirement
|
21
|
+
requirements:
|
22
|
+
- - ">="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 2.1.0
|
25
|
+
version:
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: schoefmax-tlattr_accessors
|
28
|
+
type: :runtime
|
29
|
+
version_requirement:
|
30
|
+
version_requirements: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 0.0.3
|
35
|
+
version:
|
36
|
+
description: Connection proxy for ActiveRecord for single master / multiple slave database deployments
|
37
|
+
email: gleb.pomykalov@qik.com
|
38
|
+
executables: []
|
39
|
+
|
40
|
+
extensions: []
|
41
|
+
|
42
|
+
extra_rdoc_files:
|
43
|
+
- LICENSE
|
44
|
+
- README.rdoc
|
45
|
+
files:
|
46
|
+
- lib/multi_db.rb
|
47
|
+
- lib/multi_db/active_record_extensions.rb
|
48
|
+
- lib/multi_db/connection_proxy.rb
|
49
|
+
- lib/multi_db/observer_extensions.rb
|
50
|
+
- lib/multi_db/query_cache_compat.rb
|
51
|
+
- lib/multi_db/scheduler.rb
|
52
|
+
- LICENSE
|
53
|
+
- README.rdoc
|
54
|
+
- spec/config/database.yml
|
55
|
+
- spec/connection_proxy_spec.rb
|
56
|
+
- spec/scheduler_spec.rb
|
57
|
+
- spec/spec_helper.rb
|
58
|
+
- multi_db.gemspec
|
59
|
+
has_rdoc: true
|
60
|
+
homepage: http://github.com/glebpom/multi_db
|
61
|
+
licenses:
|
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
|
+
|