celluloid 0.6.2 → 0.7.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.md +37 -482
- data/lib/celluloid.rb +45 -18
- data/lib/celluloid/actor.rb +64 -22
- data/lib/celluloid/actor_pool.rb +1 -1
- data/lib/celluloid/actor_proxy.rb +1 -1
- data/lib/celluloid/application.rb +1 -1
- data/lib/celluloid/calls.rb +2 -2
- data/lib/celluloid/fiber.rb +2 -31
- data/lib/celluloid/fsm.rb +141 -0
- data/lib/celluloid/future.rb +10 -13
- data/lib/celluloid/logger.rb +8 -3
- data/lib/celluloid/mailbox.rb +16 -4
- data/lib/celluloid/receivers.rb +49 -19
- data/lib/celluloid/registry.rb +2 -2
- data/lib/celluloid/signals.rb +12 -10
- data/lib/celluloid/supervisor.rb +7 -4
- data/lib/celluloid/task.rb +53 -0
- data/lib/celluloid/tcp_server.rb +2 -1
- data/lib/celluloid/timers.rb +109 -0
- data/lib/celluloid/version.rb +1 -1
- data/spec/support/actor_examples.rb +453 -0
- data/spec/support/mailbox_examples.rb +52 -0
- metadata +11 -11
- data/lib/celluloid/io.rb +0 -24
- data/lib/celluloid/io/mailbox.rb +0 -65
- data/lib/celluloid/io/reactor.rb +0 -63
- data/lib/celluloid/io/waker.rb +0 -43
data/lib/celluloid/version.rb
CHANGED
@@ -0,0 +1,453 @@
|
|
1
|
+
shared_context "a Celluloid Actor" do |included_module|
|
2
|
+
class ExampleCrash < StandardError; end
|
3
|
+
|
4
|
+
let :actor_class do
|
5
|
+
Class.new do
|
6
|
+
include included_module
|
7
|
+
attr_reader :name
|
8
|
+
|
9
|
+
def initialize(name)
|
10
|
+
@name = name
|
11
|
+
end
|
12
|
+
|
13
|
+
def change_name(new_name)
|
14
|
+
@name = new_name
|
15
|
+
end
|
16
|
+
|
17
|
+
def change_name_async(new_name)
|
18
|
+
change_name! new_name
|
19
|
+
end
|
20
|
+
|
21
|
+
def greet
|
22
|
+
"Hi, I'm #{@name}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def run(*args)
|
26
|
+
yield(*args)
|
27
|
+
end
|
28
|
+
|
29
|
+
def crash
|
30
|
+
raise ExampleCrash, "the spec purposely crashed me :("
|
31
|
+
end
|
32
|
+
|
33
|
+
def crash_with_abort(reason)
|
34
|
+
abort ExampleCrash.new(reason)
|
35
|
+
end
|
36
|
+
|
37
|
+
def internal_hello
|
38
|
+
external_hello
|
39
|
+
end
|
40
|
+
|
41
|
+
def external_hello
|
42
|
+
"Hello"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
it "returns the actor's class, not the proxy's" do
|
48
|
+
actor = actor_class.new "Troy McClure"
|
49
|
+
actor.class.should == actor_class
|
50
|
+
end
|
51
|
+
|
52
|
+
it "handles synchronous calls" do
|
53
|
+
actor = actor_class.new "Troy McClure"
|
54
|
+
actor.greet.should == "Hi, I'm Troy McClure"
|
55
|
+
end
|
56
|
+
|
57
|
+
it "handles futures for synchronous calls" do
|
58
|
+
actor = actor_class.new "Troy McClure"
|
59
|
+
future = actor.future :greet
|
60
|
+
future.value.should == "Hi, I'm Troy McClure"
|
61
|
+
end
|
62
|
+
|
63
|
+
it "handles circular synchronous calls" do
|
64
|
+
klass = Class.new do
|
65
|
+
include included_module
|
66
|
+
|
67
|
+
def greet_by_proxy(actor)
|
68
|
+
actor.greet
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_s
|
72
|
+
"a ponycopter!"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
ponycopter = klass.new
|
77
|
+
actor = actor_class.new ponycopter
|
78
|
+
ponycopter.greet_by_proxy(actor).should == "Hi, I'm a ponycopter!"
|
79
|
+
end
|
80
|
+
|
81
|
+
it "raises NoMethodError when a nonexistent method is called" do
|
82
|
+
actor = actor_class.new "Billy Bob Thornton"
|
83
|
+
|
84
|
+
expect do
|
85
|
+
actor.the_method_that_wasnt_there
|
86
|
+
end.to raise_exception(NoMethodError)
|
87
|
+
end
|
88
|
+
|
89
|
+
it "reraises exceptions which occur during synchronous calls in the caller" do
|
90
|
+
actor = actor_class.new "James Dean" # is this in bad taste?
|
91
|
+
|
92
|
+
expect do
|
93
|
+
actor.crash
|
94
|
+
end.to raise_exception(ExampleCrash)
|
95
|
+
end
|
96
|
+
|
97
|
+
it "raises exceptions in the caller when abort is called, but keeps running" do
|
98
|
+
actor = actor_class.new "Al Pacino"
|
99
|
+
|
100
|
+
expect do
|
101
|
+
actor.crash_with_abort ExampleCrash.new("You die motherfucker!")
|
102
|
+
end.to raise_exception(ExampleCrash)
|
103
|
+
|
104
|
+
actor.should be_alive
|
105
|
+
end
|
106
|
+
|
107
|
+
it "raises DeadActorError if methods are synchronously called on a dead actor" do
|
108
|
+
actor = actor_class.new "James Dean"
|
109
|
+
actor.crash rescue nil
|
110
|
+
|
111
|
+
expect do
|
112
|
+
actor.greet
|
113
|
+
end.to raise_exception(Celluloid::DeadActorError)
|
114
|
+
end
|
115
|
+
|
116
|
+
it "handles asynchronous calls" do
|
117
|
+
actor = actor_class.new "Troy McClure"
|
118
|
+
actor.change_name! "Charlie Sheen"
|
119
|
+
actor.greet.should == "Hi, I'm Charlie Sheen"
|
120
|
+
end
|
121
|
+
|
122
|
+
it "handles asynchronous calls to itself" do
|
123
|
+
actor = actor_class.new "Troy McClure"
|
124
|
+
actor.change_name_async "Charlie Sheen"
|
125
|
+
actor.greet.should == "Hi, I'm Charlie Sheen"
|
126
|
+
end
|
127
|
+
|
128
|
+
it "knows if it's inside actor scope" do
|
129
|
+
Celluloid.should_not be_actor
|
130
|
+
actor = actor_class.new "Troy McClure"
|
131
|
+
actor.run do
|
132
|
+
Celluloid.actor?
|
133
|
+
end.should be_true
|
134
|
+
end
|
135
|
+
|
136
|
+
it "inspects properly" do
|
137
|
+
actor = actor_class.new "Troy McClure"
|
138
|
+
actor.inspect.should match(/Celluloid::Actor\(/)
|
139
|
+
actor.inspect.should include('@name="Troy McClure"')
|
140
|
+
actor.inspect.should_not include("@celluloid")
|
141
|
+
end
|
142
|
+
|
143
|
+
it "inspects properly when dead" do
|
144
|
+
actor = actor_class.new "Troy McClure"
|
145
|
+
actor.terminate
|
146
|
+
actor.inspect.should match(/Celluloid::Actor\(/)
|
147
|
+
actor.inspect.should include('dead')
|
148
|
+
end
|
149
|
+
|
150
|
+
it "allows access to the wrapped object" do
|
151
|
+
actor = actor_class.new "Troy McClure"
|
152
|
+
actor.wrapped_object.should be_a actor_class
|
153
|
+
end
|
154
|
+
|
155
|
+
describe 'mocking methods' do
|
156
|
+
let(:actor) { actor_class.new "Troy McClure" }
|
157
|
+
|
158
|
+
before do
|
159
|
+
actor.wrapped_object.should_receive(:external_hello).once.and_return "World"
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'works externally via the proxy' do
|
163
|
+
actor.external_hello.should == "World"
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'works internally when called on self' do
|
167
|
+
actor.internal_hello.should == "World"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
context :termination do
|
172
|
+
it "terminates" do
|
173
|
+
actor = actor_class.new "Arnold Schwarzenegger"
|
174
|
+
actor.should be_alive
|
175
|
+
actor.terminate
|
176
|
+
sleep 0.5 # hax
|
177
|
+
actor.should_not be_alive
|
178
|
+
end
|
179
|
+
|
180
|
+
it "raises DeadActorError if called after terminated" do
|
181
|
+
actor = actor_class.new "Arnold Schwarzenegger"
|
182
|
+
actor.terminate
|
183
|
+
|
184
|
+
expect do
|
185
|
+
actor.greet
|
186
|
+
end.to raise_exception(Celluloid::DeadActorError)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
context :current_actor do
|
191
|
+
it "knows the current actor" do
|
192
|
+
actor = actor_class.new "Roger Daltrey"
|
193
|
+
actor.current_actor.should == actor
|
194
|
+
end
|
195
|
+
|
196
|
+
it "raises NotActorError if called outside an actor" do
|
197
|
+
expect do
|
198
|
+
Celluloid.current_actor
|
199
|
+
end.to raise_exception(Celluloid::NotActorError)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
context :linking do
|
204
|
+
before :each do
|
205
|
+
@kevin = actor_class.new "Kevin Bacon" # Some six degrees action here
|
206
|
+
@charlie = actor_class.new "Charlie Sheen"
|
207
|
+
end
|
208
|
+
|
209
|
+
it "links to other actors" do
|
210
|
+
@kevin.link @charlie
|
211
|
+
@kevin.linked_to?(@charlie).should be_true
|
212
|
+
@charlie.linked_to?(@kevin).should be_true
|
213
|
+
end
|
214
|
+
|
215
|
+
it "unlinks from other actors" do
|
216
|
+
@kevin.link @charlie
|
217
|
+
@kevin.unlink @charlie
|
218
|
+
|
219
|
+
@kevin.linked_to?(@charlie).should be_false
|
220
|
+
@charlie.linked_to?(@kevin).should be_false
|
221
|
+
end
|
222
|
+
|
223
|
+
it "traps exit messages from other actors" do
|
224
|
+
boss = Class.new do # like a boss
|
225
|
+
include included_module
|
226
|
+
trap_exit :lambaste_subordinate
|
227
|
+
|
228
|
+
def initialize(name)
|
229
|
+
@name = name
|
230
|
+
@subordinate_lambasted = false
|
231
|
+
end
|
232
|
+
|
233
|
+
def subordinate_lambasted?; @subordinate_lambasted; end
|
234
|
+
|
235
|
+
def lambaste_subordinate(actor, reason)
|
236
|
+
@subordinate_lambasted = true
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
chuck = boss.new "Chuck Lorre"
|
241
|
+
chuck.link @charlie
|
242
|
+
|
243
|
+
expect do
|
244
|
+
@charlie.crash
|
245
|
+
end.to raise_exception(ExampleCrash)
|
246
|
+
|
247
|
+
sleep 0.1 # hax to prevent a race between exit handling and the next call
|
248
|
+
chuck.should be_subordinate_lambasted
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
context :signaling do
|
253
|
+
before do
|
254
|
+
@signaler = Class.new do
|
255
|
+
include included_module
|
256
|
+
attr_reader :signaled
|
257
|
+
|
258
|
+
def initialize
|
259
|
+
@signaled = false
|
260
|
+
end
|
261
|
+
|
262
|
+
def wait_for_signal
|
263
|
+
value = wait :ponycopter
|
264
|
+
@signaled = true
|
265
|
+
value
|
266
|
+
end
|
267
|
+
|
268
|
+
def send_signal(value)
|
269
|
+
signal :ponycopter, value
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
it "allows methods within the same object to signal each other" do
|
275
|
+
obj = @signaler.new
|
276
|
+
obj.signaled.should be_false
|
277
|
+
|
278
|
+
obj.wait_for_signal!
|
279
|
+
obj.signaled.should be_false
|
280
|
+
|
281
|
+
obj.send_signal :foobar
|
282
|
+
obj.signaled.should be_true
|
283
|
+
end
|
284
|
+
|
285
|
+
# FIXME: This is deadlocking on Travis, and may still have issues
|
286
|
+
it "sends values along with signals", :pending => ENV['CI'] do
|
287
|
+
obj = @signaler.new
|
288
|
+
obj.signaled.should be_false
|
289
|
+
|
290
|
+
future = Celluloid::Future.new { obj.wait_for_signal }
|
291
|
+
obj.signaled.should be_false
|
292
|
+
|
293
|
+
obj.send_signal :foobar
|
294
|
+
future.value.should == :foobar
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
context :receiving do
|
299
|
+
before do
|
300
|
+
@receiver = Class.new do
|
301
|
+
include included_module
|
302
|
+
|
303
|
+
def signal_myself(obj, &block)
|
304
|
+
current_actor.mailbox << obj
|
305
|
+
receive(&block)
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
it "allows arbitrary selective receive" do
|
311
|
+
obj = Object.new
|
312
|
+
receiver = @receiver.new
|
313
|
+
received_obj = receiver.signal_myself(obj) { |o| o == obj }
|
314
|
+
received_obj.should == obj
|
315
|
+
end
|
316
|
+
|
317
|
+
it "times out after the given interval" do
|
318
|
+
interval = 0.1
|
319
|
+
started_at = Time.now
|
320
|
+
receiver = @receiver.new
|
321
|
+
|
322
|
+
receiver.receive(interval) { false }.should be_nil
|
323
|
+
(Time.now - started_at).should be_within(Celluloid::Timer::QUANTUM).of interval
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
context :timers do
|
328
|
+
before do
|
329
|
+
@klass = Class.new do
|
330
|
+
include included_module
|
331
|
+
|
332
|
+
def initialize
|
333
|
+
@sleeping = false
|
334
|
+
@fired = false
|
335
|
+
end
|
336
|
+
|
337
|
+
def do_sleep(n)
|
338
|
+
@sleeping = true
|
339
|
+
sleep n
|
340
|
+
@sleeping = false
|
341
|
+
end
|
342
|
+
|
343
|
+
def sleeping?; @sleeping end
|
344
|
+
|
345
|
+
def fire_after(n)
|
346
|
+
after(n) { @fired = true }
|
347
|
+
end
|
348
|
+
|
349
|
+
def fired?; @fired end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
it "suspends execution of a method (but not the actor) for a given time" do
|
354
|
+
actor = @klass.new
|
355
|
+
|
356
|
+
# Sleep long enough to ensure we're actually seeing behavior when asleep
|
357
|
+
# but not so long as to delay the test suite unnecessarily
|
358
|
+
interval = Celluloid::Timer::QUANTUM * 10
|
359
|
+
started_at = Time.now
|
360
|
+
|
361
|
+
future = actor.future(:do_sleep, interval)
|
362
|
+
sleep(interval / 2) # wonky! :/
|
363
|
+
actor.should be_sleeping
|
364
|
+
|
365
|
+
future.value
|
366
|
+
(Time.now - started_at).should be_within(Celluloid::Timer::QUANTUM).of interval
|
367
|
+
end
|
368
|
+
|
369
|
+
it "schedules timers which fire in the future" do
|
370
|
+
actor = @klass.new
|
371
|
+
|
372
|
+
interval = Celluloid::Timer::QUANTUM * 10
|
373
|
+
started_at = Time.now
|
374
|
+
|
375
|
+
timer = actor.fire_after(interval)
|
376
|
+
actor.should_not be_fired
|
377
|
+
|
378
|
+
sleep(interval + Celluloid::Timer::QUANTUM) # wonky! #/
|
379
|
+
actor.should be_fired
|
380
|
+
end
|
381
|
+
|
382
|
+
it "cancels timers before they fire" do
|
383
|
+
actor = @klass.new
|
384
|
+
|
385
|
+
interval = Celluloid::Timer::QUANTUM * 10
|
386
|
+
started_at = Time.now
|
387
|
+
|
388
|
+
timer = actor.fire_after(interval)
|
389
|
+
actor.should_not be_fired
|
390
|
+
timer.cancel
|
391
|
+
|
392
|
+
sleep(interval + Celluloid::Timer::QUANTUM) # wonky! #/
|
393
|
+
actor.should_not be_fired
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
context :tasks do
|
398
|
+
before do
|
399
|
+
@klass = Class.new do
|
400
|
+
include included_module
|
401
|
+
attr_reader :blocker
|
402
|
+
|
403
|
+
def initialize
|
404
|
+
@blocker = Blocker.new
|
405
|
+
end
|
406
|
+
|
407
|
+
def blocking_call
|
408
|
+
@blocker.block
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
class Blocker
|
413
|
+
include Celluloid
|
414
|
+
|
415
|
+
def block
|
416
|
+
wait :unblock
|
417
|
+
end
|
418
|
+
|
419
|
+
def unblock
|
420
|
+
signal :unblock
|
421
|
+
end
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
it "knows which tasks are waiting on calls to other actors" do
|
426
|
+
actor = @klass.new
|
427
|
+
|
428
|
+
# an alias for Celluloid::Actor#waiting_tasks
|
429
|
+
tasks = actor.tasks
|
430
|
+
tasks.size.should == 1
|
431
|
+
tasks.values.first.should == :running
|
432
|
+
|
433
|
+
future = actor.future(:blocking_call)
|
434
|
+
sleep 0.1 # hax! waiting for ^^^ call to actually start
|
435
|
+
|
436
|
+
tasks = actor.tasks
|
437
|
+
tasks.size.should == 2
|
438
|
+
|
439
|
+
blocking_task = nil
|
440
|
+
tasks.each do |task, waitable|
|
441
|
+
next if waitable == :running
|
442
|
+
blocking_task = task
|
443
|
+
break
|
444
|
+
end
|
445
|
+
|
446
|
+
tasks[blocking_task].first.should == :call
|
447
|
+
|
448
|
+
actor.blocker.unblock
|
449
|
+
sleep 0.1 # hax again :(
|
450
|
+
actor.tasks.size.should == 1
|
451
|
+
end
|
452
|
+
end
|
453
|
+
end
|