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
@@ -0,0 +1,24 @@
|
|
1
|
+
module SlavePools
|
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
|
@@ -1,19 +1,19 @@
|
|
1
|
-
module
|
1
|
+
module SlavePools
|
2
2
|
module ObserverExtensions
|
3
3
|
def self.included(base)
|
4
4
|
base.alias_method_chain :update, :masterdb
|
5
5
|
end
|
6
|
-
|
6
|
+
|
7
7
|
# Send observed_method(object) if the method exists.
|
8
8
|
# currently replicating the update method instead of using the aliased method call to update_without_master
|
9
9
|
def update_with_masterdb(observed_method, object, &block) #:nodoc:
|
10
10
|
if object.class.connection.respond_to?(:with_master)
|
11
11
|
object.class.connection.with_master do
|
12
|
-
|
12
|
+
update_without_masterdb(observed_method, object)
|
13
13
|
end
|
14
14
|
else
|
15
|
-
|
15
|
+
update_without_masterdb(observed_method, object)
|
16
16
|
end
|
17
17
|
end
|
18
18
|
end
|
19
|
-
end
|
19
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module SlavePools
|
2
|
+
class Pool
|
3
|
+
attr_reader :name, :slaves, :current, :size
|
4
|
+
|
5
|
+
def initialize(name, connections)
|
6
|
+
@name = name
|
7
|
+
@slaves = connections
|
8
|
+
@size = connections.size
|
9
|
+
self.reset
|
10
|
+
end
|
11
|
+
|
12
|
+
def reset
|
13
|
+
@cycle = slaves.cycle
|
14
|
+
self.next
|
15
|
+
end
|
16
|
+
|
17
|
+
def next
|
18
|
+
@current = @cycle.next
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module SlavePools
|
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] = SlavePools::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
|
+
super pools
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# finds valid pool configs
|
24
|
+
def pool_configurations
|
25
|
+
ActiveRecord::Base.configurations.map do |name, config|
|
26
|
+
next unless name.to_s =~ /#{SlavePools.config.environment}_pool_(.*)_name_(.*)/
|
27
|
+
next unless connection_valid?(config)
|
28
|
+
[name, $1, $2]
|
29
|
+
end.compact
|
30
|
+
end
|
31
|
+
|
32
|
+
# generates a unique ActiveRecord::Base subclass for a single replica
|
33
|
+
def connection_class(pool_name, replica_name, connection_name)
|
34
|
+
class_name = "#{pool_name.camelize}#{replica_name.camelize}"
|
35
|
+
|
36
|
+
SlavePools.module_eval %Q{
|
37
|
+
class #{class_name} < ActiveRecord::Base
|
38
|
+
self.abstract_class = true
|
39
|
+
establish_connection :#{connection_name}
|
40
|
+
def self.connection_config
|
41
|
+
configurations[#{connection_name.to_s.inspect}]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
}, __FILE__, __LINE__
|
45
|
+
SlavePools.const_get(class_name)
|
46
|
+
end
|
47
|
+
|
48
|
+
# tests a connection to be sure it's configured
|
49
|
+
def connection_valid?(db_config)
|
50
|
+
ActiveRecord::Base.establish_connection(db_config)
|
51
|
+
return ActiveRecord::Base.connection && ActiveRecord::Base.connected?
|
52
|
+
rescue => e
|
53
|
+
SlavePools.log :error, "Could not connect to #{db_config.inspect}"
|
54
|
+
SlavePools.log :error, e.to_s
|
55
|
+
return false
|
56
|
+
ensure
|
57
|
+
ActiveRecord::Base.establish_connection(SlavePools.config.environment)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module SlavePools
|
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 master 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 master
|
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
|
data/lib/slave_pools.rb
CHANGED
@@ -1,43 +1,71 @@
|
|
1
1
|
require 'active_record'
|
2
|
-
require 'slave_pools/
|
2
|
+
require 'slave_pools/config'
|
3
|
+
require 'slave_pools/pool'
|
4
|
+
require 'slave_pools/pools'
|
3
5
|
require 'slave_pools/active_record_extensions'
|
6
|
+
require 'slave_pools/hijack'
|
4
7
|
require 'slave_pools/observer_extensions'
|
5
|
-
require 'slave_pools/
|
8
|
+
require 'slave_pools/query_cache'
|
6
9
|
require 'slave_pools/connection_proxy'
|
7
10
|
|
8
|
-
|
11
|
+
require 'slave_pools/engine' if defined? Rails
|
12
|
+
ActiveRecord::Observer.send :include, SlavePools::ObserverExtensions
|
13
|
+
ActiveRecord::Base.send :include, SlavePools::ActiveRecordExtensions
|
9
14
|
|
10
|
-
|
15
|
+
module SlavePools
|
16
|
+
class << self
|
11
17
|
|
12
|
-
|
13
|
-
|
14
|
-
|
18
|
+
def config
|
19
|
+
@config ||= SlavePools::Config.new
|
20
|
+
end
|
15
21
|
|
16
|
-
|
17
|
-
|
18
|
-
|
22
|
+
def setup!
|
23
|
+
if pools.empty?
|
24
|
+
log :info, "No pools found for #{config.environment}. Loading a default pool with master instead."
|
25
|
+
pools['default'] = SlavePools::Pool.new('default', [ActiveRecord::Base])
|
26
|
+
end
|
19
27
|
|
20
|
-
|
21
|
-
|
22
|
-
|
28
|
+
ConnectionProxy.generate_safe_delegations
|
29
|
+
|
30
|
+
ActiveRecord::Base.send(:extend, SlavePools::Hijack)
|
31
|
+
ActiveRecord::Base.connection_proxy = self.proxy
|
23
32
|
|
24
|
-
|
25
|
-
if active?
|
26
|
-
ActiveRecord::Base.connection_proxy.with_pool(pool_name) { yield }
|
27
|
-
else
|
28
|
-
yield
|
33
|
+
log :info, "Proxy loaded with: #{pools.keys.join(', ')}"
|
29
34
|
end
|
30
|
-
end
|
31
35
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
36
|
+
def proxy
|
37
|
+
Thread.current[:slave_pools_proxy] ||= SlavePools::ConnectionProxy.new(
|
38
|
+
ActiveRecord::Base,
|
39
|
+
SlavePools.pools
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
def current
|
44
|
+
proxy.current
|
45
|
+
end
|
46
|
+
|
47
|
+
def next_slave!
|
48
|
+
proxy.next_slave!
|
49
|
+
end
|
50
|
+
|
51
|
+
def with_pool(*a)
|
52
|
+
proxy.with_pool(*a){ yield }
|
53
|
+
end
|
54
|
+
|
55
|
+
def with_master
|
56
|
+
proxy.with_master{ yield }
|
37
57
|
end
|
38
|
-
end
|
39
58
|
|
40
|
-
|
41
|
-
|
59
|
+
def pools
|
60
|
+
Thread.current[:slave_pools] ||= SlavePools::Pools.new
|
61
|
+
end
|
62
|
+
|
63
|
+
def log(level, message)
|
64
|
+
logger.send(level, "[SlavePools] #{message}")
|
65
|
+
end
|
66
|
+
|
67
|
+
def logger
|
68
|
+
ActiveRecord::Base.logger
|
69
|
+
end
|
42
70
|
end
|
43
71
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class TestModel < ActiveRecord::Base
|
2
|
+
has_many :test_subs
|
3
|
+
end
|
4
|
+
|
5
|
+
class TestSub < ActiveRecord::Base
|
6
|
+
belongs_to :test_model
|
7
|
+
end
|
8
|
+
|
9
|
+
class TestModelObserver < ActiveRecord::Observer
|
10
|
+
|
11
|
+
def after_create(test_model)
|
12
|
+
ActiveRecord::Base.logger.info "in observer"
|
13
|
+
TestSub.create(:test_model=>test_model)
|
14
|
+
TestModel.first
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
|