slave_pools 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +259 -0
- data/lib/slave_pools.rb +43 -0
- data/lib/slave_pools/active_record_extensions.rb +54 -0
- data/lib/slave_pools/connection_proxy.rb +280 -0
- data/lib/slave_pools/observer_extensions.rb +19 -0
- data/lib/slave_pools/query_cache_compat.rb +45 -0
- data/lib/slave_pools/slave_pool.rb +29 -0
- data/slave_pools.gemspec +24 -0
- data/spec/config/database.yml +43 -0
- data/spec/connection_proxy_spec.rb +330 -0
- data/spec/slave_pool_spec.rb +43 -0
- data/spec/slave_pools_spec.rb +69 -0
- data/spec/spec_helper.rb +17 -0
- metadata +132 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT license:
|
2
|
+
Copyright (c) 2012 Kickstarter
|
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,259 @@
|
|
1
|
+
= SlavePools
|
2
|
+
|
3
|
+
== Easy Single Master/ Multiple Slave Setup for use in Ruby/Rails projects
|
4
|
+
|
5
|
+
SlavePools builds a base layer of master/slave query splitting, by overwriting ActiveRecord's connection (with connection_proxy). With this in place, you can easily add a second layer of traffic splitting, by wrapping requests in the provided helper methods (examples below), and have a manageable master/slave solution for a standard rails application
|
6
|
+
|
7
|
+
Overview
|
8
|
+
* Sends only whitelisted SELECT-type queries to the Slaves
|
9
|
+
* Sends all other queries to the Master
|
10
|
+
* Works with query caching and transactions
|
11
|
+
* Easy to separate types of read traffic into different collections of slaves (e.g. separating admin and user traffic)
|
12
|
+
* Minimalist approach
|
13
|
+
* doesn't include sharding
|
14
|
+
* doesn't create a new ActiveRecord adapter
|
15
|
+
* doesn't weight slave db's
|
16
|
+
* Builds onto a standard database.yml file (gem doesn't initialize if no slaves are specified)
|
17
|
+
* doesn't switch slaves on its own (the user specifies when to switch in their code)
|
18
|
+
|
19
|
+
The SlavePools GEM started as a fork of Maximilian Sch\303\266fmann's https://github.com/schoefmax/multi_db
|
20
|
+
The MultiDB gem was inspired by Rick Olson's "masochism"-Plugin
|
21
|
+
|
22
|
+
== Usage
|
23
|
+
|
24
|
+
Toggle to next slave:
|
25
|
+
SlavePools.next_slave!
|
26
|
+
|
27
|
+
Specify a different slave pool than the default:
|
28
|
+
SlavePools.with_pool('other_pool') { #do stuff }
|
29
|
+
|
30
|
+
Specifically use the master for a call:
|
31
|
+
SlavePools.with_master { #do stuff }
|
32
|
+
|
33
|
+
Determine if there are slaves:
|
34
|
+
SlavePools.active?
|
35
|
+
|
36
|
+
The gem, by default, sends writes and reads to the master and slave databases, respectfully. But in your app, if you write to the master during a request, you will probably want to read from the master in that request as well, in case there is replication. You will also probably want to read from the master on the next request (after a write to the master) to cover redirects.
|
37
|
+
|
38
|
+
Using a standard rails application setup, you can achieve this by adding these example methods to your application controller (some of these may be folded in the gem, but leaving out for now):
|
39
|
+
|
40
|
+
class ApplicationController < ActionController::Base
|
41
|
+
|
42
|
+
around_filter :stick_to_master_for_updates
|
43
|
+
around_filter :use_master_for_redirect #goes with above
|
44
|
+
after_filter :switch_to_next_slave
|
45
|
+
|
46
|
+
def switch_to_next_slave
|
47
|
+
SlavePools.next_slave! if slaves?
|
48
|
+
end
|
49
|
+
|
50
|
+
def use_admin_slave_pool
|
51
|
+
SlavePools.with_pool('admin') { yield } if slaves?
|
52
|
+
end
|
53
|
+
|
54
|
+
def stick_to_master_for_updates
|
55
|
+
if slaves? && (request.post? || request.put? || request.delete?)
|
56
|
+
SlavePools.with_master { yield }
|
57
|
+
session[:stick_to_master] = 1
|
58
|
+
else
|
59
|
+
yield
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def use_master_for_redirect
|
64
|
+
if slaves? && session[:stick_to_master]
|
65
|
+
session[:stick_to_master] = nil
|
66
|
+
SlavePools.with_master { yield }
|
67
|
+
else
|
68
|
+
yield
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def use_master
|
73
|
+
if slaves?
|
74
|
+
SlavePools.with_master { yield }
|
75
|
+
session[:stick_to_master] = 1
|
76
|
+
else
|
77
|
+
yield
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def slaves?
|
82
|
+
SlavePools.active?
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
For other cases where you use the master for writes, you should wrap the request in a 'use_master' block
|
87
|
+
|
88
|
+
class PostsController < ApplicationController
|
89
|
+
around_filter :use_master, :only=>:index
|
90
|
+
|
91
|
+
def index
|
92
|
+
Activity.create()
|
93
|
+
# index is a GET call, but we've decided to record something, so we want to wrap it in a use_master block
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
* works with activerecord 3.2.12 (not tested with Rails 2)
|
98
|
+
|
99
|
+
=== Install
|
100
|
+
|
101
|
+
Add to your Gemfile
|
102
|
+
|
103
|
+
gem 'slave_pools'
|
104
|
+
|
105
|
+
=== Setup
|
106
|
+
|
107
|
+
slave_pools identifies slave databases by looking for entries of the form
|
108
|
+
"<tt><environment>_pool_<pool_name>_name_<db_name></tt>".
|
109
|
+
|
110
|
+
In your database.yml, add sections for the slaves, e.g.:
|
111
|
+
|
112
|
+
development: # that would be the master
|
113
|
+
adapter: mysql
|
114
|
+
database: myapp_production
|
115
|
+
username: root
|
116
|
+
password:
|
117
|
+
host: localhost
|
118
|
+
|
119
|
+
development_pool_default_name_slave1: # that would be a slave named 'slave1' in the 'default' pool
|
120
|
+
adapter: mysql
|
121
|
+
database: slave_db1
|
122
|
+
username: root
|
123
|
+
password:
|
124
|
+
host: 10.0.0.2
|
125
|
+
|
126
|
+
development_pool_default_name_slave2: # that would be a slave named 'slave2' in the 'default' pool
|
127
|
+
...
|
128
|
+
development_pool_admin_name_slave1: # that would be a slave named 'slave1' in the 'admin' pool (db names can be reused across pools)
|
129
|
+
...
|
130
|
+
development_pool_admin_name_another_slave: # that would be a slave named 'another_slave' in the 'admin' pool
|
131
|
+
|
132
|
+
This also creates an abstract classes named <tt>SlavePools::DefaultDb1</tt> for each db of the form <tt>SlavePools::<PoolName><DbName></tt>etc. If no slaves are specified, the SlavePools setup does not run, and the development DB would be used as normal.
|
133
|
+
|
134
|
+
For development testing, I recommend creating a read-only mysql user and just point all of your slave DB's to the your development DB using the read-only user.
|
135
|
+
|
136
|
+
The Default SlavePool will be used for all requests, so you should name on of the pools 'default' (if there isn't a 'default' slave_pool, the first slave_pool specified becomes the default)
|
137
|
+
|
138
|
+
To enable the proxy globally, add this to a config/initializers:
|
139
|
+
|
140
|
+
SlavePools.setup!
|
141
|
+
|
142
|
+
If you only want to enable it for specific environments, add this to
|
143
|
+
the corresponding file in config/environments:
|
144
|
+
|
145
|
+
config.after_initialize do
|
146
|
+
SlavePools.setup!
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
=== Using with Phusion Passenger
|
151
|
+
|
152
|
+
(this is a note from MultiDB gem and has not been verified)
|
153
|
+
|
154
|
+
With Passengers smart spawning method, child processes forked by the ApplicationSpawner
|
155
|
+
won't have the connection proxy set up properly (this is a note from ).
|
156
|
+
|
157
|
+
To make it work, add this to your <tt>environment.rb</tt> or an initializer script
|
158
|
+
(e.g. <tt>config/initializers/connection_proxy.rb</tt>):
|
159
|
+
|
160
|
+
if defined?(PhusionPassenger)
|
161
|
+
PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
162
|
+
if forked
|
163
|
+
# ... set configuration options, if any ...
|
164
|
+
SlavePools::ConnectionProxy.setup!
|
165
|
+
end
|
166
|
+
end
|
167
|
+
else # not using passenger (e.g. development/testing)
|
168
|
+
# ... set configuration options, if any ...
|
169
|
+
SlavePools::ConnectionProxy.setup!
|
170
|
+
end
|
171
|
+
|
172
|
+
=== Using with ThinkingSphinx
|
173
|
+
|
174
|
+
ThinkingSphinx looks for an adapter type and
|
175
|
+
SlavePools::ConnectionProxy.setup!
|
176
|
+
|
177
|
+
if ActiveRecord::Base.respond_to?('connection_proxy')
|
178
|
+
ThinkingSphinx::AbstractAdapter.class_eval do
|
179
|
+
def self.standard_adapter_for_model(model)
|
180
|
+
:mysql
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
=== Forcing the master for certain actions
|
186
|
+
|
187
|
+
Just add this to your controller:
|
188
|
+
|
189
|
+
around_filter(:only => :foo_action) { |c,a| ActiveRecord::Base.connection_proxy.with_master { a.call } }
|
190
|
+
|
191
|
+
=== Forcing the master for certain models
|
192
|
+
|
193
|
+
In your environment.rb or an initializer, add this *before* the call to <tt>setup!</tt>:
|
194
|
+
|
195
|
+
SlavePoolsModule::ConnectionProxy.master_models = ['CGI::Session::ActiveRecordStore::Session', 'PaymentTransaction', ...]
|
196
|
+
SlavePoolsModule::ConnectionProxy.setup!
|
197
|
+
|
198
|
+
*NOTE*: You cannot safely add more master_models after calling <tt>setup!</tt>.
|
199
|
+
=== Features
|
200
|
+
* Minimalist implementation - does include sharding, doesn't creation a new adapter (so if you don't specify slaves for
|
201
|
+
an environment, the connection is not overwritten, and the DB works as normal), doesn't blacklist/remove slaves,
|
202
|
+
* It sends everything except "select ..." queries to the master, instead of
|
203
|
+
sending only specific things to the master and anything "else" to the slave.
|
204
|
+
This avoids accidental writes to the master when there are API changes in
|
205
|
+
ActiveRecord which haven't been picked up by multi_db yet.
|
206
|
+
Note that this behavior will also always send helper methods like "+quote+" or
|
207
|
+
"<tt>add_limit!</tt>" to the master connection object, which doesn't add any
|
208
|
+
more load on the master, as these methods don't communicate with the db server
|
209
|
+
itself.
|
210
|
+
|
211
|
+
|
212
|
+
=== Differences to "multi_db":
|
213
|
+
|
214
|
+
* Supports multiple separate pools of slave databases
|
215
|
+
* query caching is fixed
|
216
|
+
* tries a slave once and immediately reverts to the master afterwards (does not cycle through slaves)
|
217
|
+
* stays with the same slave DB until explicitly told to change. In practical usage, it didn't make sense to us
|
218
|
+
to have it cycle through slaves in the same web request, so I made the 'sticky slave' feature permanent
|
219
|
+
* removed weighted slave rotation for now (didn't need it)
|
220
|
+
* Currently not using Threaded variables (left this commented out in the code for now, may revisit)
|
221
|
+
* Added with_pool method
|
222
|
+
* does not blacklist slaves for timing out (we want other more robust monitoring software to take care of this)
|
223
|
+
* better default case handling - if no slave DB's are specified, the regular Environment database is used, and the gem is
|
224
|
+
not initialized
|
225
|
+
* added a wrapper class for shorter calls
|
226
|
+
|
227
|
+
=== See also
|
228
|
+
|
229
|
+
===== Masochism
|
230
|
+
|
231
|
+
The original master/slave plugin:
|
232
|
+
|
233
|
+
* http://github.com/technoweenie/masochism
|
234
|
+
|
235
|
+
===== MultiDb
|
236
|
+
|
237
|
+
The project is based on:
|
238
|
+
|
239
|
+
* https://github.com/schoefmax/multi_db
|
240
|
+
|
241
|
+
=== Running specs
|
242
|
+
|
243
|
+
If you haven't already, install the rspec gem, then set up your database
|
244
|
+
with a test database and a read_only user.
|
245
|
+
|
246
|
+
To match spec/config/database.yml, you can:
|
247
|
+
|
248
|
+
mysql>
|
249
|
+
create database test_db;
|
250
|
+
create user 'read_only'@'localhost' identified by 'readme';
|
251
|
+
grant select on db_test.* to 'read_only'@'localhost';
|
252
|
+
|
253
|
+
From the plugin directory, run:
|
254
|
+
|
255
|
+
rspec spec
|
256
|
+
|
257
|
+
Author: Dan Drabik
|
258
|
+
Copyright (c) 2012, Kickstarter
|
259
|
+
Released under the MIT license
|
data/lib/slave_pools.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'slave_pools/slave_pool'
|
3
|
+
require 'slave_pools/active_record_extensions'
|
4
|
+
require 'slave_pools/observer_extensions'
|
5
|
+
require 'slave_pools/query_cache_compat'
|
6
|
+
require 'slave_pools/connection_proxy'
|
7
|
+
|
8
|
+
#wrapper class to make the calls more succinct
|
9
|
+
|
10
|
+
class SlavePools
|
11
|
+
|
12
|
+
def self.setup!
|
13
|
+
SlavePoolsModule::ConnectionProxy.setup!
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.active?
|
17
|
+
ActiveRecord::Base.respond_to?('connection_proxy')
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.next_slave!
|
21
|
+
ActiveRecord::Base.connection_proxy.next_slave! if active?
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.with_pool(pool_name = 'default')
|
25
|
+
if active?
|
26
|
+
ActiveRecord::Base.connection_proxy.with_pool(pool_name) { yield }
|
27
|
+
else
|
28
|
+
yield
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.with_master
|
33
|
+
if active?
|
34
|
+
ActiveRecord::Base.connection_proxy.with_master { yield }
|
35
|
+
else
|
36
|
+
yield
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.current
|
41
|
+
ActiveRecord::Base.connection_proxy.current if active?
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module SlavePoolsModule
|
2
|
+
module ActiveRecordExtensions
|
3
|
+
def self.included(base)
|
4
|
+
base.send :include, InstanceMethods
|
5
|
+
base.send :extend, ClassMethods
|
6
|
+
base.cattr_accessor :connection_proxy
|
7
|
+
# handle subclasses which were defined by the framework or plugins
|
8
|
+
base.send(:descendants).each do |child|
|
9
|
+
child.hijack_connection
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module InstanceMethods
|
14
|
+
def reload(options = nil)
|
15
|
+
self.connection_proxy.with_master { super }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
# Make sure transactions always switch to the master
|
21
|
+
def transaction(options = {}, &block)
|
22
|
+
if self.connection.kind_of?(ConnectionProxy)
|
23
|
+
super
|
24
|
+
else
|
25
|
+
self.connection_proxy.with_master { super }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Make sure caching always uses master connection
|
30
|
+
def cache(&block)
|
31
|
+
if ActiveRecord::Base.configurations.blank?
|
32
|
+
yield
|
33
|
+
else
|
34
|
+
ActiveRecord::Base.connection.cache(&block)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def inherited(child)
|
39
|
+
super
|
40
|
+
child.hijack_connection
|
41
|
+
end
|
42
|
+
|
43
|
+
def hijack_connection
|
44
|
+
return if ConnectionProxy.master_models.include?(self.to_s)
|
45
|
+
# logger.info "[SlavePools] hijacking connection for #{self.to_s}" # commenting out noisy logging
|
46
|
+
class << self
|
47
|
+
def connection
|
48
|
+
self.connection_proxy
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,280 @@
|
|
1
|
+
require 'active_record/connection_adapters/abstract/query_cache'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module SlavePoolsModule
|
5
|
+
class ConnectionProxy
|
6
|
+
include ActiveRecord::ConnectionAdapters::QueryCache
|
7
|
+
include QueryCacheCompat
|
8
|
+
|
9
|
+
# Safe methods are those that should either go to the slave ONLY or go
|
10
|
+
# to the current active connection.
|
11
|
+
SAFE_METHODS = Set.new([ :select_all, :select_one, :select_value, :select_values,
|
12
|
+
:select_rows, :select, :verify!, :raw_connection, :active?, :reconnect!,
|
13
|
+
:disconnect!, :reset_runtime, :log, :log_info ])
|
14
|
+
|
15
|
+
if ActiveRecord.const_defined?(:SessionStore) # >= Rails 2.3
|
16
|
+
DEFAULT_MASTER_MODELS = ['ActiveRecord::SessionStore::Session']
|
17
|
+
else # =< Rails 2.3
|
18
|
+
DEFAULT_MASTER_MODELS = ['CGI::Session::ActiveRecordStore::Session']
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_accessor :master
|
22
|
+
attr_accessor :master_depth, :current, :current_pool
|
23
|
+
|
24
|
+
class << self
|
25
|
+
|
26
|
+
# defaults to Rails.env if multi_db is used with Rails
|
27
|
+
# defaults to 'development' when used outside Rails
|
28
|
+
attr_accessor :environment
|
29
|
+
|
30
|
+
# a list of models that should always go directly to the master
|
31
|
+
#
|
32
|
+
# Example:
|
33
|
+
#
|
34
|
+
# SlavePool::ConnectionProxy.master_models = ['MySessionStore', 'PaymentTransaction']
|
35
|
+
attr_accessor :master_models
|
36
|
+
|
37
|
+
#true or false - whether you want to include the ActionController helpers or not
|
38
|
+
#this allow
|
39
|
+
|
40
|
+
# if master should be the default db
|
41
|
+
attr_accessor :defaults_to_master
|
42
|
+
|
43
|
+
# #setting a config instance variable so that thinking sphinx,and other gems that use the connection.instance_variable_get(:@config), work correctly
|
44
|
+
attr_accessor :config
|
45
|
+
|
46
|
+
# Replaces the connection of ActiveRecord::Base with a proxy and
|
47
|
+
# establishes the connections to the slaves.
|
48
|
+
def setup!
|
49
|
+
self.master_models ||= DEFAULT_MASTER_MODELS
|
50
|
+
self.environment ||= (defined?(Rails.env) ? Rails.env : 'development')
|
51
|
+
|
52
|
+
slave_pools = init_slave_pools
|
53
|
+
# if there are no slave pools, we just want to silently exit and not edit the ActiveRecord::Base.connection
|
54
|
+
if !slave_pools.empty?
|
55
|
+
master = ActiveRecord::Base
|
56
|
+
master.send :include, SlavePoolsModule::ActiveRecordExtensions
|
57
|
+
ActiveRecord::Observer.send :include, SlavePoolsModule::ObserverExtensions
|
58
|
+
|
59
|
+
master.connection_proxy = new(master, slave_pools)
|
60
|
+
master.logger.info("** slave_pools with master and #{slave_pools.length} slave_pool#{"s" if slave_pools.length > 1} (#{slave_pools.keys}) loaded.")
|
61
|
+
else
|
62
|
+
ActiveRecord::Base.logger.info(" No Slave Pools specified for this environment") #this is currently not logging
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
protected
|
67
|
+
|
68
|
+
def init_slave_pools
|
69
|
+
slave_pools = {}
|
70
|
+
ActiveRecord::Base.configurations.each do |name, db_config|
|
71
|
+
# look for dbs matching the slave_pool format and verify a test connection before adding it to the pools
|
72
|
+
if name.to_s =~ /#{self.environment}_pool_(.*)_name_(.*)/ && connection_valid?(db_config)
|
73
|
+
slave_pools = add_to_pool(slave_pools, $1, $2, name, db_config)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
return slave_pools
|
77
|
+
end
|
78
|
+
|
79
|
+
private :new
|
80
|
+
|
81
|
+
end # end class << self
|
82
|
+
|
83
|
+
def initialize(master, slave_pools)
|
84
|
+
@slave_pools = {}
|
85
|
+
slave_pools.each do |pool_name, slaves|
|
86
|
+
@slave_pools[pool_name.to_sym] = SlavePool.new(pool_name, slaves)
|
87
|
+
end
|
88
|
+
@master = master
|
89
|
+
@reconnect = false
|
90
|
+
@current_pool = default_pool
|
91
|
+
if self.class.defaults_to_master
|
92
|
+
@current = @master
|
93
|
+
@master_depth = 1
|
94
|
+
@config = master.connection.instance_variable_get(:@config)
|
95
|
+
else
|
96
|
+
@current = slave
|
97
|
+
@master_depth = 0
|
98
|
+
@config = @current.config_hash #setting this
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
def default_pool
|
104
|
+
@slave_pools[:default] || @slave_pools.values.first #if there is no default specified, use the first pool found
|
105
|
+
end
|
106
|
+
|
107
|
+
def slave_pools
|
108
|
+
@slave_pools
|
109
|
+
end
|
110
|
+
|
111
|
+
def slave
|
112
|
+
@current_pool.current
|
113
|
+
end
|
114
|
+
|
115
|
+
def with_pool(pool_name = 'default')
|
116
|
+
@current_pool = @slave_pools[pool_name.to_sym] || default_pool
|
117
|
+
@current = slave unless within_master_block?
|
118
|
+
yield
|
119
|
+
ensure
|
120
|
+
@current_pool = default_pool
|
121
|
+
@current = slave unless within_master_block?
|
122
|
+
end
|
123
|
+
|
124
|
+
def with_master
|
125
|
+
@current = @master
|
126
|
+
@master_depth += 1
|
127
|
+
yield
|
128
|
+
ensure
|
129
|
+
@master_depth -= 1
|
130
|
+
@master_depth = 0 if @master_depth < 0 # ensure that master depth never gets below 0
|
131
|
+
@current = slave if !within_master_block?
|
132
|
+
end
|
133
|
+
|
134
|
+
def within_master_block?
|
135
|
+
@master_depth > 0
|
136
|
+
end
|
137
|
+
|
138
|
+
def transaction(start_db_transaction = true, &block)
|
139
|
+
with_master { @master.retrieve_connection.transaction(start_db_transaction, &block) }
|
140
|
+
end
|
141
|
+
|
142
|
+
# Calls the method on master/slave and dynamically creates a new
|
143
|
+
# method on success to speed up subsequent calls
|
144
|
+
def method_missing(method, *args, &block)
|
145
|
+
send(target_method(method), method, *args, &block).tap do
|
146
|
+
create_delegation_method!(method)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Switches to the next slave database for read operations.
|
151
|
+
# Fails over to the master database if all slaves are unavailable.
|
152
|
+
def next_slave!
|
153
|
+
return if within_master_block? # don't if in with_master block
|
154
|
+
@current = @current_pool.next
|
155
|
+
rescue
|
156
|
+
@current = @master
|
157
|
+
end
|
158
|
+
|
159
|
+
protected
|
160
|
+
|
161
|
+
def create_delegation_method!(method)
|
162
|
+
self.instance_eval %Q{
|
163
|
+
def #{method}(*args, &block)
|
164
|
+
#{target_method(method)}(:#{method}, *args, &block)
|
165
|
+
end
|
166
|
+
}, __FILE__, __LINE__
|
167
|
+
end
|
168
|
+
|
169
|
+
def target_method(method)
|
170
|
+
unsafe?(method) ? :send_to_master : :send_to_current
|
171
|
+
end
|
172
|
+
|
173
|
+
def send_to_master(method, *args, &block)
|
174
|
+
reconnect_master! if @reconnect
|
175
|
+
@master.retrieve_connection.send(method, *args, &block)
|
176
|
+
rescue => e
|
177
|
+
log_errors(e, 'send_to_master', method)
|
178
|
+
raise_master_error(e)
|
179
|
+
end
|
180
|
+
|
181
|
+
def send_to_current(method, *args, &block)
|
182
|
+
reconnect_master! if @reconnect && master?
|
183
|
+
# logger.debug "[SlavePools] Using #{@current.name}"
|
184
|
+
@current = @master if unsafe?(method) #failsafe to avoid sending dangerous method to master
|
185
|
+
@current.retrieve_connection.send(method, *args, &block)
|
186
|
+
rescue Mysql2::Error, ActiveRecord::StatementInvalid => e
|
187
|
+
log_errors(e, 'send_to_current', method)
|
188
|
+
raise_master_error(e) if master?
|
189
|
+
logger.warn "[SlavePools] Error reading from slave database"
|
190
|
+
logger.error %(#{e.message}\n#{e.backtrace.join("\n")})
|
191
|
+
if e.message.match(/Timeout waiting for a response from the last query/)
|
192
|
+
# Verify that the connection is active & re-raise
|
193
|
+
@current.retrieve_connection.verify!
|
194
|
+
raise e
|
195
|
+
else
|
196
|
+
send_to_master(method, *args, &block) # if cant connect, send the query to master
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def reconnect_master!
|
201
|
+
@master.retrieve_connection.reconnect!
|
202
|
+
@reconnect = false
|
203
|
+
end
|
204
|
+
|
205
|
+
def raise_master_error(error)
|
206
|
+
logger.fatal "[SlavePools] Error accessing master database. Scheduling reconnect"
|
207
|
+
@reconnect = true
|
208
|
+
raise error
|
209
|
+
end
|
210
|
+
|
211
|
+
def unsafe?(method)
|
212
|
+
!SAFE_METHODS.include?(method)
|
213
|
+
end
|
214
|
+
|
215
|
+
def master?
|
216
|
+
@current == @master
|
217
|
+
end
|
218
|
+
|
219
|
+
def logger
|
220
|
+
ActiveRecord::Base.logger
|
221
|
+
end
|
222
|
+
|
223
|
+
private
|
224
|
+
|
225
|
+
def self.add_to_pool(slave_pools, pool_name, slave_name, full_db_name, db_config)
|
226
|
+
slave_pools[pool_name] ||= []
|
227
|
+
db_config_with_symbols = db_config.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
|
228
|
+
SlavePoolsModule.module_eval %Q{
|
229
|
+
class #{pool_name.camelize}#{slave_name.camelize} < ActiveRecord::Base
|
230
|
+
self.abstract_class = true
|
231
|
+
establish_connection :#{full_db_name}
|
232
|
+
def self.config_hash
|
233
|
+
#{db_config_with_symbols.inspect}
|
234
|
+
end
|
235
|
+
end
|
236
|
+
}, __FILE__, __LINE__
|
237
|
+
slave_pools[pool_name] << "SlavePoolsModule::#{pool_name.camelize}#{slave_name.camelize}".constantize
|
238
|
+
return slave_pools
|
239
|
+
end
|
240
|
+
|
241
|
+
# method to verify whether DB connection is active?
|
242
|
+
def self.connection_valid?(db_config = nil)
|
243
|
+
is_connected = false
|
244
|
+
if db_config
|
245
|
+
begin
|
246
|
+
ActiveRecord::Base.establish_connection(db_config)
|
247
|
+
ActiveRecord::Base.connection
|
248
|
+
is_connected = ActiveRecord::Base.connected?
|
249
|
+
rescue => e
|
250
|
+
log_errors(e, 'self.connection_valid?')
|
251
|
+
ensure
|
252
|
+
ActiveRecord::Base.establish_connection(environment) #rollback to the current environment to avoid issues
|
253
|
+
end
|
254
|
+
end
|
255
|
+
return is_connected
|
256
|
+
end
|
257
|
+
|
258
|
+
# logging instance errors
|
259
|
+
def log_errors(error, sp_method, db_method)
|
260
|
+
logger.error "[SlavePools] - Error: #{error}"
|
261
|
+
logger.error "[SlavePools] - SlavePool Method: #{sp_method}"
|
262
|
+
logger.error "[SlavePools] - Master Value: #{@master}"
|
263
|
+
logger.error "[SlavePools] - Master Depth: #{@master_depth}"
|
264
|
+
logger.error "[SlavePools] - Current Value: #{@current}"
|
265
|
+
logger.error "[SlavePools] - Current Pool: #{@current_pool}"
|
266
|
+
logger.error "[SlavePools] - Current Pool Slaves: #{@current_pool.slaves}" if @current_pool
|
267
|
+
logger.error "[SlavePools] - Current Pool Name: #{@current_pool.name}" if @current_pool
|
268
|
+
logger.error "[SlavePools] - Reconnect Value: #{@reconnect}"
|
269
|
+
logger.error "[SlavePools] - Default Pool: #{default_pool}"
|
270
|
+
logger.error "[SlavePools] - DB Method: #{db_method}"
|
271
|
+
end
|
272
|
+
|
273
|
+
# logging class errors
|
274
|
+
def self.log_errors(error, sp_method)
|
275
|
+
logger = ActiveRecord::Base.logger
|
276
|
+
logger.error "[SlavePools] - Error: #{error}"
|
277
|
+
logger.error "[SlavePools] - SlavePool Method: #{sp_method}"
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|