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.
@@ -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 SlavePoolsModule
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
- send(observed_method, object, &block) if respond_to?(observed_method) && !disabled_for?(object)
12
+ update_without_masterdb(observed_method, object)
13
13
  end
14
14
  else
15
- send(observed_method, object, &block) if respond_to?(observed_method) && !disabled_for?(object)
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
@@ -0,0 +1,3 @@
1
+ module SlavePools
2
+ VERSION = "1.0.0.rc1"
3
+ end
data/lib/slave_pools.rb CHANGED
@@ -1,43 +1,71 @@
1
1
  require 'active_record'
2
- require 'slave_pools/slave_pool'
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/query_cache_compat'
8
+ require 'slave_pools/query_cache'
6
9
  require 'slave_pools/connection_proxy'
7
10
 
8
- #wrapper class to make the calls more succinct
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
- class SlavePools
15
+ module SlavePools
16
+ class << self
11
17
 
12
- def self.setup!
13
- SlavePoolsModule::ConnectionProxy.setup!
14
- end
18
+ def config
19
+ @config ||= SlavePools::Config.new
20
+ end
15
21
 
16
- def self.active?
17
- ActiveRecord::Base.respond_to?('connection_proxy')
18
- end
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
- def self.next_slave!
21
- ActiveRecord::Base.connection_proxy.next_slave! if active?
22
- end
28
+ ConnectionProxy.generate_safe_delegations
29
+
30
+ ActiveRecord::Base.send(:extend, SlavePools::Hijack)
31
+ ActiveRecord::Base.connection_proxy = self.proxy
23
32
 
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
33
+ log :info, "Proxy loaded with: #{pools.keys.join(', ')}"
29
34
  end
30
- end
31
35
 
32
- def self.with_master
33
- if active?
34
- ActiveRecord::Base.connection_proxy.with_master { yield }
35
- else
36
- yield
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
- def self.current
41
- ActiveRecord::Base.connection_proxy.current if active?
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
+