celluloid 0.0.1
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/.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
|