slave_pools 0.1.2 → 1.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +196 -0
- data/lib/slave_pools/active_record_extensions.rb +7 -36
- data/lib/slave_pools/config.rb +22 -0
- data/lib/slave_pools/connection_proxy.rb +86 -225
- data/lib/slave_pools/engine.rb +24 -0
- data/lib/slave_pools/hijack.rb +24 -0
- data/lib/slave_pools/observer_extensions.rb +5 -5
- data/lib/slave_pools/pool.rb +21 -0
- data/lib/slave_pools/pools.rb +60 -0
- data/lib/slave_pools/query_cache.rb +35 -0
- data/lib/slave_pools/version.rb +3 -0
- data/lib/slave_pools.rb +55 -27
- data/spec/config/test_model.rb +19 -0
- data/spec/connection_proxy_spec.rb +182 -257
- data/spec/observer_extensions_spec.rb +21 -0
- data/spec/pool_spec.rb +35 -0
- data/spec/query_cache_spec.rb +83 -0
- data/spec/slave_pools_spec.rb +18 -56
- data/spec/spec_helper.rb +34 -6
- metadata +105 -108
- data/README.rdoc +0 -259
- data/lib/slave_pools/query_cache_compat.rb +0 -45
- data/lib/slave_pools/slave_pool.rb +0 -29
- data/slave_pools.gemspec +0 -25
- data/spec/config/database.yml +0 -43
- data/spec/slave_pool_spec.rb +0 -43
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ca6c92616b9b71f41b9a8f0acf4487266dddc08e
|
4
|
+
data.tar.gz: 353fe4fd321793f797ca765a6a1fbac2cd942485
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e8a36182804f50de04b2c049dd671244a1b6a90dc6ab53a0ba57df3898caf171da964af02c2871e19b2a1f673ba14560cb6e797e0b950ef463f228858524d606
|
7
|
+
data.tar.gz: e22b49cd625efcbfc5baff7b7868f79494f0f9499d992fafc8cdccfba6580855a55c59023e0b18492db0e784dc24f32974b807b8f0d2dbdaecd3d33e37569ea8
|
data/README.md
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
# SlavePools
|
2
|
+
|
3
|
+
Easy Single Master/ Multiple Slave Setup for use in Ruby/Rails projects
|
4
|
+
|
5
|
+
[![Build
|
6
|
+
Status](https://travis-ci.org/kickstarter/slave_pools.png?branch=owningit)](https://travis-ci.org/kickstarter/slave_pools)
|
7
|
+
|
8
|
+
## Overview
|
9
|
+
|
10
|
+
SlavePools replaces ActiveRecord's connection with a proxy that routes database interactions to the proper connection. Safe (whitelisted) methods may go to the current replica, and all other methods go to the master connection.
|
11
|
+
|
12
|
+
SlavePools also provides helpers so you can customize your replica strategy. You can organize replicas into pools and cycle through them (e.g. in a before_filter). You can make the connection default to the master, or the default replica pool, and then use block helpers to temporarily change the behavior (e.g. in an around_filter).
|
13
|
+
|
14
|
+
* Uses a naming convention in database.yml to designate replica pools.
|
15
|
+
* Defaults to a given replica pool, but may also be configured to default to master.
|
16
|
+
* Routes database interactions (queries) to the right connection
|
17
|
+
* Whitelisted queries go to the current connection (might be a replica).
|
18
|
+
* All queries inside a transaction run on master.
|
19
|
+
* All other queries are also sent to the master connection.
|
20
|
+
* Supports ActiveRecord's in-memory query caching.
|
21
|
+
* Helper methods can be used to easily load balance replicas, route traffic to different replica pools, or run directly against master. (examples below)
|
22
|
+
|
23
|
+
## Not Supported
|
24
|
+
|
25
|
+
* Sharding.
|
26
|
+
* Automatic load balancing strategies.
|
27
|
+
* Replica weights. You can accomplish this in your own load balancing strategy.
|
28
|
+
* Whitelisting models that always use master.
|
29
|
+
* Blacklisting poorly performing replicas. This could cause load spikes on your master. Whatever provisions your database.yml should make this choice.
|
30
|
+
|
31
|
+
## Installation and Setup
|
32
|
+
|
33
|
+
Add to your Gemfile:
|
34
|
+
|
35
|
+
gem 'slave_pools'
|
36
|
+
|
37
|
+
### Adding Replicas
|
38
|
+
|
39
|
+
Add entries to your database.yml in the form of `<environment>_pool_<pool_name>_name_<db_name>`
|
40
|
+
|
41
|
+
For example:
|
42
|
+
|
43
|
+
# Master connection for production environment
|
44
|
+
production:
|
45
|
+
adapter: mysql
|
46
|
+
database: myapp_production
|
47
|
+
username: root
|
48
|
+
password:
|
49
|
+
host: localhost
|
50
|
+
|
51
|
+
# Default pool for production environment
|
52
|
+
production_pool_default_name_replica1:
|
53
|
+
adapter: mysql
|
54
|
+
database: replica_db1
|
55
|
+
username: root
|
56
|
+
password:
|
57
|
+
host: 10.0.0.2
|
58
|
+
production_pool_default_name_replica2:
|
59
|
+
...
|
60
|
+
|
61
|
+
# Special pool for production environment
|
62
|
+
production_pool_admin_name_replica1:
|
63
|
+
...
|
64
|
+
production_pool_admin_name_another_replica:
|
65
|
+
...
|
66
|
+
|
67
|
+
### Simulating Replicas
|
68
|
+
|
69
|
+
If you don't have any replicas (e.g. in your development environment), SlavePools will create a default pool containing only master. But if you want to mimic your production environment more closely you can create a read-only mysql user and use it like a replica.
|
70
|
+
|
71
|
+
# Development connection
|
72
|
+
development: &dev
|
73
|
+
adapter: mysql
|
74
|
+
database: myapp_development
|
75
|
+
username: root
|
76
|
+
password:
|
77
|
+
host: localhost
|
78
|
+
|
79
|
+
development_pool_default_name_replica1:
|
80
|
+
username: readonly
|
81
|
+
<<: &dev
|
82
|
+
|
83
|
+
Don't do this in your test environment if you use transactional tests though! The replica connections won't be able to see any fixtures or factory data.
|
84
|
+
|
85
|
+
### Configuring
|
86
|
+
|
87
|
+
Add a `config/initializers/slave_pools.rb` if you want to change config settings:
|
88
|
+
|
89
|
+
SlavePools.config.defaults_to_master = true
|
90
|
+
|
91
|
+
## Usage
|
92
|
+
|
93
|
+
Toggle to next replica:
|
94
|
+
|
95
|
+
SlavePools.next_slave!
|
96
|
+
|
97
|
+
Specify a pool besides the default:
|
98
|
+
|
99
|
+
SlavePools.with_pool('other_pool') { #do stuff }
|
100
|
+
|
101
|
+
Specifically use the master for a call:
|
102
|
+
|
103
|
+
SlavePools.with_master { #do stuff }
|
104
|
+
|
105
|
+
### Load Balancing
|
106
|
+
|
107
|
+
If you have multiple replicas in a pool and you'd like to load balance requests between them, you can easily accomplish this with a `before_filter`:
|
108
|
+
|
109
|
+
class ApplicationController < ActionController::Base
|
110
|
+
after_filter :switch_to_next_slave
|
111
|
+
|
112
|
+
protected
|
113
|
+
|
114
|
+
def switch_to_next_slave
|
115
|
+
SlavePools.next_slave!
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
### Specialty Pools
|
120
|
+
|
121
|
+
If you have specialized replica pools and would like to use them for different controllers or actions, you can use an `around_filter`:
|
122
|
+
|
123
|
+
class ApplicationController < ActionController::Base
|
124
|
+
around_filter :use_special_replicas
|
125
|
+
|
126
|
+
protected
|
127
|
+
|
128
|
+
def use_special_replicas
|
129
|
+
SlavePools.with_pool('special'){ yield }
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
### Replica Lag
|
134
|
+
|
135
|
+
By default, writes are sent to the master and reads are sent to replicas. But replicas might lag behind the master by seconds or even minutes. So if you write to master during a request you probably want to read from master in that request as well. You may even want to read from the master on the _next_ request, to cover redirects.
|
136
|
+
|
137
|
+
Here's one way to accomplish that:
|
138
|
+
|
139
|
+
class ApplicationController < ActionController::Base
|
140
|
+
|
141
|
+
around_filter :stick_to_master_for_updates
|
142
|
+
around_filter :use_master_for_redirect #goes with above
|
143
|
+
|
144
|
+
def stick_to_master_for_updates
|
145
|
+
if request.get?
|
146
|
+
yield
|
147
|
+
else
|
148
|
+
SlavePools.with_master { yield }
|
149
|
+
session[:stick_to_master] = 1
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def use_master_for_redirect
|
154
|
+
if session[:stick_to_master]
|
155
|
+
session[:stick_to_master] = nil
|
156
|
+
SlavePools.with_master { yield }
|
157
|
+
else
|
158
|
+
yield
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
## Running specs
|
164
|
+
|
165
|
+
If you haven't already, install the rspec gem, then set up your database
|
166
|
+
with a test database and a read_only user.
|
167
|
+
|
168
|
+
To match spec/config/database.yml, you can run:
|
169
|
+
|
170
|
+
rake bootstrap
|
171
|
+
|
172
|
+
From the plugin directory, run:
|
173
|
+
|
174
|
+
rspec spec
|
175
|
+
|
176
|
+
## Authors
|
177
|
+
|
178
|
+
Author: Dan Drabik, Lance Ivy
|
179
|
+
|
180
|
+
Copyright (c) 2012-2013, Kickstarter
|
181
|
+
|
182
|
+
Released under the MIT license
|
183
|
+
|
184
|
+
## See also
|
185
|
+
|
186
|
+
### MultiDb
|
187
|
+
|
188
|
+
The project is based on:
|
189
|
+
|
190
|
+
* https://github.com/schoefmax/multi_db
|
191
|
+
|
192
|
+
### Masochism
|
193
|
+
|
194
|
+
The original master/slave plugin:
|
195
|
+
|
196
|
+
* http://github.com/technoweenie/masochism
|
@@ -1,23 +1,18 @@
|
|
1
|
-
module
|
1
|
+
module SlavePools
|
2
2
|
module ActiveRecordExtensions
|
3
3
|
def self.included(base)
|
4
|
-
base.send :include, InstanceMethods
|
5
4
|
base.send :extend, ClassMethods
|
6
5
|
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
6
|
end
|
12
7
|
|
13
|
-
|
14
|
-
|
15
|
-
self.connection_proxy.with_master { super }
|
16
|
-
end
|
8
|
+
def reload(options = nil)
|
9
|
+
self.connection_proxy.with_master { super }
|
17
10
|
end
|
18
11
|
|
19
12
|
module ClassMethods
|
20
|
-
# Make sure transactions
|
13
|
+
# Make sure transactions run on master
|
14
|
+
# Even if they're initiated from ActiveRecord::Base
|
15
|
+
# (which doesn't have our hijack).
|
21
16
|
def transaction(options = {}, &block)
|
22
17
|
if self.connection.kind_of?(ConnectionProxy)
|
23
18
|
super
|
@@ -25,30 +20,6 @@ module SlavePoolsModule
|
|
25
20
|
self.connection_proxy.with_master { super }
|
26
21
|
end
|
27
22
|
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
23
|
end
|
53
24
|
end
|
54
|
-
end
|
25
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module SlavePools
|
2
|
+
class Config
|
3
|
+
# The current environment. Normally set to Rails.env, but
|
4
|
+
# will default to 'development' outside of Rails apps.
|
5
|
+
attr_accessor :environment
|
6
|
+
|
7
|
+
# When true, all queries will go to master unless wrapped in with_pool{}.
|
8
|
+
# When false, all safe queries will go to the current replica unless wrapped in with_master{}.
|
9
|
+
# Defaults to false.
|
10
|
+
attr_accessor :defaults_to_master
|
11
|
+
|
12
|
+
# The list of methods considered safe to send to a readonly connection.
|
13
|
+
# Defaults are based on Rails version.
|
14
|
+
attr_accessor :safe_methods
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@environment = 'development'
|
18
|
+
@defaults_to_master = false
|
19
|
+
@safe_methods = []
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -1,282 +1,143 @@
|
|
1
1
|
require 'active_record/connection_adapters/abstract/query_cache'
|
2
2
|
require 'set'
|
3
3
|
|
4
|
-
module
|
4
|
+
module SlavePools
|
5
5
|
class ConnectionProxy
|
6
|
-
include
|
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
|
6
|
+
include SlavePools::QueryCache
|
20
7
|
|
21
8
|
attr_accessor :master
|
22
|
-
attr_accessor :master_depth, :current, :current_pool
|
9
|
+
attr_accessor :master_depth, :current, :current_pool, :slave_pools
|
23
10
|
|
24
11
|
class << self
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
12
|
+
def generate_safe_delegations
|
13
|
+
SlavePools.config.safe_methods.each do |method|
|
14
|
+
generate_safe_delegation(method) unless instance_methods.include?(method)
|
63
15
|
end
|
64
16
|
end
|
65
17
|
|
66
18
|
protected
|
67
19
|
|
68
|
-
def
|
69
|
-
|
70
|
-
|
71
|
-
|
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)
|
20
|
+
def generate_safe_delegation(method)
|
21
|
+
class_eval <<-END, __FILE__, __LINE__ + 1
|
22
|
+
def #{method}(*args, &block)
|
23
|
+
route_to(current, :#{method}, *args, &block)
|
74
24
|
end
|
75
|
-
|
76
|
-
return slave_pools
|
25
|
+
END
|
77
26
|
end
|
27
|
+
end
|
78
28
|
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
29
|
+
def initialize(master, pools)
|
30
|
+
@master = master
|
31
|
+
@slave_pools = pools
|
32
|
+
@master_depth = 0
|
90
33
|
@current_pool = default_pool
|
91
|
-
|
92
|
-
|
93
|
-
@
|
94
|
-
@config = master.connection.instance_variable_get(:@config)
|
34
|
+
|
35
|
+
if SlavePools.config.defaults_to_master
|
36
|
+
@current = master
|
95
37
|
else
|
96
|
-
@current =
|
97
|
-
@master_depth = 0
|
98
|
-
@config = @current.config_hash #setting this
|
38
|
+
@current = current_slave
|
99
39
|
end
|
100
40
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
@
|
105
|
-
end
|
106
|
-
|
107
|
-
def slave_pools
|
108
|
-
@slave_pools
|
109
|
-
end
|
110
|
-
|
111
|
-
def slave
|
112
|
-
@current_pool.current
|
41
|
+
# this ivar is for ConnectionAdapter compatibility
|
42
|
+
# some gems (e.g. newrelic_rpm) will actually use
|
43
|
+
# instance_variable_get(:@config) to find it.
|
44
|
+
@config = current.connection_config
|
113
45
|
end
|
114
46
|
|
115
47
|
def with_pool(pool_name = 'default')
|
116
|
-
|
117
|
-
|
48
|
+
last_conn, last_pool = self.current, self.current_pool
|
49
|
+
self.current_pool = slave_pools[pool_name.to_sym] || default_pool
|
50
|
+
self.current = current_slave unless within_master_block?
|
118
51
|
yield
|
119
52
|
ensure
|
120
|
-
|
121
|
-
|
53
|
+
self.current_pool = last_pool
|
54
|
+
self.current = last_conn
|
122
55
|
end
|
123
56
|
|
124
57
|
def with_master
|
125
|
-
|
126
|
-
|
58
|
+
last_conn = self.current
|
59
|
+
self.current = master
|
60
|
+
self.master_depth += 1
|
127
61
|
yield
|
128
62
|
ensure
|
129
|
-
|
130
|
-
|
131
|
-
@current = slave if !within_master_block?
|
132
|
-
end
|
133
|
-
|
134
|
-
def within_master_block?
|
135
|
-
@master_depth > 0
|
63
|
+
self.master_depth = [master_depth - 1, 0].max
|
64
|
+
self.current = last_conn
|
136
65
|
end
|
137
66
|
|
138
|
-
def transaction(
|
139
|
-
with_master {
|
67
|
+
def transaction(*args, &block)
|
68
|
+
with_master { master.transaction(*args, &block) }
|
140
69
|
end
|
141
70
|
|
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
71
|
def next_slave!
|
153
|
-
return if within_master_block?
|
154
|
-
|
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
|
-
logger.error "[SlavePools] Slave Query Timeout - do not send to master"
|
194
|
-
@current.retrieve_connection.verify!
|
195
|
-
raise e
|
196
|
-
else
|
197
|
-
logger.error "[SlavePools] Slave Query Error - sending to master"
|
198
|
-
send_to_master(method, *args, &block) # if cant connect, send the query to master
|
199
|
-
end
|
72
|
+
return if within_master_block?
|
73
|
+
self.current = current_pool.next
|
200
74
|
end
|
201
75
|
|
202
|
-
def
|
203
|
-
|
204
|
-
@reconnect = false
|
76
|
+
def current_slave
|
77
|
+
current_pool.current
|
205
78
|
end
|
206
79
|
|
207
|
-
|
208
|
-
logger.fatal "[SlavePools] Error accessing master database. Scheduling reconnect"
|
209
|
-
@reconnect = true
|
210
|
-
raise error
|
211
|
-
end
|
80
|
+
protected
|
212
81
|
|
213
|
-
def
|
214
|
-
|
82
|
+
def default_pool
|
83
|
+
slave_pools[:default] || slave_pools.values.first
|
215
84
|
end
|
216
85
|
|
217
|
-
|
218
|
-
|
86
|
+
# Proxies any unknown methods to master.
|
87
|
+
# Safe methods have been generated during `setup!`.
|
88
|
+
# Creates a method to speed up subsequent calls.
|
89
|
+
def method_missing(method, *args, &block)
|
90
|
+
generate_unsafe_delegation(method)
|
91
|
+
send(method, *args, &block)
|
219
92
|
end
|
220
93
|
|
221
|
-
def
|
222
|
-
|
94
|
+
def within_master_block?
|
95
|
+
master_depth > 0
|
223
96
|
end
|
224
97
|
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
db_config_with_symbols = db_config.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
|
230
|
-
SlavePoolsModule.module_eval %Q{
|
231
|
-
class #{pool_name.camelize}#{slave_name.camelize} < ActiveRecord::Base
|
232
|
-
self.abstract_class = true
|
233
|
-
establish_connection :#{full_db_name}
|
234
|
-
def self.config_hash
|
235
|
-
#{db_config_with_symbols.inspect}
|
236
|
-
end
|
98
|
+
def generate_unsafe_delegation(method)
|
99
|
+
self.class_eval <<-END, __FILE__, __LINE__ + 1
|
100
|
+
def #{method}(*args, &block)
|
101
|
+
route_to(master, :#{method}, *args, &block)
|
237
102
|
end
|
238
|
-
|
239
|
-
slave_pools[pool_name] << "SlavePoolsModule::#{pool_name.camelize}#{slave_name.camelize}".constantize
|
240
|
-
return slave_pools
|
103
|
+
END
|
241
104
|
end
|
242
105
|
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
106
|
+
def route_to(conn, method, *args, &block)
|
107
|
+
conn.retrieve_connection.send(method, *args, &block)
|
108
|
+
rescue => e
|
109
|
+
SlavePools.log :error, "Error during ##{method}: #{e}"
|
110
|
+
log_proxy_state
|
111
|
+
raise if conn == master
|
112
|
+
|
113
|
+
if safe_to_replay(e)
|
114
|
+
SlavePools.log :error, %(#{e.message}\n#{e.backtrace.join("\n")})
|
115
|
+
SlavePools.log :error, "Replaying on master."
|
116
|
+
route_to(master, method, *args, &block)
|
117
|
+
else
|
118
|
+
current.retrieve_connection.verify! # may reconnect
|
119
|
+
raise e
|
256
120
|
end
|
257
|
-
return is_connected
|
258
121
|
end
|
259
122
|
|
260
|
-
#
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
logger.error "[SlavePools] - Current Value: #{@current}"
|
267
|
-
logger.error "[SlavePools] - Current Pool: #{@current_pool}"
|
268
|
-
logger.error "[SlavePools] - Current Pool Slaves: #{@current_pool.slaves}" if @current_pool
|
269
|
-
logger.error "[SlavePools] - Current Pool Name: #{@current_pool.name}" if @current_pool
|
270
|
-
logger.error "[SlavePools] - Reconnect Value: #{@reconnect}"
|
271
|
-
logger.error "[SlavePools] - Default Pool: #{default_pool}"
|
272
|
-
logger.error "[SlavePools] - DB Method: #{db_method}"
|
123
|
+
# decides whether to replay query against master based on the
|
124
|
+
# exception raised. this could become more sophisticated.
|
125
|
+
def safe_to_replay(e)
|
126
|
+
# don't replay queries that time out. we don't have the time, and they
|
127
|
+
# could be dangerous.
|
128
|
+
! e.message.match(/Timeout waiting for a response from the last query/)
|
273
129
|
end
|
274
130
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
131
|
+
private
|
132
|
+
|
133
|
+
def log_proxy_state
|
134
|
+
SlavePools.log :error, "Master Value: #{master}"
|
135
|
+
SlavePools.log :error, "Master Depth: #{master_depth}"
|
136
|
+
SlavePools.log :error, "Current Value: #{current}"
|
137
|
+
SlavePools.log :error, "Current Pool: #{current_pool}"
|
138
|
+
SlavePools.log :error, "Current Pool Slaves: #{current_pool.slaves}" if current_pool
|
139
|
+
SlavePools.log :error, "Current Pool Name: #{current_pool.name}" if current_pool
|
140
|
+
SlavePools.log :error, "Default Pool: #{default_pool}"
|
280
141
|
end
|
281
142
|
end
|
282
143
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rails/engine'
|
2
|
+
|
3
|
+
module SlavePools
|
4
|
+
class Engine < Rails::Engine
|
5
|
+
initializer 'slave_pools.defaults' do
|
6
|
+
SlavePools.config.environment = Rails.env
|
7
|
+
|
8
|
+
SlavePools.config.safe_methods =
|
9
|
+
if ActiveRecord::VERSION::MAJOR == 3
|
10
|
+
[
|
11
|
+
: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
|
+
else
|
16
|
+
warn "Unsupported ActiveRecord version #{ActiveRecord.version}. Please whitelist the safe methods."
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
config.after_initialize do
|
21
|
+
SlavePools.setup!
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|