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.
@@ -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
@@ -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
@@ -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