Skip to content

Commit 4a14c11

Browse files
committed
Fixed metaprogramming bug in Concurrent::Async
1 parent e490705 commit 4a14c11

File tree

2 files changed

+57
-40
lines changed

2 files changed

+57
-40
lines changed

lib/concurrent/async.rb

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ module Concurrent
77
# A mixin module that provides simple asynchronous behavior to any standard
88
# class/object or object.
99
#
10+
# Scenario:
11+
# As a stateful, plain old Ruby class/object
12+
# I want safe, asynchronous behavior
13+
# So my long-running methods don't block the main thread
14+
#
1015
# Stateful, mutable objects must be managed carefully when used asynchronously.
1116
# But Ruby is an object-oriented language so designing with objects and classes
1217
# plays to Ruby's strengths and is often more natural to many Ruby programmers.
@@ -25,7 +30,7 @@ module Concurrent
2530
#
2631
# Very loosely based on the +async+ and +await+ keywords in C#.
2732
#
28-
# @example
33+
# @example Defining an asynchronous class
2934
# class Echo
3035
# include Concurrent::Async
3136
#
@@ -42,6 +47,18 @@ module Concurrent
4247
# horn.async.echo('one') # asynchronous, non-blocking, thread-safe
4348
# horn.await.echo('two') # synchronous, blocking, thread-safe
4449
#
50+
# @example Monkey-patching an existing object
51+
# numbers = 1_000_000.times.collect{ rand }
52+
# numbers.extend(Concurrent::Async)
53+
#
54+
# future = numbers.async.max
55+
# future.state #=> :pending
56+
#
57+
# sleep(2)
58+
#
59+
# future.state #=> :fulfilled
60+
# future.value #=> 0.999999138918843
61+
#
4562
# @note Thread safe guarantees can only be made when asynchronous method calls
4663
# are not mixed with synchronous method calls. Use only synchronous calls
4764
# when the object is used exclusively on a single thread. Use only
@@ -119,7 +136,7 @@ def method_missing(method, *args, &block)
119136
super unless @delegate.respond_to?(method)
120137
Async::validate_argc(@delegate, method, *args)
121138

122-
AwaitDelegator.send(:define_method, method) do |*args|
139+
self.define_singleton_method(method) do |*args|
123140
Async::validate_argc(@delegate, method, *args)
124141
ivar = Concurrent::IVar.new
125142
value, reason = nil, nil
@@ -171,7 +188,7 @@ def method_missing(method, *args, &block)
171188
super unless @delegate.respond_to?(method)
172189
Async::validate_argc(@delegate, method, *args)
173190

174-
AsyncDelegator.send(:define_method, method) do |*args|
191+
self.define_singleton_method(method) do |*args|
175192
Async::validate_argc(@delegate, method, *args)
176193
Concurrent::Future.execute do
177194
mutex.synchronize do

spec/concurrent/async_spec.rb

Lines changed: 37 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -135,29 +135,29 @@ def many(*args, &block) nil; end
135135
val.should be_rejected
136136
end
137137

138-
it 'defines the method after the first call' do
139-
pending('failing inconsistently')
140-
expect { subject.async.method(:echo) }.to raise_error(NameError)
141-
subject.async.echo(:foo)
142-
sleep(0.1)
143-
expect { subject.async.method(:echo) }.not_to raise_error
138+
it 'is aliased as #future' do
139+
val = subject.future.wait(5)
140+
val.should be_a Concurrent::Future
144141
end
145142

146-
it 'does not define the method on name/arity exception' do
147-
pending('failing inconsistently')
148-
expect { subject.async.method(:bogus) }.to raise_error(NameError)
149-
expect { subject.async.bogus }.to raise_error(NameError)
150-
expect { subject.async.method(:bogus) }.to raise_error(NameError)
151-
end
143+
context '#method_missing' do
152144

153-
it 'uses the same mutex as #await' do
154-
subject.await.mutex.should eq subject.async.mutex
155-
end
145+
it 'defines the method after the first call' do
146+
expect { subject.async.method(:echo) }.to raise_error(NameError)
147+
subject.async.echo(:foo)
148+
sleep(0.1)
149+
expect { subject.async.method(:echo) }.not_to raise_error
150+
end
156151

157-
it 'is aliased as #future' do
158-
val = subject.future.wait(5)
159-
val.should be_a Concurrent::Future
160-
val.should be_pending
152+
it 'does not define the method on name/arity exception' do
153+
expect { subject.async.method(:bogus) }.to raise_error(NameError)
154+
expect { subject.async.bogus }.to raise_error(NameError)
155+
expect { subject.async.method(:bogus) }.to raise_error(NameError)
156+
end
157+
158+
it 'uses the same mutex as #await' do
159+
subject.await.mutex.should eq subject.async.mutex
160+
end
161161
end
162162
end
163163

@@ -206,28 +206,28 @@ def many(*args, &block) nil; end
206206
val.should be_rejected
207207
end
208208

209-
it 'defines the method after the first call' do
210-
pending('failing inconsistently')
211-
expect { subject.await.method(:echo) }.to raise_error(NameError)
212-
subject.await.echo(:foo)
213-
expect { subject.await.method(:echo) }.not_to raise_error
209+
it 'is aliased as #defer' do
210+
val = subject.defer.echo(5)
211+
val.should be_a Concurrent::IVar
214212
end
215213

216-
it 'does not define the method on name/arity exception' do
217-
pending('failing inconsistently')
218-
expect { subject.await.method(:bogus) }.to raise_error(NameError)
219-
expect { subject.await.bogus }.to raise_error(NameError)
220-
expect { subject.await.method(:bogus) }.to raise_error(NameError)
221-
end
214+
context '#method_missing' do
222215

223-
it 'uses the same mutex as #async' do
224-
subject.await.mutex.should eq subject.async.mutex
225-
end
216+
it 'defines the method after the first call' do
217+
expect { subject.await.method(:echo) }.to raise_error(NameError)
218+
subject.await.echo(:foo)
219+
expect { subject.await.method(:echo) }.not_to raise_error
220+
end
226221

227-
it 'is aliased as #defer' do
228-
val = subject.defer.echo(5)
229-
val.should be_a Concurrent::IVar
230-
val.should be_fulfilled
222+
it 'does not define the method on name/arity exception' do
223+
expect { subject.await.method(:bogus) }.to raise_error(NameError)
224+
expect { subject.await.bogus }.to raise_error(NameError)
225+
expect { subject.await.method(:bogus) }.to raise_error(NameError)
226+
end
227+
228+
it 'uses the same mutex as #async' do
229+
subject.await.mutex.should eq subject.async.mutex
230+
end
231231
end
232232
end
233233
end

0 commit comments

Comments
 (0)