replica_pools 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +196 -0
- data/lib/replica_pools.rb +63 -0
- data/lib/replica_pools/active_record_extensions.rb +28 -0
- data/lib/replica_pools/config.rb +22 -0
- data/lib/replica_pools/connection_proxy.rb +125 -0
- data/lib/replica_pools/engine.rb +30 -0
- data/lib/replica_pools/hijack.rb +24 -0
- data/lib/replica_pools/pool.rb +21 -0
- data/lib/replica_pools/pools.rb +52 -0
- data/lib/replica_pools/query_cache.rb +35 -0
- data/lib/replica_pools/version.rb +3 -0
- data/spec/config/test_model.rb +2 -0
- data/spec/connection_proxy_spec.rb +238 -0
- data/spec/pool_spec.rb +35 -0
- data/spec/query_cache_spec.rb +86 -0
- data/spec/slave_pools_spec.rb +31 -0
- data/spec/spec_helper.rb +44 -0
- metadata +153 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 87d48e4179aaed026e490fc8449b9b9b68f288cc
|
4
|
+
data.tar.gz: 4892116bdad80f053398ca6939104ce7f2de74a6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d98d3a9401f909fffdfad0eb313c1735c50d016d5bcd54186d61459ad20be23c5d2c83d97a451e1fbb2e39c11449058239790584c88e10f7088b11c06924c75a
|
7
|
+
data.tar.gz: ff7aaeb624ca01d426b6938778500e12677bde3a9ed627b37cea77eaf1da7ebf8f42f17c7ae37e7b9b033b93ff03dfb608e5148fcf8667c42c77f4051a0a34b3
|
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.md
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
# ReplicaPools
|
2
|
+
|
3
|
+
Easy Single Leader / Multiple Replica Setup for use in Ruby/Rails projects
|
4
|
+
|
5
|
+
[![Build
|
6
|
+
Status](https://travis-ci.org/kickstarter/replica_pools.png?branch=owningit)](https://travis-ci.org/kickstarter/replica_pools)
|
7
|
+
|
8
|
+
## Overview
|
9
|
+
|
10
|
+
ReplicaPools 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 leader connection.
|
11
|
+
|
12
|
+
ReplicaPools 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 leader, 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 leader.
|
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 leader.
|
19
|
+
* All other queries are also sent to the leader 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 leader. (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 leader.
|
29
|
+
* Blacklisting poorly performing replicas. This could cause load spikes on your leader. Whatever provisions your database.yml should make this choice.
|
30
|
+
|
31
|
+
## Installation and Setup
|
32
|
+
|
33
|
+
Add to your Gemfile:
|
34
|
+
|
35
|
+
gem 'replica_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
|
+
# Leader 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), ReplicaPools will create a default pool containing only leader. 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/replica_pools.rb` if you want to change config settings:
|
88
|
+
|
89
|
+
ReplicaPools.config.defaults_to_leader = true
|
90
|
+
|
91
|
+
## Usage
|
92
|
+
|
93
|
+
Toggle to next replica:
|
94
|
+
|
95
|
+
ReplicaPools.next_replica!
|
96
|
+
|
97
|
+
Specify a pool besides the default:
|
98
|
+
|
99
|
+
ReplicaPools.with_pool('other_pool') { #do stuff }
|
100
|
+
|
101
|
+
Specifically use the leader for a call:
|
102
|
+
|
103
|
+
ReplicaPools.with_leader { #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_replica
|
111
|
+
|
112
|
+
protected
|
113
|
+
|
114
|
+
def switch_to_next_replica
|
115
|
+
ReplicaPools.next_replica!
|
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
|
+
ReplicaPools.with_pool('special'){ yield }
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
### Replica Lag
|
134
|
+
|
135
|
+
By default, writes are sent to the leader and reads are sent to replicas. But replicas might lag behind the leader by seconds or even minutes. So if you write to leader during a request you probably want to read from leader in that request as well. You may even want to read from the leader 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_leader_for_updates
|
142
|
+
around_filter :use_leader_for_redirect #goes with above
|
143
|
+
|
144
|
+
def stick_to_leader_for_updates
|
145
|
+
if request.get?
|
146
|
+
yield
|
147
|
+
else
|
148
|
+
ReplicaPools.with_leader { yield }
|
149
|
+
session[:stick_to_leader] = 1
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def use_leader_for_redirect
|
154
|
+
if session[:stick_to_leader]
|
155
|
+
session[:stick_to_leader] = nil
|
156
|
+
ReplicaPools.with_leader { 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 leader/replica plugin:
|
195
|
+
|
196
|
+
* http://github.com/technoweenie/masochism
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'replica_pools/config'
|
3
|
+
require 'replica_pools/pool'
|
4
|
+
require 'replica_pools/pools'
|
5
|
+
require 'replica_pools/active_record_extensions'
|
6
|
+
require 'replica_pools/hijack'
|
7
|
+
require 'replica_pools/query_cache'
|
8
|
+
require 'replica_pools/connection_proxy'
|
9
|
+
|
10
|
+
require 'replica_pools/engine' if defined? Rails
|
11
|
+
ActiveRecord::Base.send :include, ReplicaPools::ActiveRecordExtensions
|
12
|
+
|
13
|
+
module ReplicaPools
|
14
|
+
class << self
|
15
|
+
|
16
|
+
def config
|
17
|
+
@config ||= ReplicaPools::Config.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def setup!
|
21
|
+
ConnectionProxy.generate_safe_delegations
|
22
|
+
|
23
|
+
ActiveRecord::Base.send(:extend, ReplicaPools::Hijack)
|
24
|
+
|
25
|
+
log :info, "Proxy loaded with: #{pools.keys.join(', ')}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def proxy
|
29
|
+
Thread.current[:replica_pools_proxy] ||= ReplicaPools::ConnectionProxy.new(
|
30
|
+
ActiveRecord::Base,
|
31
|
+
ReplicaPools.pools
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def current
|
36
|
+
proxy.current
|
37
|
+
end
|
38
|
+
|
39
|
+
def next_replica!
|
40
|
+
proxy.next_replica!
|
41
|
+
end
|
42
|
+
|
43
|
+
def with_pool(*a)
|
44
|
+
proxy.with_pool(*a){ yield }
|
45
|
+
end
|
46
|
+
|
47
|
+
def with_leader
|
48
|
+
proxy.with_leader{ yield }
|
49
|
+
end
|
50
|
+
|
51
|
+
def pools
|
52
|
+
Thread.current[:replica_pools] ||= ReplicaPools::Pools.new
|
53
|
+
end
|
54
|
+
|
55
|
+
def log(level, message)
|
56
|
+
logger.send(level, "[ReplicaPools] #{message}")
|
57
|
+
end
|
58
|
+
|
59
|
+
def logger
|
60
|
+
ActiveRecord::Base.logger
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ReplicaPools
|
2
|
+
module ActiveRecordExtensions
|
3
|
+
def self.included(base)
|
4
|
+
base.send :extend, ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
def reload(options = nil)
|
8
|
+
self.class.connection_proxy.with_leader { super }
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def connection_proxy
|
13
|
+
ReplicaPools.proxy
|
14
|
+
end
|
15
|
+
|
16
|
+
# Make sure transactions run on leader
|
17
|
+
# Even if they're initiated from ActiveRecord::Base
|
18
|
+
# (which doesn't have our hijack).
|
19
|
+
def transaction(options = {}, &block)
|
20
|
+
if self.connection.kind_of?(ConnectionProxy)
|
21
|
+
super
|
22
|
+
else
|
23
|
+
self.connection_proxy.with_leader { super }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ReplicaPools
|
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 leader unless wrapped in with_pool{}.
|
8
|
+
# When false, all safe queries will go to the current replica unless wrapped in with_leader{}.
|
9
|
+
# Defaults to false.
|
10
|
+
attr_accessor :defaults_to_leader
|
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_leader = false
|
19
|
+
@safe_methods = []
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'active_record/connection_adapters/abstract/query_cache'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module ReplicaPools
|
5
|
+
class ConnectionProxy
|
6
|
+
include ReplicaPools::QueryCache
|
7
|
+
|
8
|
+
attr_accessor :leader
|
9
|
+
attr_accessor :leader_depth, :current, :current_pool, :replica_pools
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def generate_safe_delegations
|
13
|
+
ReplicaPools.config.safe_methods.each do |method|
|
14
|
+
generate_safe_delegation(method) unless instance_methods.include?(method)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
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)
|
24
|
+
end
|
25
|
+
END
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(leader, pools)
|
30
|
+
@leader = leader
|
31
|
+
@replica_pools = pools
|
32
|
+
@leader_depth = 0
|
33
|
+
@current_pool = default_pool
|
34
|
+
|
35
|
+
if ReplicaPools.config.defaults_to_leader
|
36
|
+
@current = leader
|
37
|
+
else
|
38
|
+
@current = current_replica
|
39
|
+
end
|
40
|
+
|
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
|
45
|
+
end
|
46
|
+
|
47
|
+
def with_pool(pool_name = 'default')
|
48
|
+
last_conn, last_pool = self.current, self.current_pool
|
49
|
+
self.current_pool = replica_pools[pool_name.to_sym] || default_pool
|
50
|
+
self.current = current_replica unless within_leader_block?
|
51
|
+
yield
|
52
|
+
ensure
|
53
|
+
self.current_pool = last_pool
|
54
|
+
self.current = last_conn
|
55
|
+
end
|
56
|
+
|
57
|
+
def with_leader
|
58
|
+
last_conn = self.current
|
59
|
+
self.current = leader
|
60
|
+
self.leader_depth += 1
|
61
|
+
yield
|
62
|
+
ensure
|
63
|
+
self.leader_depth = [leader_depth - 1, 0].max
|
64
|
+
self.current = last_conn
|
65
|
+
end
|
66
|
+
|
67
|
+
def transaction(*args, &block)
|
68
|
+
with_leader { leader.transaction(*args, &block) }
|
69
|
+
end
|
70
|
+
|
71
|
+
def next_replica!
|
72
|
+
return if within_leader_block?
|
73
|
+
self.current = current_pool.next
|
74
|
+
end
|
75
|
+
|
76
|
+
def current_replica
|
77
|
+
current_pool.current
|
78
|
+
end
|
79
|
+
|
80
|
+
protected
|
81
|
+
|
82
|
+
def default_pool
|
83
|
+
replica_pools[:default] || replica_pools.values.first
|
84
|
+
end
|
85
|
+
|
86
|
+
# Proxies any unknown methods to leader.
|
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)
|
92
|
+
end
|
93
|
+
|
94
|
+
def within_leader_block?
|
95
|
+
leader_depth > 0
|
96
|
+
end
|
97
|
+
|
98
|
+
def generate_unsafe_delegation(method)
|
99
|
+
self.class_eval <<-END, __FILE__, __LINE__ + 1
|
100
|
+
def #{method}(*args, &block)
|
101
|
+
route_to(leader, :#{method}, *args, &block)
|
102
|
+
end
|
103
|
+
END
|
104
|
+
end
|
105
|
+
|
106
|
+
def route_to(conn, method, *args, &block)
|
107
|
+
conn.retrieve_connection.send(method, *args, &block)
|
108
|
+
rescue => e
|
109
|
+
ReplicaPools.log :error, "Error during ##{method}: #{e}"
|
110
|
+
log_proxy_state
|
111
|
+
|
112
|
+
current.retrieve_connection.verify! # may reconnect
|
113
|
+
raise e
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def log_proxy_state
|
119
|
+
ReplicaPools.log :error, "Current Connection: #{current}"
|
120
|
+
ReplicaPools.log :error, "Current Pool Name: #{current_pool.name}"
|
121
|
+
ReplicaPools.log :error, "Current Pool Members: #{current_pool.replicas}"
|
122
|
+
ReplicaPools.log :error, "leader Depth: #{leader_depth}"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rails/engine'
|
2
|
+
|
3
|
+
module ReplicaPools
|
4
|
+
class Engine < Rails::Engine
|
5
|
+
initializer 'replica_pools.defaults' do
|
6
|
+
ReplicaPools.config.environment = Rails.env
|
7
|
+
|
8
|
+
ReplicaPools.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
|
+
elsif ActiveRecord::VERSION::MAJOR == 4
|
16
|
+
[
|
17
|
+
:select_all, :select_one, :select_value, :select_values,
|
18
|
+
:select_rows, :select, :verify!, :raw_connection, :active?, :reconnect!,
|
19
|
+
:disconnect!, :reset_runtime, :log
|
20
|
+
]
|
21
|
+
else
|
22
|
+
warn "Unsupported ActiveRecord version #{ActiveRecord.version}. Please whitelist the safe methods."
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
config.after_initialize do
|
27
|
+
ReplicaPools.setup!
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ReplicaPools
|
2
|
+
# The hijack is added to ActiveRecord::Base but only applies to
|
3
|
+
# its descendants. The Base.connection is left in place.
|
4
|
+
module Hijack
|
5
|
+
def self.extended(base)
|
6
|
+
# hijack models that have already been loaded
|
7
|
+
base.send(:descendants).each do |child|
|
8
|
+
child.hijack_connection
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# hijack models that get loaded later
|
13
|
+
def inherited(child)
|
14
|
+
super
|
15
|
+
child.hijack_connection
|
16
|
+
end
|
17
|
+
|
18
|
+
def hijack_connection
|
19
|
+
class << self
|
20
|
+
alias_method :connection, :connection_proxy
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ReplicaPools
|
2
|
+
class Pool
|
3
|
+
attr_reader :name, :replicas, :current, :size
|
4
|
+
|
5
|
+
def initialize(name, connections)
|
6
|
+
@name = name
|
7
|
+
@replicas = connections
|
8
|
+
@size = connections.size
|
9
|
+
self.reset
|
10
|
+
end
|
11
|
+
|
12
|
+
def reset
|
13
|
+
@cycle = replicas.cycle
|
14
|
+
self.next
|
15
|
+
end
|
16
|
+
|
17
|
+
def next
|
18
|
+
@current = @cycle.next
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module ReplicaPools
|
4
|
+
class Pools < ::SimpleDelegator
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
pools = {}
|
9
|
+
pool_configurations.group_by{|_, name, _| name }.each do |name, set|
|
10
|
+
pools[name.to_sym] = ReplicaPools::Pool.new(
|
11
|
+
name,
|
12
|
+
set.map{ |conn_name, _, replica_name|
|
13
|
+
connection_class(name, replica_name, conn_name)
|
14
|
+
}
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
if pools.empty?
|
19
|
+
ReplicaPools.log :info, "No pools found for #{ReplicaPools.config.environment}. Loading a default pool with leader instead."
|
20
|
+
pools[:default] = ReplicaPools::Pool.new('default', [ActiveRecord::Base])
|
21
|
+
end
|
22
|
+
|
23
|
+
super pools
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# finds valid pool configs
|
29
|
+
def pool_configurations
|
30
|
+
ActiveRecord::Base.configurations.map do |name, config|
|
31
|
+
next unless name.to_s =~ /#{ReplicaPools.config.environment}_pool_(.*)_name_(.*)/
|
32
|
+
[name, $1, $2]
|
33
|
+
end.compact
|
34
|
+
end
|
35
|
+
|
36
|
+
# generates a unique ActiveRecord::Base subclass for a single replica
|
37
|
+
def connection_class(pool_name, replica_name, connection_name)
|
38
|
+
class_name = "#{pool_name.camelize}#{replica_name.camelize}"
|
39
|
+
|
40
|
+
ReplicaPools.module_eval %Q{
|
41
|
+
class #{class_name} < ActiveRecord::Base
|
42
|
+
self.abstract_class = true
|
43
|
+
establish_connection :#{connection_name}
|
44
|
+
def self.connection_config
|
45
|
+
configurations[#{connection_name.to_s.inspect}]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
}, __FILE__, __LINE__
|
49
|
+
ReplicaPools.const_get(class_name)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module ReplicaPools
|
2
|
+
# duck-types with ActiveRecord::ConnectionAdapters::QueryCache
|
3
|
+
# but relies on ActiveRecord::Base.query_cache for state so we
|
4
|
+
# don't fragment the cache across multiple connections
|
5
|
+
#
|
6
|
+
# we could use more of ActiveRecord's QueryCache if it only
|
7
|
+
# used accessors for its internal ivars.
|
8
|
+
module QueryCache
|
9
|
+
query_cache_methods = ActiveRecord::ConnectionAdapters::QueryCache.instance_methods(false)
|
10
|
+
|
11
|
+
# these methods can all use the leader connection
|
12
|
+
(query_cache_methods - [:select_all]).each do |method_name|
|
13
|
+
module_eval <<-END, __FILE__, __LINE__ + 1
|
14
|
+
def #{method_name}(*a, &b)
|
15
|
+
ActiveRecord::Base.connection.#{method_name}(*a, &b)
|
16
|
+
end
|
17
|
+
END
|
18
|
+
end
|
19
|
+
|
20
|
+
# select_all is trickier. it needs to use the leader
|
21
|
+
# connection for cache logic, but ultimately pass its query
|
22
|
+
# through to whatever connection is current.
|
23
|
+
def select_all(arel, name = nil, binds = [])
|
24
|
+
if query_cache_enabled && !locked?(arel)
|
25
|
+
sql = to_sql(arel, binds)
|
26
|
+
cache_sql(sql, binds) { route_to(current, :select_all, sql, name, binds) }
|
27
|
+
else
|
28
|
+
route_to(current, :select_all, arel, name, binds)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# these can use the unsafe delegation built into ConnectionProxy
|
33
|
+
# [:insert, :update, :delete]
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
require_relative 'config/test_model'
|
3
|
+
|
4
|
+
describe ReplicaPools do
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
@sql = 'SELECT NOW()'
|
8
|
+
|
9
|
+
@proxy = ReplicaPools.proxy
|
10
|
+
@leader = @proxy.leader.retrieve_connection
|
11
|
+
|
12
|
+
reset_proxy(@proxy)
|
13
|
+
create_replica_aliases(@proxy)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'AR::B should respond to #connection_proxy' do
|
17
|
+
ActiveRecord::Base.should respond_to(:connection_proxy)
|
18
|
+
ActiveRecord::Base.connection_proxy.should be_kind_of(ReplicaPools::ConnectionProxy)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'TestModel#connection should return an instance of ReplicaPools::ConnectionProxy' do
|
22
|
+
TestModel.connection.should be_kind_of(ReplicaPools::ConnectionProxy)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should generate classes for each entry in the database.yml" do
|
26
|
+
defined?(ReplicaPools::DefaultDb1).should_not be_nil
|
27
|
+
defined?(ReplicaPools::DefaultDb2).should_not be_nil
|
28
|
+
defined?(ReplicaPools::SecondaryDb1).should_not be_nil
|
29
|
+
defined?(ReplicaPools::SecondaryDb2).should_not be_nil
|
30
|
+
defined?(ReplicaPools::SecondaryDb3).should_not be_nil
|
31
|
+
end
|
32
|
+
|
33
|
+
context "with_leader" do
|
34
|
+
it 'should revert to previous replica connection' do
|
35
|
+
@proxy.current = @proxy.current_replica
|
36
|
+
@proxy.with_leader do
|
37
|
+
@proxy.current.should equal(@proxy.leader)
|
38
|
+
end
|
39
|
+
@proxy.current.name.should eq('ReplicaPools::DefaultDb1')
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should revert to previous leader connection' do
|
43
|
+
@proxy.current = @proxy.leader
|
44
|
+
@proxy.with_leader do
|
45
|
+
@proxy.current.should equal(@proxy.leader)
|
46
|
+
end
|
47
|
+
@proxy.current.should equal(@proxy.leader)
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should know when in block' do
|
51
|
+
@proxy.send(:within_leader_block?).should_not be
|
52
|
+
@proxy.with_leader do
|
53
|
+
@proxy.send(:within_leader_block?).should be
|
54
|
+
end
|
55
|
+
@proxy.send(:within_leader_block?).should_not be
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context "transaction" do
|
60
|
+
it 'should send all to leader' do
|
61
|
+
@leader.should_receive(:select_all).exactly(1)
|
62
|
+
@default_replica1.should_receive(:select_all).exactly(0)
|
63
|
+
|
64
|
+
TestModel.transaction do
|
65
|
+
@proxy.select_all(@sql)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'should send all to leader even if transactions begins on AR::Base' do
|
70
|
+
@leader.should_receive(:select_all).exactly(1)
|
71
|
+
@default_replica1.should_receive(:select_all).exactly(0)
|
72
|
+
|
73
|
+
ActiveRecord::Base.transaction do
|
74
|
+
@proxy.select_all(@sql)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'should perform transactions on the leader, and selects outside of transaction on the replica' do
|
80
|
+
@default_replica1.should_receive(:select_all).exactly(2) # before and after the transaction go to replicas
|
81
|
+
@leader.should_receive(:select_all).exactly(5)
|
82
|
+
@proxy.select_all(@sql)
|
83
|
+
ActiveRecord::Base.transaction do
|
84
|
+
5.times {@proxy.select_all(@sql)}
|
85
|
+
end
|
86
|
+
@proxy.select_all(@sql)
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'should not switch replicas automatically on selects' do
|
90
|
+
@default_replica1.should_receive(:select_one).exactly(6)
|
91
|
+
@default_replica2.should_receive(:select_one).exactly(0)
|
92
|
+
6.times { @proxy.select_one(@sql) }
|
93
|
+
end
|
94
|
+
|
95
|
+
context "next_replica!" do
|
96
|
+
it 'should switch to the next replica' do
|
97
|
+
@default_replica1.should_receive(:select_one).exactly(1)
|
98
|
+
@default_replica2.should_receive(:select_one).exactly(1)
|
99
|
+
|
100
|
+
@proxy.select_one(@sql)
|
101
|
+
@proxy.next_replica!
|
102
|
+
@proxy.select_one(@sql)
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'should not switch when in a with_leader-block' do
|
106
|
+
@leader.should_receive(:select_one).exactly(2)
|
107
|
+
@default_replica1.should_not_receive(:select_one)
|
108
|
+
@default_replica2.should_not_receive(:select_one)
|
109
|
+
|
110
|
+
@proxy.with_leader do
|
111
|
+
@proxy.select_one(@sql)
|
112
|
+
@proxy.next_replica!
|
113
|
+
@proxy.select_one(@sql)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'should send dangerous methods to the leader' do
|
119
|
+
meths = [:insert, :update, :delete, :execute]
|
120
|
+
meths.each do |meth|
|
121
|
+
@default_replica1.stub(meth).and_raise(RuntimeError)
|
122
|
+
@leader.should_receive(meth).and_return(true)
|
123
|
+
@proxy.send(meth, @sql)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should not allow leader depth to get below 0" do
|
128
|
+
@proxy.instance_variable_set("@leader_depth", -500)
|
129
|
+
@proxy.instance_variable_get("@leader_depth").should == -500
|
130
|
+
@proxy.with_leader {@sql}
|
131
|
+
@proxy.instance_variable_get("@leader_depth").should == 0
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'should pre-generate safe methods' do
|
135
|
+
@proxy.should respond_to(:select_value)
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'should dynamically generate unsafe methods' do
|
139
|
+
@leader.should_receive(:unsafe).and_return(true)
|
140
|
+
|
141
|
+
@proxy.should_not respond_to(:unsafe)
|
142
|
+
@proxy.unsafe(@sql)
|
143
|
+
@proxy.should respond_to(:unsafe)
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'should not replay errors on leader' do
|
147
|
+
@default_replica1.should_receive(:select_all).once.and_raise(ArgumentError.new('random message'))
|
148
|
+
@default_replica2.should_not_receive(:select_all)
|
149
|
+
@leader.should_not_receive(:select_all)
|
150
|
+
lambda { @proxy.select_all(@sql) }.should raise_error(ArgumentError)
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'should reload models from the leader' do
|
154
|
+
foo = TestModel.create!
|
155
|
+
@leader.should_receive(:select_all).and_return(ActiveRecord::Result.new(["id"], ["1"]))
|
156
|
+
@default_replica1.should_not_receive(:select_all)
|
157
|
+
@default_replica2.should_not_receive(:select_all)
|
158
|
+
foo.reload
|
159
|
+
end
|
160
|
+
|
161
|
+
context "with_pool" do
|
162
|
+
|
163
|
+
it "should switch to the named pool" do
|
164
|
+
@proxy.with_pool('secondary') do
|
165
|
+
@proxy.current_pool.name.should eq('secondary')
|
166
|
+
@proxy.current.name.should eq('ReplicaPools::SecondaryDb1')
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
it "should switch to default pool if an unknown pool is specified" do
|
171
|
+
@proxy.with_pool('unknown') do
|
172
|
+
@proxy.current_pool.name.should eq('default')
|
173
|
+
@proxy.current.name.should eq('ReplicaPools::DefaultDb1')
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
it "should switch to default pool if no pool is specified" do
|
178
|
+
@proxy.with_pool do
|
179
|
+
@proxy.current_pool.name.should eq('default')
|
180
|
+
@proxy.current.name.should eq('ReplicaPools::DefaultDb1')
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
it "should cycle replicas only within the pool" do
|
185
|
+
@proxy.with_pool('secondary') do
|
186
|
+
@proxy.current.name.should eq('ReplicaPools::SecondaryDb1')
|
187
|
+
@proxy.next_replica!
|
188
|
+
@proxy.current.name.should eq('ReplicaPools::SecondaryDb2')
|
189
|
+
@proxy.next_replica!
|
190
|
+
@proxy.current.name.should eq('ReplicaPools::SecondaryDb3')
|
191
|
+
@proxy.next_replica!
|
192
|
+
@proxy.current.name.should eq('ReplicaPools::SecondaryDb1')
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
it "should allow switching back to leader" do
|
197
|
+
@proxy.with_pool('secondary') do
|
198
|
+
@proxy.current.name.should eq('ReplicaPools::SecondaryDb1')
|
199
|
+
@proxy.with_leader do
|
200
|
+
@proxy.current.name.should eq('ActiveRecord::Base')
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
it "should not switch to pool when nested inside with_leader" do
|
206
|
+
@proxy.current.name.should eq('ReplicaPools::DefaultDb1')
|
207
|
+
@proxy.with_leader do
|
208
|
+
@proxy.with_pool('secondary') do
|
209
|
+
@proxy.current.name.should eq('ActiveRecord::Base')
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
it "should switch back to previous pool and replica" do
|
215
|
+
@proxy.next_replica!
|
216
|
+
|
217
|
+
@proxy.current_pool.name.should eq('default')
|
218
|
+
@proxy.current.name.should eq('ReplicaPools::DefaultDb2')
|
219
|
+
|
220
|
+
@proxy.with_pool('secondary') do
|
221
|
+
@proxy.current_pool.name.should eq('secondary')
|
222
|
+
@proxy.current.name.should eq('ReplicaPools::SecondaryDb1')
|
223
|
+
|
224
|
+
@proxy.with_pool('default') do
|
225
|
+
@proxy.current_pool.name.should eq('default')
|
226
|
+
@proxy.current.name.should eq('ReplicaPools::DefaultDb2')
|
227
|
+
end
|
228
|
+
|
229
|
+
@proxy.current_pool.name.should eq('secondary')
|
230
|
+
@proxy.current.name.should eq('ReplicaPools::SecondaryDb1')
|
231
|
+
end
|
232
|
+
|
233
|
+
@proxy.current_pool.name.should eq('default')
|
234
|
+
@proxy.current.name.should eq('ReplicaPools::DefaultDb2')
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
data/spec/pool_spec.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
describe ReplicaPools::Pool do
|
4
|
+
|
5
|
+
context "Multiple replicas" do
|
6
|
+
before do
|
7
|
+
@replicas = ["db1", "db2", "db3"]
|
8
|
+
@replica_pool = ReplicaPools::Pool.new("name", @replicas.clone)
|
9
|
+
end
|
10
|
+
|
11
|
+
specify {@replica_pool.size.should == 3}
|
12
|
+
|
13
|
+
it "should return items in a round robin fashion" do
|
14
|
+
@replica_pool.current.should == @replicas[0]
|
15
|
+
@replica_pool.next.should == @replicas[1]
|
16
|
+
@replica_pool.next.should == @replicas[2]
|
17
|
+
@replica_pool.next.should == @replicas[0]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context "Single replica" do
|
22
|
+
before do
|
23
|
+
@replicas = ["db1"]
|
24
|
+
@replica_pool = ReplicaPools::Pool.new("name", @replicas.clone)
|
25
|
+
end
|
26
|
+
|
27
|
+
specify {@replica_pool.size.should == 1}
|
28
|
+
|
29
|
+
it "should return items in a round robin fashion" do
|
30
|
+
@replica_pool.current.should == @replicas[0]
|
31
|
+
@replica_pool.next.should == @replicas[0]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require_relative 'spec_helper'
|
3
|
+
|
4
|
+
describe ReplicaPools::QueryCache do
|
5
|
+
before(:each) do
|
6
|
+
@sql = 'SELECT NOW()'
|
7
|
+
|
8
|
+
@proxy = ReplicaPools.proxy
|
9
|
+
@leader = @proxy.leader.retrieve_connection
|
10
|
+
|
11
|
+
@leader.clear_query_cache
|
12
|
+
|
13
|
+
reset_proxy(@proxy)
|
14
|
+
create_replica_aliases(@proxy)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should cache queries using select_all' do
|
18
|
+
ActiveRecord::Base.cache do
|
19
|
+
# next_replica will be called and switch to the replicaDatabase2
|
20
|
+
@default_replica1.should_receive(:select_all).exactly(1).and_return([])
|
21
|
+
@default_replica2.should_not_receive(:select_all)
|
22
|
+
@leader.should_not_receive(:select_all)
|
23
|
+
3.times { @proxy.select_all(@sql) }
|
24
|
+
@leader.query_cache.keys.size.should == 1
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should invalidate the cache on insert, delete and update' do
|
29
|
+
ActiveRecord::Base.cache do
|
30
|
+
meths = [:insert, :update, :delete, :insert, :update]
|
31
|
+
meths.each do |meth|
|
32
|
+
@leader.should_receive("exec_#{meth}").and_return(true)
|
33
|
+
end
|
34
|
+
|
35
|
+
@default_replica1.should_receive(:select_all).exactly(5).and_return([])
|
36
|
+
@default_replica2.should_receive(:select_all).exactly(0)
|
37
|
+
5.times do |i|
|
38
|
+
@proxy.select_all(@sql)
|
39
|
+
@proxy.select_all(@sql)
|
40
|
+
@leader.query_cache.keys.size.should == 1
|
41
|
+
@proxy.send(meths[i], '')
|
42
|
+
@leader.query_cache.keys.size.should == 0
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "using querycache middleware" do
|
48
|
+
it 'should cache queries using select_all' do
|
49
|
+
mw = ActiveRecord::QueryCache.new lambda { |env|
|
50
|
+
@default_replica1.should_receive(:select_all).exactly(1).and_return([])
|
51
|
+
@default_replica2.should_not_receive(:select_all)
|
52
|
+
@leader.should_not_receive(:select_all)
|
53
|
+
3.times { @proxy.select_all(@sql) }
|
54
|
+
@proxy.next_replica!
|
55
|
+
3.times { @proxy.select_all(@sql) }
|
56
|
+
@proxy.next_replica!
|
57
|
+
3.times { @proxy.select_all(@sql)}
|
58
|
+
@leader.query_cache.keys.size.should == 1
|
59
|
+
[200, {}, nil]
|
60
|
+
}
|
61
|
+
mw.call({})
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should invalidate the cache on insert, delete and update' do
|
65
|
+
mw = ActiveRecord::QueryCache.new lambda { |env|
|
66
|
+
meths = [:insert, :update, :delete, :insert, :update]
|
67
|
+
meths.each do |meth|
|
68
|
+
@leader.should_receive("exec_#{meth}").and_return(true)
|
69
|
+
end
|
70
|
+
|
71
|
+
@default_replica1.should_receive(:select_all).exactly(5).and_return([])
|
72
|
+
@default_replica2.should_receive(:select_all).exactly(0)
|
73
|
+
5.times do |i|
|
74
|
+
@proxy.select_all(@sql)
|
75
|
+
@proxy.select_all(@sql)
|
76
|
+
@leader.query_cache.keys.size.should == 1
|
77
|
+
@proxy.send(meths[i], '')
|
78
|
+
@leader.query_cache.keys.size.should == 0
|
79
|
+
end
|
80
|
+
[200, {}, nil]
|
81
|
+
}
|
82
|
+
mw.call({})
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
|
3
|
+
describe ReplicaPools do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
ReplicaPools.pools.each{|_, pool| pool.reset }
|
7
|
+
@proxy = ReplicaPools.proxy
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should delegate next_replica! call to connection proxy' do
|
11
|
+
@proxy.should_receive(:next_replica!).exactly(1)
|
12
|
+
ReplicaPools.next_replica!
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should delegate with_pool call to connection proxy' do
|
16
|
+
@proxy.should_receive(:with_pool).exactly(1)
|
17
|
+
ReplicaPools.with_pool('test')
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should delegate with_leader call to connection proxy' do
|
21
|
+
@proxy.should_receive(:with_leader).exactly(1)
|
22
|
+
ReplicaPools.with_leader
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should delegate current call to connection proxy' do
|
26
|
+
@proxy.should_receive(:current).exactly(1)
|
27
|
+
ReplicaPools.current
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module Rails
|
6
|
+
def self.env
|
7
|
+
ActiveSupport::StringInquirer.new("test")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'active_record'
|
12
|
+
spec_dir = File.dirname(__FILE__)
|
13
|
+
ActiveRecord::Base.logger = Logger.new(spec_dir + "/debug.log")
|
14
|
+
ActiveRecord::Base.configurations = YAML::load(File.open(spec_dir + '/config/database.yml'))
|
15
|
+
|
16
|
+
ActiveRecord::Base.establish_connection :test
|
17
|
+
ActiveRecord::Migration.verbose = false
|
18
|
+
ActiveRecord::Migration.create_table(:test_models, :force => true) {}
|
19
|
+
|
20
|
+
require 'replica_pools'
|
21
|
+
ReplicaPools::Engine.initializers.each(&:run)
|
22
|
+
ActiveSupport.run_load_hooks(:after_initialize, ReplicaPools::Engine)
|
23
|
+
|
24
|
+
module ReplicaPools::Testing
|
25
|
+
# Creates aliases for the replica connections in each pool
|
26
|
+
# for easy reference in tests.
|
27
|
+
def create_replica_aliases(proxy)
|
28
|
+
proxy.replica_pools.each do |name, pool|
|
29
|
+
pool.replicas.each.with_index do |replica, i|
|
30
|
+
instance_variable_set("@#{name}_replica#{i + 1}", replica.retrieve_connection)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def reset_proxy(proxy)
|
36
|
+
proxy.replica_pools.each{|_, pool| pool.reset }
|
37
|
+
proxy.current_pool = proxy.replica_pools[:default]
|
38
|
+
proxy.current = proxy.current_replica
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
RSpec.configure do |c|
|
43
|
+
c.include ReplicaPools::Testing
|
44
|
+
end
|
metadata
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: replica_pools
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.3.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dan Drabik
|
8
|
+
- Lance Ivy
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2015-02-12 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activerecord
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - '>='
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: 3.2.12
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - '>='
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: 3.2.12
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: mysql2
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ~>
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 0.3.11
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ~>
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: 0.3.11
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: rack
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: rspec
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: rake
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - '>='
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: rails
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - '>='
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - '>='
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
description: Connection proxy for ActiveRecord for leader / replica setups.
|
99
|
+
email: dan@kickstarter.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- lib/replica_pools/active_record_extensions.rb
|
105
|
+
- lib/replica_pools/config.rb
|
106
|
+
- lib/replica_pools/connection_proxy.rb
|
107
|
+
- lib/replica_pools/engine.rb
|
108
|
+
- lib/replica_pools/hijack.rb
|
109
|
+
- lib/replica_pools/pool.rb
|
110
|
+
- lib/replica_pools/pools.rb
|
111
|
+
- lib/replica_pools/query_cache.rb
|
112
|
+
- lib/replica_pools/version.rb
|
113
|
+
- lib/replica_pools.rb
|
114
|
+
- LICENSE
|
115
|
+
- README.md
|
116
|
+
- spec/config/test_model.rb
|
117
|
+
- spec/connection_proxy_spec.rb
|
118
|
+
- spec/pool_spec.rb
|
119
|
+
- spec/query_cache_spec.rb
|
120
|
+
- spec/slave_pools_spec.rb
|
121
|
+
- spec/spec_helper.rb
|
122
|
+
homepage: https://github.com/kickstarter/replica_pools
|
123
|
+
licenses:
|
124
|
+
- MIT
|
125
|
+
metadata: {}
|
126
|
+
post_install_message:
|
127
|
+
rdoc_options: []
|
128
|
+
require_paths:
|
129
|
+
- lib
|
130
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
131
|
+
requirements:
|
132
|
+
- - '>='
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - '>='
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '1.2'
|
140
|
+
requirements: []
|
141
|
+
rubyforge_project:
|
142
|
+
rubygems_version: 2.0.14
|
143
|
+
signing_key:
|
144
|
+
specification_version: 4
|
145
|
+
summary: Connection proxy for ActiveRecord for leader / replica setups.
|
146
|
+
test_files:
|
147
|
+
- spec/config/test_model.rb
|
148
|
+
- spec/connection_proxy_spec.rb
|
149
|
+
- spec/pool_spec.rb
|
150
|
+
- spec/query_cache_spec.rb
|
151
|
+
- spec/slave_pools_spec.rb
|
152
|
+
- spec/spec_helper.rb
|
153
|
+
has_rdoc:
|