schoefmax-multi_db 0.1.4 → 0.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/README.rdoc +27 -6
- data/lib/multi_db.rb +1 -0
- data/lib/multi_db/active_record_extensions.rb +42 -15
- data/lib/multi_db/connection_proxy.rb +32 -28
- data/lib/multi_db/scheduler.rb +10 -8
- data/lib/multi_db/thread_local_accessors.rb +32 -0
- data/multi_db.gemspec +3 -3
- data/spec/config/database.yml +3 -0
- data/spec/connection_proxy_spec.rb +62 -14
- data/spec/scheduler_spec.rb +18 -3
- data/spec/spec_helper.rb +3 -1
- data/spec/thread_local_accessors_spec.rb +40 -0
- metadata +4 -2
data/README.rdoc
CHANGED
|
@@ -8,10 +8,27 @@ Within transactions, while executing ActiveRecord Observers and
|
|
|
8
8
|
within "with_master" blocks (see below), even read queries are sent to the
|
|
9
9
|
master database.
|
|
10
10
|
|
|
11
|
+
=== Important changes in 0.2.0
|
|
12
|
+
|
|
13
|
+
* As of this version, <tt>ActiveRecord::Base.connection</tt> does not return the
|
|
14
|
+
connection proxy by default anymore (therefore the jump to 0.2.0). Only models
|
|
15
|
+
inheriting from AR::B return the proxy, unless they are defined as master_models
|
|
16
|
+
(see below). If you want to access the connection proxy from AR::B directly,
|
|
17
|
+
use <tt>ActiveRecord::Base.connection_proxy</tt>.
|
|
18
|
+
|
|
19
|
+
* This version is the first attempt for thread-safety of this gem. <em>There might
|
|
20
|
+
still be some threading issues left!</em>. So please test your apps thoroughly
|
|
21
|
+
and report any issues you might encounter.
|
|
22
|
+
|
|
23
|
+
* <tt>CGI::Session::ActiveRecordStore::Session</tt> is now automatically registered
|
|
24
|
+
as a master model.
|
|
25
|
+
|
|
11
26
|
=== Caveats
|
|
12
27
|
|
|
13
|
-
* works with activerecord 2.1 and 2.2
|
|
14
|
-
* when using Passenger or lightspeed you will probably need to introduce a
|
|
28
|
+
* works with activerecord 2.1 and 2.2
|
|
29
|
+
* when using Passenger or lightspeed you will probably need to introduce a
|
|
30
|
+
before_filter which checks if the proxy is setup (see the discussion in the
|
|
31
|
+
masochism readme: http://github.com/technoweenie/masochism)
|
|
15
32
|
|
|
16
33
|
=== Install
|
|
17
34
|
|
|
@@ -73,13 +90,16 @@ one on your development machine.
|
|
|
73
90
|
|
|
74
91
|
Just add this to your controller:
|
|
75
92
|
|
|
76
|
-
around_filter(:only => :foo_action) { |c,a|
|
|
93
|
+
around_filter(:only => :foo_action) { |c,a| ActiveRecord::Base.connection_proxy.with_master { a.call } }
|
|
77
94
|
|
|
78
95
|
=== Forcing the master for certain models
|
|
79
96
|
|
|
80
|
-
In your environment.rb or an initializer, add
|
|
97
|
+
In your environment.rb or an initializer, add this *before* the call to <tt>setup!</tt>:
|
|
98
|
+
|
|
99
|
+
MultiDb::ConnectionProxy.master_models = ['CGI::Session::ActiveRecordStore::Session', 'PaymentTransaction', ...]
|
|
100
|
+
MultiDb::ConnectionProxy.setup!
|
|
81
101
|
|
|
82
|
-
|
|
102
|
+
*NOTE*: You cannot safely add more master_models after calling <tt>setup!</tt>.
|
|
83
103
|
|
|
84
104
|
=== Making one slave database sticky during a request
|
|
85
105
|
|
|
@@ -153,7 +173,8 @@ Note that the configurations hash should contain strings as keys instead of symb
|
|
|
153
173
|
This might help with HA setups using virtual IPs (a test setup would be nice
|
|
154
174
|
to verify this)
|
|
155
175
|
* You specify slave databases in the configuration instead of specifying an extra
|
|
156
|
-
master database. This makes disabling or removing multi_db less dangerous
|
|
176
|
+
master database. This makes disabling or removing multi_db less dangerous
|
|
177
|
+
(Update: Recent versions of masochism support this, too).
|
|
157
178
|
* There are no <tt>set_to_master!</tt> and <tt>set_to_slave!</tt> methods, just
|
|
158
179
|
<tt>with_master(&block)</tt>
|
|
159
180
|
* All proxied methods are dynamically generated for better performance
|
data/lib/multi_db.rb
CHANGED
|
@@ -1,27 +1,54 @@
|
|
|
1
1
|
module MultiDb
|
|
2
2
|
module ActiveRecordExtensions
|
|
3
3
|
def self.included(base)
|
|
4
|
+
base.send :include, InstanceMethods
|
|
5
|
+
base.send :extend, ClassMethods
|
|
4
6
|
base.alias_method_chain :reload, :master
|
|
7
|
+
base.cattr_accessor :connection_proxy
|
|
8
|
+
# handle subclasses which were defined by the framework or plugins
|
|
9
|
+
base.send(:subclasses).each do |child|
|
|
10
|
+
child.hijack_connection
|
|
11
|
+
end
|
|
12
|
+
end
|
|
5
13
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
14
|
+
module InstanceMethods
|
|
15
|
+
def reload_with_master(*args, &block)
|
|
16
|
+
self.connection_proxy.with_master { reload_without_master }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
9
19
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
module ClassMethods
|
|
21
|
+
# Make sure transactions always switch to the master
|
|
22
|
+
def transaction(&block)
|
|
23
|
+
if self.connection.kind_of?(ConnectionProxy)
|
|
24
|
+
super
|
|
25
|
+
else
|
|
26
|
+
self.connection_proxy.with_master { super }
|
|
17
27
|
end
|
|
18
28
|
end
|
|
19
29
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
# make caching always use the ConnectionProxy
|
|
31
|
+
def cache(&block)
|
|
32
|
+
if ActiveRecord::Base.configurations.blank?
|
|
33
|
+
yield
|
|
34
|
+
else
|
|
35
|
+
self.connection_proxy.cache(&block)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def inherited(child)
|
|
40
|
+
super
|
|
41
|
+
child.hijack_connection
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def hijack_connection
|
|
45
|
+
return if ConnectionProxy.master_models.include?(self.to_s)
|
|
46
|
+
logger.info "[MULTIDB] hijacking connection for #{self.to_s}"
|
|
47
|
+
class << self
|
|
48
|
+
def connection
|
|
49
|
+
self.connection_proxy
|
|
50
|
+
end
|
|
51
|
+
end
|
|
25
52
|
end
|
|
26
53
|
end
|
|
27
54
|
end
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
module MultiDb
|
|
2
2
|
class ConnectionProxy
|
|
3
|
-
include
|
|
3
|
+
include QueryCacheCompat
|
|
4
4
|
include ActiveRecord::ConnectionAdapters::QueryCache
|
|
5
|
+
extend ThreadLocalAccessors
|
|
5
6
|
|
|
6
7
|
# Safe methods are those that should either go to the slave ONLY or go
|
|
7
8
|
# to the current active connection.
|
|
@@ -9,8 +10,11 @@ module MultiDb
|
|
|
9
10
|
:select_rows, :select, :verify!, :raw_connection, :active?, :reconnect!,
|
|
10
11
|
:disconnect!, :reset_runtime, :log, :log_info ]
|
|
11
12
|
|
|
13
|
+
DEFAULT_MASTER_MODELS = ['CGI::Session::ActiveRecordStore::Session']
|
|
14
|
+
|
|
12
15
|
attr_accessor :master
|
|
13
|
-
|
|
16
|
+
tlattr_accessor :master_depth
|
|
17
|
+
tlattr_accessor :current
|
|
14
18
|
|
|
15
19
|
class << self
|
|
16
20
|
|
|
@@ -22,7 +26,7 @@ module MultiDb
|
|
|
22
26
|
#
|
|
23
27
|
# Example:
|
|
24
28
|
#
|
|
25
|
-
# MultiDb::ConnectionProxy.master_models = ['
|
|
29
|
+
# MultiDb::ConnectionProxy.master_models = ['MySessionStore', 'PaymentTransaction']
|
|
26
30
|
attr_accessor :master_models
|
|
27
31
|
|
|
28
32
|
# decides if we should switch to the next reader automatically.
|
|
@@ -34,7 +38,7 @@ module MultiDb
|
|
|
34
38
|
# Replaces the connection of ActiveRecord::Base with a proxy and
|
|
35
39
|
# establishes the connections to the slaves.
|
|
36
40
|
def setup!
|
|
37
|
-
self.master_models ||=
|
|
41
|
+
self.master_models ||= DEFAULT_MASTER_MODELS
|
|
38
42
|
self.environment ||= (defined?(RAILS_ENV) ? RAILS_ENV : 'development')
|
|
39
43
|
self.sticky_slave ||= false
|
|
40
44
|
|
|
@@ -47,6 +51,8 @@ module MultiDb
|
|
|
47
51
|
master.logger.info("** multi_db with master and #{slaves.length} slave#{"s" if slaves.length > 1} loaded.")
|
|
48
52
|
end
|
|
49
53
|
|
|
54
|
+
protected
|
|
55
|
+
|
|
50
56
|
# Slave entries in the database.yml must be named like this
|
|
51
57
|
# development_slave_database:
|
|
52
58
|
# or
|
|
@@ -69,32 +75,34 @@ module MultiDb
|
|
|
69
75
|
end
|
|
70
76
|
end
|
|
71
77
|
end
|
|
78
|
+
|
|
79
|
+
private :new
|
|
72
80
|
|
|
73
81
|
end
|
|
74
82
|
|
|
75
83
|
def initialize(master, slaves)
|
|
76
|
-
@slaves
|
|
77
|
-
@master
|
|
78
|
-
@
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
@slaves = Scheduler.new(slaves)
|
|
85
|
+
@master = master
|
|
86
|
+
@reconnect = false
|
|
87
|
+
self.current = @slaves.current
|
|
88
|
+
self.master_depth = 0
|
|
81
89
|
end
|
|
82
|
-
|
|
90
|
+
|
|
83
91
|
def slave
|
|
84
92
|
@slaves.current
|
|
85
93
|
end
|
|
86
94
|
|
|
87
95
|
def with_master
|
|
88
|
-
|
|
89
|
-
|
|
96
|
+
self.current = @master
|
|
97
|
+
self.master_depth += 1
|
|
90
98
|
yield
|
|
91
99
|
ensure
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
self.master_depth -= 1
|
|
101
|
+
self.current = slave if master_depth.zero?
|
|
94
102
|
end
|
|
95
103
|
|
|
96
104
|
def transaction(start_db_transaction = true, &block)
|
|
97
|
-
with_master {
|
|
105
|
+
with_master { @master.retrieve_connection.transaction(start_db_transaction, &block) }
|
|
98
106
|
end
|
|
99
107
|
|
|
100
108
|
# Calls the method on master/slave and dynamically creates a new
|
|
@@ -108,19 +116,15 @@ module MultiDb
|
|
|
108
116
|
# Switches to the next slave database for read operations.
|
|
109
117
|
# Fails over to the master database if all slaves are unavailable.
|
|
110
118
|
def next_reader!
|
|
111
|
-
return
|
|
112
|
-
|
|
119
|
+
return unless master_depth.zero? # don't if in with_master block
|
|
120
|
+
self.current = @slaves.next
|
|
113
121
|
rescue Scheduler::NoMoreItems
|
|
114
122
|
logger.warn "[MULTIDB] All slaves are blacklisted. Reading from master"
|
|
115
|
-
|
|
123
|
+
self.current = @master
|
|
116
124
|
end
|
|
117
125
|
|
|
118
126
|
protected
|
|
119
127
|
|
|
120
|
-
def get_connection(db_class)
|
|
121
|
-
db_class.retrieve_connection
|
|
122
|
-
end
|
|
123
|
-
|
|
124
128
|
def create_delegation_method!(method)
|
|
125
129
|
self.instance_eval %Q{
|
|
126
130
|
def #{method}(*args, &block)
|
|
@@ -136,27 +140,27 @@ module MultiDb
|
|
|
136
140
|
|
|
137
141
|
def send_to_master(method, *args, &block)
|
|
138
142
|
reconnect_master! if @reconnect
|
|
139
|
-
|
|
143
|
+
@master.retrieve_connection.send(method, *args, &block)
|
|
140
144
|
rescue => e
|
|
141
145
|
raise_master_error(e)
|
|
142
146
|
end
|
|
143
147
|
|
|
144
148
|
def send_to_current(method, *args, &block)
|
|
145
149
|
reconnect_master! if @reconnect && master?
|
|
146
|
-
|
|
150
|
+
current.retrieve_connection.send(method, *args, &block)
|
|
147
151
|
rescue NotImplementedError, NoMethodError
|
|
148
152
|
raise
|
|
149
153
|
rescue => e # TODO don't rescue everything
|
|
150
154
|
raise_master_error(e) if master?
|
|
151
155
|
logger.warn "[MULTIDB] Error reading from slave database"
|
|
152
156
|
logger.error %(#{e.message}\n#{e.backtrace.join("\n")})
|
|
153
|
-
@slaves.blacklist!(
|
|
157
|
+
@slaves.blacklist!(current)
|
|
154
158
|
next_reader!
|
|
155
159
|
retry
|
|
156
160
|
end
|
|
157
161
|
|
|
158
162
|
def reconnect_master!
|
|
159
|
-
|
|
163
|
+
@master.retrieve_connection.reconnect!
|
|
160
164
|
@reconnect = false
|
|
161
165
|
end
|
|
162
166
|
|
|
@@ -171,12 +175,12 @@ module MultiDb
|
|
|
171
175
|
end
|
|
172
176
|
|
|
173
177
|
def master?
|
|
174
|
-
|
|
178
|
+
current == @master
|
|
175
179
|
end
|
|
176
180
|
|
|
177
181
|
def logger
|
|
178
182
|
ActiveRecord::Base.logger
|
|
179
183
|
end
|
|
180
|
-
|
|
184
|
+
|
|
181
185
|
end
|
|
182
186
|
end
|
data/lib/multi_db/scheduler.rb
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
module MultiDb
|
|
2
2
|
class Scheduler
|
|
3
3
|
class NoMoreItems < Exception; end
|
|
4
|
-
|
|
4
|
+
extend ThreadLocalAccessors
|
|
5
|
+
|
|
5
6
|
attr :items
|
|
6
7
|
delegate :[], :[]=, :to => :items
|
|
8
|
+
tlattr_accessor :current_index
|
|
7
9
|
|
|
8
10
|
def initialize(items, blacklist_timeout = 1.minute)
|
|
9
11
|
@n = items.length
|
|
10
12
|
@items = items
|
|
11
13
|
@blacklist = Array.new(@n, Time.at(0))
|
|
12
|
-
@current = 0
|
|
13
14
|
@blacklist_timeout = blacklist_timeout
|
|
15
|
+
self.current_index = 0
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
def blacklist!(item)
|
|
@@ -18,22 +20,22 @@ module MultiDb
|
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
def current
|
|
21
|
-
@items[
|
|
23
|
+
@items[current_index]
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
def next
|
|
25
|
-
previous =
|
|
27
|
+
previous = current_index
|
|
26
28
|
until(@blacklist[next_index!] < Time.now - @blacklist_timeout) do
|
|
27
|
-
raise NoMoreItems, 'All items are blacklisted' if
|
|
29
|
+
raise NoMoreItems, 'All items are blacklisted' if current_index == previous
|
|
28
30
|
end
|
|
29
|
-
|
|
31
|
+
current
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
protected
|
|
33
35
|
|
|
34
36
|
def next_index!
|
|
35
|
-
|
|
37
|
+
self.current_index = (current_index + 1) % @n
|
|
36
38
|
end
|
|
37
|
-
|
|
39
|
+
|
|
38
40
|
end
|
|
39
41
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module MultiDb
|
|
2
|
+
module ThreadLocalAccessors
|
|
3
|
+
# Creates thread-local accessors for the given attribute name.
|
|
4
|
+
#
|
|
5
|
+
# The first invokation of the setter will make this value the default
|
|
6
|
+
# for any subsequent threads.
|
|
7
|
+
def tlattr_accessor(name)
|
|
8
|
+
ivar = "@_tlattr_#{name}"
|
|
9
|
+
class_eval %Q{
|
|
10
|
+
def #{name}
|
|
11
|
+
if #{ivar}
|
|
12
|
+
#{ivar}[Thread.current.object_id]
|
|
13
|
+
else
|
|
14
|
+
nil
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def #{name}=(val)
|
|
19
|
+
if #{ivar}
|
|
20
|
+
unless #{ivar}.has_key?(Thread.current.object_id)
|
|
21
|
+
ObjectSpace.define_finalizer(Thread.current, lambda {|id| #{ivar}.delete(id) })
|
|
22
|
+
end
|
|
23
|
+
#{ivar}[Thread.current.object_id] = val
|
|
24
|
+
else
|
|
25
|
+
#{ivar} = Hash.new {|h, k| h[k] = val}
|
|
26
|
+
val
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
}, __FILE__, __LINE__
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/multi_db.gemspec
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Gem::Specification.new do |s|
|
|
4
4
|
s.name = %q{multi_db}
|
|
5
|
-
s.version = "0.
|
|
5
|
+
s.version = "0.2.0"
|
|
6
6
|
|
|
7
7
|
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
|
8
8
|
s.authors = ["Maximilian Sch\303\266fmann"]
|
|
9
|
-
s.date = %q{2009-
|
|
9
|
+
s.date = %q{2009-03-08}
|
|
10
10
|
s.description = "Connection proxy for ActiveRecord for single master / multiple slave database deployments"
|
|
11
11
|
s.email = "max@pragmatic-it.de"
|
|
12
12
|
s.extra_rdoc_files = ["LICENSE", "README.rdoc"]
|
|
13
|
-
s.files = ["lib/multi_db.rb", "lib/multi_db/active_record_extensions.rb", "lib/multi_db/connection_proxy.rb", "lib/multi_db/observer_extensions.rb", "lib/multi_db/query_cache_compat.rb", "lib/multi_db/scheduler.rb", "LICENSE", "README.rdoc", "spec/config/database.yml", "spec/connection_proxy_spec.rb", "spec/scheduler_spec.rb", "spec/spec_helper.rb", "multi_db.gemspec"]
|
|
13
|
+
s.files = ["lib/multi_db.rb", "lib/multi_db/thread_local_accessors.rb", "lib/multi_db/active_record_extensions.rb", "lib/multi_db/connection_proxy.rb", "lib/multi_db/observer_extensions.rb", "lib/multi_db/query_cache_compat.rb", "lib/multi_db/scheduler.rb", "LICENSE", "README.rdoc", "spec/config/database.yml", "spec/connection_proxy_spec.rb", "spec/scheduler_spec.rb", "spec/thread_local_accessors_spec.rb", "spec/spec_helper.rb", "multi_db.gemspec"]
|
|
14
14
|
s.has_rdoc = true
|
|
15
15
|
s.homepage = "http://github.com/schoefmax/multi_db"
|
|
16
16
|
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "multi_db", "--main", "README.rdoc"]
|
data/spec/config/database.yml
CHANGED
|
@@ -4,6 +4,7 @@ test:
|
|
|
4
4
|
username: root
|
|
5
5
|
password:
|
|
6
6
|
host: 127.0.0.1
|
|
7
|
+
pool: 5
|
|
7
8
|
|
|
8
9
|
test_slave_database_1:
|
|
9
10
|
adapter: mysql
|
|
@@ -11,6 +12,7 @@ test_slave_database_1:
|
|
|
11
12
|
username: root
|
|
12
13
|
password:
|
|
13
14
|
host: 127.0.0.1
|
|
15
|
+
pool: 5
|
|
14
16
|
|
|
15
17
|
test_slave_database_2:
|
|
16
18
|
adapter: mysql
|
|
@@ -18,3 +20,4 @@ test_slave_database_2:
|
|
|
18
20
|
username: root
|
|
19
21
|
password:
|
|
20
22
|
host: 127.0.0.1
|
|
23
|
+
pool: 5
|
|
@@ -2,6 +2,7 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
|
2
2
|
require MULTI_DB_SPEC_DIR + '/../lib/multi_db/query_cache_compat'
|
|
3
3
|
require MULTI_DB_SPEC_DIR + '/../lib/multi_db/active_record_extensions'
|
|
4
4
|
require MULTI_DB_SPEC_DIR + '/../lib/multi_db/observer_extensions'
|
|
5
|
+
require MULTI_DB_SPEC_DIR + '/../lib/multi_db/thread_local_accessors'
|
|
5
6
|
require MULTI_DB_SPEC_DIR + '/../lib/multi_db/scheduler'
|
|
6
7
|
require MULTI_DB_SPEC_DIR + '/../lib/multi_db/connection_proxy'
|
|
7
8
|
|
|
@@ -21,6 +22,7 @@ describe MultiDb::ConnectionProxy do
|
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
before(:each) do
|
|
25
|
+
MultiDb::ConnectionProxy.master_models = ['MasterModel']
|
|
24
26
|
MultiDb::ConnectionProxy.setup!
|
|
25
27
|
@proxy = ActiveRecord::Base.connection_proxy
|
|
26
28
|
@master = @proxy.master.retrieve_connection
|
|
@@ -32,8 +34,16 @@ describe MultiDb::ConnectionProxy do
|
|
|
32
34
|
ActiveRecord::Base.send :alias_method, :reload, :reload_without_master
|
|
33
35
|
end
|
|
34
36
|
|
|
35
|
-
it 'AR::B
|
|
36
|
-
ActiveRecord::Base.
|
|
37
|
+
it 'AR::B should respond to #connection_proxy' do
|
|
38
|
+
ActiveRecord::Base.connection_proxy.should be_kind_of(MultiDb::ConnectionProxy)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'FooModel#connection should return an instance of MultiDb::ConnectionProxy' do
|
|
42
|
+
FooModel.connection.should be_kind_of(MultiDb::ConnectionProxy)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'MasterModel#connection should not return an instance of MultiDb::ConnectionProxy' do
|
|
46
|
+
MasterModel.connection.should_not be_kind_of(MultiDb::ConnectionProxy)
|
|
37
47
|
end
|
|
38
48
|
|
|
39
49
|
it "should generate classes for each entry in the database.yml" do
|
|
@@ -135,18 +145,6 @@ describe MultiDb::ConnectionProxy do
|
|
|
135
145
|
@proxy.insert(@sql)
|
|
136
146
|
end
|
|
137
147
|
|
|
138
|
-
it 'should always use the master database for models configured as master models' do
|
|
139
|
-
@slave2.should_receive(:select_all).once.and_return([])
|
|
140
|
-
MasterModel.connection.should == @proxy
|
|
141
|
-
MasterModel.first
|
|
142
|
-
|
|
143
|
-
MultiDb::ConnectionProxy.master_models = ['MasterModel']
|
|
144
|
-
MasterModel.connection.should == @master
|
|
145
|
-
@slave1.should_not_receive(:select_all)
|
|
146
|
-
MasterModel.retrieve_connection.should_receive(:select_all).once.and_return([])
|
|
147
|
-
MasterModel.first
|
|
148
|
-
end
|
|
149
|
-
|
|
150
148
|
it 'should reload models from the master' do
|
|
151
149
|
foo = FooModel.create!(:bar => 'baz')
|
|
152
150
|
foo.bar = "not_saved"
|
|
@@ -178,5 +176,55 @@ describe MultiDb::ConnectionProxy do
|
|
|
178
176
|
|
|
179
177
|
end
|
|
180
178
|
|
|
179
|
+
describe '(accessed from multiple threads)' do
|
|
180
|
+
# NOTE: We cannot put expectations on the connection objects itself
|
|
181
|
+
# for the threading specs, as connection pooling will cause
|
|
182
|
+
# different connections being returned for different threads.
|
|
183
|
+
|
|
184
|
+
it '#current and #next_reader! should be local to the thread' do
|
|
185
|
+
@proxy.current.should == MultiDb::SlaveDatabase1
|
|
186
|
+
@proxy.next_reader!.should == MultiDb::SlaveDatabase2
|
|
187
|
+
Thread.new do
|
|
188
|
+
@proxy.current.should == MultiDb::SlaveDatabase1
|
|
189
|
+
@proxy.next_reader!.should == MultiDb::SlaveDatabase2
|
|
190
|
+
@proxy.current.should == MultiDb::SlaveDatabase2
|
|
191
|
+
@proxy.next_reader!.should == MultiDb::SlaveDatabase1
|
|
192
|
+
@proxy.current.should == MultiDb::SlaveDatabase1
|
|
193
|
+
end
|
|
194
|
+
@proxy.current.should == MultiDb::SlaveDatabase2
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it '#with_master should be local to the thread' do
|
|
198
|
+
@proxy.current.should_not == @proxy.master
|
|
199
|
+
@proxy.with_master do
|
|
200
|
+
@proxy.current.should == @proxy.master
|
|
201
|
+
Thread.new do
|
|
202
|
+
@proxy.current.should_not == @proxy.master
|
|
203
|
+
@proxy.with_master do
|
|
204
|
+
@proxy.current.should == @proxy.master
|
|
205
|
+
end
|
|
206
|
+
@proxy.current.should_not == @proxy.master
|
|
207
|
+
end
|
|
208
|
+
@proxy.current.should == @proxy.master
|
|
209
|
+
end
|
|
210
|
+
@proxy.current.should_not == @proxy.master
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
it 'should switch to the next reader even whithin with_master-block in different threads' do
|
|
214
|
+
# Because of connection pooling in AR 2.2, the second thread will cause
|
|
215
|
+
# a new connection being created behind the scenes. We therefore just test
|
|
216
|
+
# that these connections are beting retrieved for the right databases here.
|
|
217
|
+
@proxy.master.should_not_receive(:retrieve_connection).and_return(@master)
|
|
218
|
+
MultiDb::SlaveDatabase1.should_receive(:retrieve_connection).twice.and_return(@slave1)
|
|
219
|
+
MultiDb::SlaveDatabase2.should_receive(:retrieve_connection).once.and_return(@slave2)
|
|
220
|
+
@proxy.with_master do
|
|
221
|
+
Thread.new do
|
|
222
|
+
3.times { @proxy.select_one(@sql) }
|
|
223
|
+
end.join
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
end
|
|
228
|
+
|
|
181
229
|
end
|
|
182
230
|
|
data/spec/scheduler_spec.rb
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
2
|
+
require MULTI_DB_SPEC_DIR + '/../lib/multi_db/thread_local_accessors'
|
|
2
3
|
require MULTI_DB_SPEC_DIR + '/../lib/multi_db/scheduler'
|
|
3
4
|
|
|
4
5
|
describe MultiDb::Scheduler do
|
|
5
6
|
|
|
6
7
|
before do
|
|
7
|
-
@items = [5,
|
|
8
|
+
@items = [5, 7, 4, 8]
|
|
8
9
|
@scheduler = MultiDb::Scheduler.new(@items.clone)
|
|
9
10
|
end
|
|
10
11
|
|
|
@@ -35,9 +36,23 @@ describe MultiDb::Scheduler do
|
|
|
35
36
|
|
|
36
37
|
it 'should unblacklist items automatically' do
|
|
37
38
|
@scheduler = MultiDb::Scheduler.new(@items.clone, 1.second)
|
|
38
|
-
@scheduler.blacklist!(
|
|
39
|
+
@scheduler.blacklist!(7)
|
|
39
40
|
sleep(1)
|
|
40
|
-
@scheduler.next.should ==
|
|
41
|
+
@scheduler.next.should == 7
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe '(accessed from multiple threads)' do
|
|
45
|
+
|
|
46
|
+
it '#current and #next should return the same item for the same thread' do
|
|
47
|
+
@scheduler.current.should == 5
|
|
48
|
+
@scheduler.next.should == 7
|
|
49
|
+
Thread.new do
|
|
50
|
+
@scheduler.current.should == 5
|
|
51
|
+
@scheduler.next.should == 7
|
|
52
|
+
end.join
|
|
53
|
+
@scheduler.next.should == 4
|
|
54
|
+
end
|
|
55
|
+
|
|
41
56
|
end
|
|
42
57
|
|
|
43
58
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
2
|
+
require MULTI_DB_SPEC_DIR + '/../lib/multi_db/thread_local_accessors.rb'
|
|
3
|
+
|
|
4
|
+
class Foo
|
|
5
|
+
extend MultiDb::ThreadLocalAccessors
|
|
6
|
+
tlattr_accessor :bar
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
describe MultiDb::ThreadLocalAccessors do
|
|
10
|
+
|
|
11
|
+
it "should store values local to the thread" do
|
|
12
|
+
x = Foo.new
|
|
13
|
+
x.bar = 2
|
|
14
|
+
Thread.new do
|
|
15
|
+
x.bar.should == 2
|
|
16
|
+
x.bar = 3
|
|
17
|
+
Thread.new do
|
|
18
|
+
x.bar.should == 2
|
|
19
|
+
x.bar = 5
|
|
20
|
+
end.join
|
|
21
|
+
x.bar.should == 3
|
|
22
|
+
end.join
|
|
23
|
+
x.bar.should == 2
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'should not leak memory' do
|
|
27
|
+
x = Foo.new
|
|
28
|
+
n = 6000
|
|
29
|
+
# create many thread-local values to make sure GC is invoked
|
|
30
|
+
n.times do
|
|
31
|
+
Thread.new do
|
|
32
|
+
x.bar = rand
|
|
33
|
+
end.join
|
|
34
|
+
end
|
|
35
|
+
hash = x.send :instance_variable_get, '@_tlattr_bar'
|
|
36
|
+
hash.size.should < (n / 2) # it should be a lot lower than n!
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
end
|
|
40
|
+
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: schoefmax-multi_db
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- "Maximilian Sch\xC3\xB6fmann"
|
|
@@ -9,7 +9,7 @@ autorequire:
|
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
11
|
|
|
12
|
-
date: 2009-
|
|
12
|
+
date: 2009-03-08 00:00:00 -08:00
|
|
13
13
|
default_executable:
|
|
14
14
|
dependencies:
|
|
15
15
|
- !ruby/object:Gem::Dependency
|
|
@@ -33,6 +33,7 @@ extra_rdoc_files:
|
|
|
33
33
|
- README.rdoc
|
|
34
34
|
files:
|
|
35
35
|
- lib/multi_db.rb
|
|
36
|
+
- lib/multi_db/thread_local_accessors.rb
|
|
36
37
|
- lib/multi_db/active_record_extensions.rb
|
|
37
38
|
- lib/multi_db/connection_proxy.rb
|
|
38
39
|
- lib/multi_db/observer_extensions.rb
|
|
@@ -43,6 +44,7 @@ files:
|
|
|
43
44
|
- spec/config/database.yml
|
|
44
45
|
- spec/connection_proxy_spec.rb
|
|
45
46
|
- spec/scheduler_spec.rb
|
|
47
|
+
- spec/thread_local_accessors_spec.rb
|
|
46
48
|
- spec/spec_helper.rb
|
|
47
49
|
- multi_db.gemspec
|
|
48
50
|
has_rdoc: true
|