replica 1.1.0 → 1.2.0
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.
- data/VERSION +1 -1
- data/lib/replica.rb +5 -143
- data/lib/replica/association_collection.rb +38 -0
- data/lib/replica/base.rb +120 -0
- data/lib/replica/connection_pool.rb +23 -0
- data/test/models.rb +2 -0
- data/test/replica_test.rb +20 -1
- metadata +5 -2
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.2.0
|
data/lib/replica.rb
CHANGED
@@ -1,145 +1,7 @@
|
|
1
1
|
require 'activerecord'
|
2
|
+
require 'replica/base'
|
3
|
+
require 'replica/association_collection'
|
4
|
+
require 'replica/connection_pool'
|
2
5
|
|
3
|
-
|
4
|
-
|
5
|
-
module Replica
|
6
|
-
|
7
|
-
# Executes queries using the slave database. Fails over to master if no slave is found.
|
8
|
-
# if you want to execute a block of code on the slave you can go:
|
9
|
-
# Account.with_slave do
|
10
|
-
# Account.first
|
11
|
-
# end
|
12
|
-
# the first account will be found on the slave DB
|
13
|
-
#
|
14
|
-
# For one-liners you can simply do
|
15
|
-
# Account.with_slave.first
|
16
|
-
#
|
17
|
-
# this is the same as:
|
18
|
-
# Account.with_replica(:slave) do
|
19
|
-
# Account.first
|
20
|
-
# end
|
21
|
-
def with_slave(&block)
|
22
|
-
with_replica(:slave, &block)
|
23
|
-
end
|
24
|
-
|
25
|
-
# See with_slave
|
26
|
-
def with_master(&block)
|
27
|
-
with_replica(nil, &block)
|
28
|
-
end
|
29
|
-
|
30
|
-
def with_slave_if(condition, &block)
|
31
|
-
condition ? with_slave(&block) : with_master(&block)
|
32
|
-
end
|
33
|
-
|
34
|
-
def with_slave_unless(condition, &block)
|
35
|
-
with_slave_if(!condition, &block)
|
36
|
-
end
|
37
|
-
|
38
|
-
# Name of the connection pool. Used by ConnectionHandler to retrieve the current connection pool.
|
39
|
-
def connection_pool_name # :nodoc:
|
40
|
-
replica = current_replica_name
|
41
|
-
if replica
|
42
|
-
"#{name}_#{replica}"
|
43
|
-
elsif self == ActiveRecord::Base
|
44
|
-
name
|
45
|
-
else
|
46
|
-
superclass.connection_pool_name
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
# Specify which database to use.
|
51
|
-
#
|
52
|
-
# Example:
|
53
|
-
# database.yml
|
54
|
-
# test_slave:
|
55
|
-
# adapter: mysql
|
56
|
-
# ...
|
57
|
-
#
|
58
|
-
# Account.with_replica(:slave) { Account.count }
|
59
|
-
# Account.with_replica(:slave).count
|
60
|
-
#
|
61
|
-
def with_replica(replica_name, &block)
|
62
|
-
if block_given?
|
63
|
-
with_replica_block(replica_name, &block)
|
64
|
-
else
|
65
|
-
Proxy.new(self, replica_name)
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
def with_replica_block(replica_name, &block)
|
70
|
-
old_replica_name = current_replica_name
|
71
|
-
begin
|
72
|
-
self.current_replica_name = replica_name
|
73
|
-
rescue ActiveRecord::AdapterNotSpecified => e
|
74
|
-
self.current_replica_name = old_replica_name
|
75
|
-
logger.warn("Failed to establish replica connection: #{e.message} - defaulting to master")
|
76
|
-
end
|
77
|
-
yield
|
78
|
-
ensure
|
79
|
-
self.current_replica_name = old_replica_name
|
80
|
-
end
|
81
|
-
|
82
|
-
private
|
83
|
-
|
84
|
-
def current_replica_name
|
85
|
-
Thread.current[replica_key]
|
86
|
-
end
|
87
|
-
|
88
|
-
def current_replica_name=(new_replica_name)
|
89
|
-
Thread.current[replica_key] = new_replica_name
|
90
|
-
|
91
|
-
establish_replica_connection(new_replica_name) unless connected_to_replica?
|
92
|
-
end
|
93
|
-
|
94
|
-
def establish_replica_connection(replica_name)
|
95
|
-
name = replica_name ? "#{RAILS_ENV}_#{replica_name}" : RAILS_ENV
|
96
|
-
spec = configurations[name]
|
97
|
-
raise AdapterNotSpecified.new("No database defined by #{name} in database.yml") if spec.nil?
|
98
|
-
|
99
|
-
connection_handler.establish_connection(connection_pool_name, ConnectionSpecification.new(spec, "#{spec['adapter']}_connection"))
|
100
|
-
end
|
101
|
-
|
102
|
-
def connected_to_replica?
|
103
|
-
connection_handler.connection_pools.has_key?(connection_pool_name)
|
104
|
-
end
|
105
|
-
|
106
|
-
def replica_key
|
107
|
-
@replica_key ||= "#{name}_replica"
|
108
|
-
end
|
109
|
-
|
110
|
-
class Proxy
|
111
|
-
def initialize(target, replica)
|
112
|
-
@target = target
|
113
|
-
@replica = replica
|
114
|
-
end
|
115
|
-
|
116
|
-
def method_missing(method, *args, &block)
|
117
|
-
@target.with_replica_block(@replica) { @target.send(method, *args, &block) }
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|
121
|
-
end
|
122
|
-
Base.extend(Base::Replica)
|
123
|
-
|
124
|
-
# The only difference here is that we use klass.connection_pool_name
|
125
|
-
# instead of klass.name as the pool key
|
126
|
-
module ConnectionAdapters # :nodoc:
|
127
|
-
class ConnectionHandler # :nodoc:
|
128
|
-
|
129
|
-
def retrieve_connection_pool(klass)
|
130
|
-
pool = @connection_pools[klass.connection_pool_name]
|
131
|
-
return pool if pool
|
132
|
-
return nil if ActiveRecord::Base == klass
|
133
|
-
retrieve_connection_pool klass.superclass
|
134
|
-
end
|
135
|
-
|
136
|
-
def remove_connection(klass)
|
137
|
-
pool = @connection_pools[klass.connection_pool_name]
|
138
|
-
@connection_pools.delete_if { |key, value| value == pool }
|
139
|
-
pool.disconnect! if pool
|
140
|
-
pool.spec.config if pool
|
141
|
-
end
|
142
|
-
|
143
|
-
end
|
144
|
-
end
|
145
|
-
end
|
6
|
+
ActiveRecord::Base.extend ActiveRecord::Base::Replica
|
7
|
+
ActiveRecord::Associations::AssociationCollection.send(:include, ActiveRecord::Associations::AssociationCollection::Replica)
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module ActiveRecord # :nodoc:
|
2
|
+
module Associations
|
3
|
+
class AssociationCollection < AssociationProxy #:nodoc:
|
4
|
+
module Replica
|
5
|
+
def with_slave
|
6
|
+
with_replica(:slave)
|
7
|
+
end
|
8
|
+
|
9
|
+
def with_master
|
10
|
+
with_replica(nil)
|
11
|
+
end
|
12
|
+
|
13
|
+
def with_slave_if(condition)
|
14
|
+
condition ? with_slave : with_master
|
15
|
+
end
|
16
|
+
|
17
|
+
def with_slave_unless(condition)
|
18
|
+
with_slave_if(!condition)
|
19
|
+
end
|
20
|
+
|
21
|
+
def with_replica(replica_name)
|
22
|
+
Proxy.new(self, replica_name)
|
23
|
+
end
|
24
|
+
|
25
|
+
class Proxy
|
26
|
+
def initialize(association_collection, replica)
|
27
|
+
@association_collection = association_collection
|
28
|
+
@replica = replica
|
29
|
+
end
|
30
|
+
|
31
|
+
def method_missing(method, *args, &block)
|
32
|
+
@association_collection.proxy_reflection.klass.with_replica_block(@replica) { @association_collection.send(method, *args, &block) }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/replica/base.rb
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
module ActiveRecord # :nodoc:
|
2
|
+
class Base # :nodoc:
|
3
|
+
module Replica
|
4
|
+
|
5
|
+
# Executes queries using the slave database. Fails over to master if no slave is found.
|
6
|
+
# if you want to execute a block of code on the slave you can go:
|
7
|
+
# Account.with_slave do
|
8
|
+
# Account.first
|
9
|
+
# end
|
10
|
+
# the first account will be found on the slave DB
|
11
|
+
#
|
12
|
+
# For one-liners you can simply do
|
13
|
+
# Account.with_slave.first
|
14
|
+
#
|
15
|
+
# this is the same as:
|
16
|
+
# Account.with_replica(:slave) do
|
17
|
+
# Account.first
|
18
|
+
# end
|
19
|
+
def with_slave(&block)
|
20
|
+
with_replica(:slave, &block)
|
21
|
+
end
|
22
|
+
|
23
|
+
# See with_slave
|
24
|
+
def with_master(&block)
|
25
|
+
with_replica(nil, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def with_slave_if(condition, &block)
|
29
|
+
condition ? with_slave(&block) : with_master(&block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def with_slave_unless(condition, &block)
|
33
|
+
with_slave_if(!condition, &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Name of the connection pool. Used by ConnectionHandler to retrieve the current connection pool.
|
37
|
+
def connection_pool_name # :nodoc:
|
38
|
+
replica = current_replica_name
|
39
|
+
if replica
|
40
|
+
"#{name}_#{replica}"
|
41
|
+
elsif self == ActiveRecord::Base
|
42
|
+
name
|
43
|
+
else
|
44
|
+
superclass.connection_pool_name
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Specify which database to use.
|
49
|
+
#
|
50
|
+
# Example:
|
51
|
+
# database.yml
|
52
|
+
# test_slave:
|
53
|
+
# adapter: mysql
|
54
|
+
# ...
|
55
|
+
#
|
56
|
+
# Account.with_replica(:slave) { Account.count }
|
57
|
+
# Account.with_replica(:slave).count
|
58
|
+
#
|
59
|
+
def with_replica(replica_name, &block)
|
60
|
+
if block_given?
|
61
|
+
with_replica_block(replica_name, &block)
|
62
|
+
else
|
63
|
+
Proxy.new(self, replica_name)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def with_replica_block(replica_name, &block)
|
68
|
+
old_replica_name = current_replica_name
|
69
|
+
begin
|
70
|
+
self.current_replica_name = replica_name
|
71
|
+
rescue ActiveRecord::AdapterNotSpecified => e
|
72
|
+
self.current_replica_name = old_replica_name
|
73
|
+
logger.warn("Failed to establish replica connection: #{e.message} - defaulting to master")
|
74
|
+
end
|
75
|
+
yield
|
76
|
+
ensure
|
77
|
+
self.current_replica_name = old_replica_name
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def current_replica_name
|
83
|
+
Thread.current[replica_key]
|
84
|
+
end
|
85
|
+
|
86
|
+
def current_replica_name=(new_replica_name)
|
87
|
+
Thread.current[replica_key] = new_replica_name
|
88
|
+
|
89
|
+
establish_replica_connection(new_replica_name) unless connected_to_replica?
|
90
|
+
end
|
91
|
+
|
92
|
+
def establish_replica_connection(replica_name)
|
93
|
+
name = replica_name ? "#{RAILS_ENV}_#{replica_name}" : RAILS_ENV
|
94
|
+
spec = configurations[name]
|
95
|
+
raise AdapterNotSpecified.new("No database defined by #{name} in database.yml") if spec.nil?
|
96
|
+
|
97
|
+
connection_handler.establish_connection(connection_pool_name, ConnectionSpecification.new(spec, "#{spec['adapter']}_connection"))
|
98
|
+
end
|
99
|
+
|
100
|
+
def connected_to_replica?
|
101
|
+
connection_handler.connection_pools.has_key?(connection_pool_name)
|
102
|
+
end
|
103
|
+
|
104
|
+
def replica_key
|
105
|
+
@replica_key ||= "#{name}_replica"
|
106
|
+
end
|
107
|
+
|
108
|
+
class Proxy
|
109
|
+
def initialize(target, replica)
|
110
|
+
@target = target
|
111
|
+
@replica = replica
|
112
|
+
end
|
113
|
+
|
114
|
+
def method_missing(method, *args, &block)
|
115
|
+
@target.with_replica_block(@replica) { @target.send(method, *args, &block) }
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ActiveRecord # :nodoc:
|
2
|
+
# The only difference here is that we use klass.connection_pool_name
|
3
|
+
# instead of klass.name as the pool key
|
4
|
+
module ConnectionAdapters # :nodoc:
|
5
|
+
class ConnectionHandler # :nodoc:
|
6
|
+
|
7
|
+
def retrieve_connection_pool(klass)
|
8
|
+
pool = @connection_pools[klass.connection_pool_name]
|
9
|
+
return pool if pool
|
10
|
+
return nil if ActiveRecord::Base == klass
|
11
|
+
retrieve_connection_pool klass.superclass
|
12
|
+
end
|
13
|
+
|
14
|
+
def remove_connection(klass)
|
15
|
+
pool = @connection_pools[klass.connection_pool_name]
|
16
|
+
@connection_pools.delete_if { |key, value| value == pool }
|
17
|
+
pool.disconnect! if pool
|
18
|
+
pool.spec.config if pool
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/test/models.rb
CHANGED
data/test/replica_test.rb
CHANGED
@@ -36,7 +36,6 @@ class ReplicaTest < ActiveRecord::TestCase
|
|
36
36
|
|
37
37
|
assert_not_equal Account.count, ActiveRecord::Base.with_slave { Account.count }
|
38
38
|
assert_not_equal Account.count, Account.with_slave { Account.count }
|
39
|
-
assert_not_equal Account.count, Account.with_slave.count
|
40
39
|
assert_equal Account.count, Ticket.with_slave { Account.count }
|
41
40
|
end
|
42
41
|
|
@@ -112,4 +111,24 @@ class ReplicaTest < ActiveRecord::TestCase
|
|
112
111
|
end
|
113
112
|
|
114
113
|
end
|
114
|
+
|
115
|
+
context "replica proxy" do
|
116
|
+
should "successfully execute queries" do
|
117
|
+
assert_using_master_db(Account)
|
118
|
+
Account.create!
|
119
|
+
|
120
|
+
assert_not_equal Account.count, Account.with_slave.count
|
121
|
+
end
|
122
|
+
|
123
|
+
should "work association collections" do
|
124
|
+
assert_using_master_db(Account)
|
125
|
+
account = Account.create!
|
126
|
+
|
127
|
+
Ticket.connection.expects(:select_all).with("SELECT * FROM `tickets` WHERE (`tickets`.account_id = #{account.id}) LIMIT 1", anything).returns([])
|
128
|
+
Ticket.with_slave.connection.expects(:select_all).with("SELECT * FROM `tickets` WHERE (`tickets`.account_id = #{account.id}) LIMIT 1", anything).returns([])
|
129
|
+
|
130
|
+
account.tickets.first
|
131
|
+
account.tickets.with_slave.first
|
132
|
+
end
|
133
|
+
end
|
115
134
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: replica
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eric Chapweske
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2010-
|
13
|
+
date: 2010-02-08 00:00:00 -08:00
|
14
14
|
default_executable:
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
@@ -40,6 +40,9 @@ files:
|
|
40
40
|
- Rakefile
|
41
41
|
- VERSION
|
42
42
|
- lib/replica.rb
|
43
|
+
- lib/replica/association_collection.rb
|
44
|
+
- lib/replica/base.rb
|
45
|
+
- lib/replica/connection_pool.rb
|
43
46
|
- test/database.yml
|
44
47
|
- test/helper.rb
|
45
48
|
- test/models.rb
|