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 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, but is not threadsafe (yet)
14
- * when using Passenger or lightspeed you will probably need to introduce a before_filter which checks if the proxy is setup (see the discussion in the masochism readme: http://github.com/technoweenie/masochism)
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| MyModel.connection.with_master { a.call } }
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
- MultiDb::ConnectionProxy.master_models = ['CGI::Session::ActiveRecordStore', 'PaymentTransaction', ...]
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,3 +1,4 @@
1
+ require 'multi_db/thread_local_accessors'
1
2
  require 'multi_db/scheduler'
2
3
  require 'multi_db/active_record_extensions'
3
4
  require 'multi_db/observer_extensions'
@@ -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
- class << base
7
-
8
- cattr_accessor :connection_proxy
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
- # hijack the original method
11
- def connection
12
- if ConnectionProxy.master_models.include?(self.to_s)
13
- self.retrieve_connection
14
- else
15
- self.connection_proxy
16
- end
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
- end
21
-
22
- def reload_with_master(*args, &block)
23
- self.class.connection_proxy.with_master do
24
- reload_without_master(*args, &block)
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 MultiDb::QueryCacheCompat
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
- attr_accessor :current
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 = ['CGI::Session::ActiveRecordStore']
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 = Scheduler.new(slaves)
77
- @master = master
78
- @current = @slaves.current
79
- @reconnect = false
80
- @with_master = 0
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
- @current = @master
89
- @with_master += 1
96
+ self.current = @master
97
+ self.master_depth += 1
90
98
  yield
91
99
  ensure
92
- @with_master -= 1
93
- @current = @slaves.current if @with_master == 0
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 { get_connection(@current).transaction(start_db_transaction, &block) }
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 if @with_master > 0 # don't if in with_master block
112
- @current = @slaves.next
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
- @current = @master
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
- get_connection(@master).send(method, *args, &block)
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
- get_connection(@current).send(method, *args, &block)
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!(@current)
157
+ @slaves.blacklist!(current)
154
158
  next_reader!
155
159
  retry
156
160
  end
157
161
 
158
162
  def reconnect_master!
159
- get_connection(@master).reconnect!
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
- @current == @master
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
@@ -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[@current]
23
+ @items[current_index]
22
24
  end
23
25
 
24
26
  def next
25
- previous = @current
27
+ previous = current_index
26
28
  until(@blacklist[next_index!] < Time.now - @blacklist_timeout) do
27
- raise NoMoreItems, 'All items are blacklisted' if @current == previous
29
+ raise NoMoreItems, 'All items are blacklisted' if current_index == previous
28
30
  end
29
- @items[@current]
31
+ current
30
32
  end
31
33
 
32
34
  protected
33
35
 
34
36
  def next_index!
35
- @current = (@current + 1) % @n
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.1.4"
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-01-29}
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"]
@@ -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#connection should return an instance of MultiDb::ConnectionProxy' do
36
- ActiveRecord::Base.connection.should be_kind_of(MultiDb::ConnectionProxy)
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
 
@@ -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, 2, 4, 8]
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!(2)
39
+ @scheduler.blacklist!(7)
39
40
  sleep(1)
40
- @scheduler.next.should == 2
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
@@ -1,4 +1,6 @@
1
- %w[rubygems active_record yaml erb spec].each {|lib| require lib}
1
+ require 'rubygems'
2
+ gem 'activerecord', '2.2.2'
3
+ %w[active_record yaml erb spec].each {|lib| require lib}
2
4
 
3
5
  RAILS_ENV = ENV['RAILS_ENV'] = 'test'
4
6
 
@@ -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.1.4
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-01-29 00:00:00 -08:00
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