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.
@@ -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
+