celluloid 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +14 -0
- data/.document +5 -0
- data/.rspec +3 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +34 -0
- data/LICENSE.txt +20 -0
- data/README.md +286 -0
- data/Rakefile +51 -0
- data/VERSION +1 -0
- data/celluloid.gemspec +87 -0
- data/lib/celluloid.rb +19 -0
- data/lib/celluloid/actor.rb +179 -0
- data/lib/celluloid/actor_proxy.rb +81 -0
- data/lib/celluloid/calls.rb +56 -0
- data/lib/celluloid/core_ext.rb +6 -0
- data/lib/celluloid/events.rb +14 -0
- data/lib/celluloid/future.rb +28 -0
- data/lib/celluloid/linking.rb +75 -0
- data/lib/celluloid/mailbox.rb +97 -0
- data/lib/celluloid/registry.rb +33 -0
- data/lib/celluloid/responses.rb +16 -0
- data/lib/celluloid/supervisor.rb +40 -0
- data/lib/celluloid/waker.rb +41 -0
- data/spec/actor_spec.rb +141 -0
- data/spec/future_spec.rb +20 -0
- data/spec/mailbox_spec.rb +50 -0
- data/spec/registry_spec.rb +22 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/supervisor_spec.rb +94 -0
- data/spec/waker_spec.rb +34 -0
- metadata +138 -0
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Celluloid
|
4
|
+
class MailboxError < StandardError; end # you can't message the dead
|
5
|
+
|
6
|
+
# Actors communicate with asynchronous messages. Messages are buffered in
|
7
|
+
# Mailboxes until Actors can act upon them.
|
8
|
+
class Mailbox
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@messages = []
|
13
|
+
@lock = Mutex.new
|
14
|
+
@waker = Waker.new
|
15
|
+
end
|
16
|
+
|
17
|
+
# Add a message to the Mailbox
|
18
|
+
def <<(message)
|
19
|
+
@lock.synchronize do
|
20
|
+
@messages << message
|
21
|
+
@waker.signal
|
22
|
+
end
|
23
|
+
nil
|
24
|
+
rescue WakerError
|
25
|
+
raise MailboxError, "dead recipient"
|
26
|
+
end
|
27
|
+
|
28
|
+
# Add a high-priority system event to the Mailbox
|
29
|
+
def system_event(event)
|
30
|
+
@lock.synchronize do
|
31
|
+
@messages.unshift event
|
32
|
+
|
33
|
+
begin
|
34
|
+
@waker.signal
|
35
|
+
rescue WakerError
|
36
|
+
# Silently fail if messages are sent to dead actors
|
37
|
+
end
|
38
|
+
end
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
|
42
|
+
# Receive a message from the Mailbox
|
43
|
+
def receive(&block)
|
44
|
+
message = nil
|
45
|
+
|
46
|
+
begin
|
47
|
+
@waker.wait
|
48
|
+
message = locate(&block)
|
49
|
+
raise message if message.is_a?(Celluloid::SystemEvent)
|
50
|
+
end while message.nil?
|
51
|
+
|
52
|
+
message
|
53
|
+
rescue Celluloid::WakerError
|
54
|
+
cleanup # force cleanup of the mailbox
|
55
|
+
raise MailboxError, "mailbox cleanup called during receive"
|
56
|
+
end
|
57
|
+
|
58
|
+
# Locate and remove a message from the mailbox which matches the given filter
|
59
|
+
def locate
|
60
|
+
@lock.synchronize do
|
61
|
+
if block_given?
|
62
|
+
index = @messages.index do |msg|
|
63
|
+
yield(msg) || msg.is_a?(Celluloid::SystemEvent)
|
64
|
+
end
|
65
|
+
|
66
|
+
@messages.slice!(index, 1).first if index
|
67
|
+
else
|
68
|
+
@messages.shift
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Cleanup any IO objects this Mailbox may be using
|
74
|
+
def cleanup
|
75
|
+
@lock.synchronize do
|
76
|
+
@waker.cleanup
|
77
|
+
@messages.each { |msg| msg.cleanup if msg.respond_to? :cleanup }
|
78
|
+
@messages.clear
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Cast to an array
|
83
|
+
def to_a
|
84
|
+
@lock.synchronize { @messages.dup }
|
85
|
+
end
|
86
|
+
|
87
|
+
# Iterate through the mailbox
|
88
|
+
def each(&block)
|
89
|
+
to_a.each(&block)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Inspect the contents of the Mailbox
|
93
|
+
def inspect
|
94
|
+
"#<Celluloid::Mailbox[#{map { |m| m.inspect }.join(', ')}]>"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Celluloid
|
4
|
+
# The Registry allows us to refer to specific actors by human-meaningful names
|
5
|
+
module Registry
|
6
|
+
@@registry = {}
|
7
|
+
@@registry_lock = Mutex.new
|
8
|
+
|
9
|
+
# Register an Actor
|
10
|
+
def []=(name, actor)
|
11
|
+
actor_singleton = class << actor; self; end
|
12
|
+
unless actor_singleton.ancestors.include?(Celluloid::ActorProxy)
|
13
|
+
raise ArgumentError, "not an actor"
|
14
|
+
end
|
15
|
+
|
16
|
+
@@registry_lock.synchronize do
|
17
|
+
@@registry[name.to_sym] = actor
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Retrieve an actor by name
|
22
|
+
def [](name)
|
23
|
+
@@registry_lock.synchronize do
|
24
|
+
@@registry[name.to_sym]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Extend the actor module with the registry methods
|
30
|
+
module Actor
|
31
|
+
extend Registry
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Celluloid
|
2
|
+
# Responses to calls
|
3
|
+
class Response
|
4
|
+
attr_reader :call, :value
|
5
|
+
|
6
|
+
def initialize(call, value)
|
7
|
+
@call, @value = call, value
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# Call completed successfully
|
12
|
+
class SuccessResponse < Response; end
|
13
|
+
|
14
|
+
# Call was aborted due to caller error
|
15
|
+
class ErrorResponse < Response; end
|
16
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Celluloid
|
2
|
+
# Supervisors are actors that watch over other actors and restart them if
|
3
|
+
# they crash
|
4
|
+
class Supervisor
|
5
|
+
include Actor
|
6
|
+
trap_exit :restart_actor
|
7
|
+
|
8
|
+
# Retrieve the actor this supervisor is supervising
|
9
|
+
attr_reader :actor
|
10
|
+
|
11
|
+
def self.supervise(klass, *args, &block)
|
12
|
+
spawn(nil, klass, *args, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.supervise_as(name, klass, *args, &block)
|
16
|
+
spawn(name, klass, *args, &block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(name, klass, *args, &block)
|
20
|
+
@name, @klass, @args, @block = name, klass, args, block
|
21
|
+
start_actor
|
22
|
+
end
|
23
|
+
|
24
|
+
def start_actor
|
25
|
+
@actor = @klass.spawn_link(*@args, &@block)
|
26
|
+
Celluloid::Actor[@name] = @actor if @name
|
27
|
+
end
|
28
|
+
|
29
|
+
# When actors die, regardless of the reason, restart them
|
30
|
+
def restart_actor(actor, reason)
|
31
|
+
start_actor
|
32
|
+
end
|
33
|
+
|
34
|
+
def inspect
|
35
|
+
str = "#<Celluloid::Supervisor(#{@klass})"
|
36
|
+
str << " " << @args.map { |arg| arg.inspect }.join(' ') unless @args.empty?
|
37
|
+
str << ">"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Celluloid
|
2
|
+
class WakerError < StandardError; end # You can't wake the dead
|
3
|
+
|
4
|
+
# Wakes up sleepy threads so that they can check their mailbox
|
5
|
+
# Works like a ConditionVariable, except it's implemented as an IO object so
|
6
|
+
# that it can be multiplexed alongside other IO objects.
|
7
|
+
class Waker
|
8
|
+
PAYLOAD = "\0" # the payload doesn't matter. each byte is a signal
|
9
|
+
def initialize
|
10
|
+
@receiver, @sender = IO.pipe
|
11
|
+
end
|
12
|
+
|
13
|
+
# Wakes up the thread that is waiting for this Waker
|
14
|
+
def signal
|
15
|
+
@sender << PAYLOAD
|
16
|
+
nil
|
17
|
+
rescue IOError, Errno::EPIPE, Errno::EBADF
|
18
|
+
raise WakerError, "waker is already dead"
|
19
|
+
end
|
20
|
+
|
21
|
+
# Wait for another thread to signal this Waker
|
22
|
+
def wait
|
23
|
+
byte = @receiver.read(1)
|
24
|
+
raise WakerError, "can't wait on a dead waker" unless byte == PAYLOAD
|
25
|
+
rescue IOError
|
26
|
+
raise WakerError, "can't wait on a dead waker"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Return the IO object which will be readable when this Waker is signaled
|
30
|
+
def io
|
31
|
+
@receiver
|
32
|
+
end
|
33
|
+
|
34
|
+
# Clean up the IO objects associated with this waker
|
35
|
+
def cleanup
|
36
|
+
@receiver.close rescue nil
|
37
|
+
@sender.close rescue nil
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/spec/actor_spec.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Celluloid::Actor do
|
4
|
+
before do
|
5
|
+
class MyActor
|
6
|
+
include Celluloid::Actor
|
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 greet
|
18
|
+
"Hi, I'm #{@name}"
|
19
|
+
end
|
20
|
+
|
21
|
+
class Crash < StandardError; end
|
22
|
+
def crash
|
23
|
+
raise Crash, "the spec purposely crashed me :("
|
24
|
+
end
|
25
|
+
|
26
|
+
def crash_with_abort(reason)
|
27
|
+
abort Crash.new(reason)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it "handles synchronous calls" do
|
33
|
+
actor = MyActor.spawn "Troy McClure"
|
34
|
+
actor.greet.should == "Hi, I'm Troy McClure"
|
35
|
+
end
|
36
|
+
|
37
|
+
it "raises NoMethodError when a nonexistent method is called" do
|
38
|
+
actor = MyActor.spawn "Billy Bob Thornton"
|
39
|
+
|
40
|
+
proc do
|
41
|
+
actor.the_method_that_wasnt_there
|
42
|
+
end.should raise_exception(NoMethodError)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "reraises exceptions which occur during synchronous calls in the caller" do
|
46
|
+
actor = MyActor.spawn "James Dean" # is this in bad taste?
|
47
|
+
|
48
|
+
proc do
|
49
|
+
actor.crash
|
50
|
+
end.should raise_exception(MyActor::Crash)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "raises exceptions in the caller when abort is called, but keeps running" do
|
54
|
+
actor = MyActor.spawn "Al Pacino"
|
55
|
+
|
56
|
+
proc do
|
57
|
+
actor.crash_with_abort MyActor::Crash.new("You die motherfucker!")
|
58
|
+
end.should raise_exception(MyActor::Crash)
|
59
|
+
|
60
|
+
actor.should be_alive
|
61
|
+
end
|
62
|
+
|
63
|
+
it "raises DeadActorError if methods are synchronously called on a dead actor" do
|
64
|
+
actor = MyActor.spawn "James Dean"
|
65
|
+
actor.crash rescue nil
|
66
|
+
|
67
|
+
proc do
|
68
|
+
actor.greet
|
69
|
+
end.should raise_exception(Celluloid::DeadActorError)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "handles asynchronous calls" do
|
73
|
+
actor = MyActor.spawn "Troy McClure"
|
74
|
+
actor.change_name! "Charlie Sheen"
|
75
|
+
actor.greet.should == "Hi, I'm Charlie Sheen"
|
76
|
+
end
|
77
|
+
|
78
|
+
it "knows when it's running as an actor" do
|
79
|
+
obj = MyActor.new "I'm an object"
|
80
|
+
obj.actor?.should be_false
|
81
|
+
|
82
|
+
actor = MyActor.spawn "Troy McClure"
|
83
|
+
actor.actor?.should be_true
|
84
|
+
end
|
85
|
+
|
86
|
+
it "inspects properly" do
|
87
|
+
actor = MyActor.spawn "Troy McClure"
|
88
|
+
actor.inspect.should match(/Celluloid::Actor\(MyActor/)
|
89
|
+
actor.inspect.should include('@name="Troy McClure"')
|
90
|
+
actor.inspect.should_not include("@celluloid")
|
91
|
+
end
|
92
|
+
|
93
|
+
context :linking do
|
94
|
+
before :each do
|
95
|
+
@kevin = MyActor.spawn "Kevin Bacon" # Some six degrees action here
|
96
|
+
@charlie = MyActor.spawn "Charlie Sheen"
|
97
|
+
end
|
98
|
+
|
99
|
+
it "links to other actors" do
|
100
|
+
@kevin.link @charlie
|
101
|
+
@kevin.linked_to?(@charlie).should be_true
|
102
|
+
@charlie.linked_to?(@kevin).should be_true
|
103
|
+
end
|
104
|
+
|
105
|
+
it "unlinks from other actors" do
|
106
|
+
@kevin.link @charlie
|
107
|
+
@kevin.unlink @charlie
|
108
|
+
|
109
|
+
@kevin.linked_to?(@charlie).should be_false
|
110
|
+
@charlie.linked_to?(@kevin).should be_false
|
111
|
+
end
|
112
|
+
|
113
|
+
it "traps exit messages from other actors" do
|
114
|
+
class Boss # like a boss
|
115
|
+
include Celluloid::Actor
|
116
|
+
trap_exit :lambaste_subordinate
|
117
|
+
|
118
|
+
def initialize(name)
|
119
|
+
@name = name
|
120
|
+
@subordinate_lambasted = false
|
121
|
+
end
|
122
|
+
|
123
|
+
def subordinate_lambasted?; @subordinate_lambasted; end
|
124
|
+
|
125
|
+
def lambaste_subordinate(actor, reason)
|
126
|
+
@subordinate_lambasted = true
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
chuck = Boss.spawn "Chuck Lorre"
|
131
|
+
chuck.link @charlie
|
132
|
+
|
133
|
+
proc do
|
134
|
+
@charlie.crash
|
135
|
+
end.should raise_exception(MyActor::Crash)
|
136
|
+
|
137
|
+
sleep 0.1 # hax to prevent a race between exit handling and the next call
|
138
|
+
chuck.should be_subordinate_lambasted
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
data/spec/future_spec.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Celluloid::Future do
|
4
|
+
it "creates future objects that can be retrieved later" do
|
5
|
+
future = Celluloid::Future() { 40 + 2 }
|
6
|
+
future.value.should == 42
|
7
|
+
end
|
8
|
+
|
9
|
+
it "passes arguments to future blocks" do
|
10
|
+
future = Celluloid::Future(40) { |n| n + 2 }
|
11
|
+
future.value.should == 42
|
12
|
+
end
|
13
|
+
|
14
|
+
it "reraises exceptions that occur when the value is retrieved" do
|
15
|
+
class ExampleError < StandardError; end
|
16
|
+
|
17
|
+
future = Celluloid::Future() { raise ExampleError, "oh noes crash!" }
|
18
|
+
proc { future.value }.should raise_exception(ExampleError)
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class TestEvent < Celluloid::SystemEvent; end
|
4
|
+
|
5
|
+
describe Celluloid::Mailbox do
|
6
|
+
before :each do
|
7
|
+
@mailbox = Celluloid::Mailbox.new
|
8
|
+
end
|
9
|
+
|
10
|
+
it "receives messages" do
|
11
|
+
message = :ohai
|
12
|
+
|
13
|
+
@mailbox << message
|
14
|
+
@mailbox.receive.should == message
|
15
|
+
end
|
16
|
+
|
17
|
+
it "raises system events when received" do
|
18
|
+
@mailbox.system_event TestEvent.new("example")
|
19
|
+
|
20
|
+
proc do
|
21
|
+
@mailbox.receive
|
22
|
+
end.should raise_exception(TestEvent)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "prioritizes system events over other messages" do
|
26
|
+
@mailbox << :dummy1
|
27
|
+
@mailbox << :dummy2
|
28
|
+
@mailbox.system_event TestEvent.new("example")
|
29
|
+
|
30
|
+
proc do
|
31
|
+
@mailbox.receive
|
32
|
+
end.should raise_exception(TestEvent)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "selectively receives messages with a block" do
|
36
|
+
class Foo; end
|
37
|
+
class Bar; end
|
38
|
+
class Baz; end
|
39
|
+
|
40
|
+
foo, bar, baz = Foo.new, Bar.new, Baz.new
|
41
|
+
|
42
|
+
@mailbox << baz
|
43
|
+
@mailbox << foo
|
44
|
+
@mailbox << bar
|
45
|
+
|
46
|
+
@mailbox.receive { |msg| msg.is_a? Foo }.should == foo
|
47
|
+
@mailbox.receive { |msg| msg.is_a? Bar }.should == bar
|
48
|
+
@mailbox.receive.should == baz
|
49
|
+
end
|
50
|
+
end
|