slave_pools 0.1.2 → 1.0.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|