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
data/lib/celluloid.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module Celluloid
|
2
|
+
VERSION = File.read File.expand_path('../../VERSION', __FILE__)
|
3
|
+
|
4
|
+
def self.version; VERSION; end
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'celluloid/actor'
|
8
|
+
require 'celluloid/actor_proxy'
|
9
|
+
require 'celluloid/calls'
|
10
|
+
require 'celluloid/core_ext'
|
11
|
+
require 'celluloid/events'
|
12
|
+
require 'celluloid/linking'
|
13
|
+
require 'celluloid/mailbox'
|
14
|
+
require 'celluloid/registry'
|
15
|
+
require 'celluloid/responses'
|
16
|
+
require 'celluloid/supervisor'
|
17
|
+
require 'celluloid/waker'
|
18
|
+
|
19
|
+
require 'celluloid/future'
|
@@ -0,0 +1,179 @@
|
|
1
|
+
module Celluloid
|
2
|
+
# Raised when trying to do Actor-like things outside Actor-space
|
3
|
+
class NotActorError < StandardError; end
|
4
|
+
|
5
|
+
# Raised when we're asked to do something to a dead actor
|
6
|
+
class DeadActorError < StandardError; end
|
7
|
+
|
8
|
+
# Raised when the caller makes an error that shouldn't crash this actor
|
9
|
+
class AbortError < StandardError
|
10
|
+
attr_reader :cause
|
11
|
+
|
12
|
+
def initialize(cause)
|
13
|
+
@cause = cause
|
14
|
+
super "caused by #{cause.inspect}: #{cause.to_s}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Actors are Celluloid's concurrency primitive. They're implemented as
|
19
|
+
# normal Ruby objects wrapped in threads which communicate with asynchronous
|
20
|
+
# messages. The implementation is inspired by Erlang's gen_server
|
21
|
+
module Actor
|
22
|
+
attr_reader :mailbox
|
23
|
+
|
24
|
+
# Methods added to classes which include Celluloid::Actor
|
25
|
+
module ClassMethods
|
26
|
+
# Retrieve the exit handler method for this class
|
27
|
+
attr_reader :exit_handler
|
28
|
+
|
29
|
+
# Create a new actor
|
30
|
+
def spawn(*args, &block)
|
31
|
+
actor = allocate
|
32
|
+
proxy = actor.__start_actor
|
33
|
+
proxy.send(:initialize, *args, &block)
|
34
|
+
proxy
|
35
|
+
end
|
36
|
+
|
37
|
+
# Create a new actor and link to the current one
|
38
|
+
def spawn_link(*args, &block)
|
39
|
+
current_actor = Thread.current[:actor]
|
40
|
+
raise NotActorError, "can't link outside actor context" unless current_actor
|
41
|
+
|
42
|
+
# FIXME: this is a bit repetitive with the code above
|
43
|
+
actor = allocate
|
44
|
+
proxy = actor.__start_actor
|
45
|
+
current_actor.link actor
|
46
|
+
proxy.send(:initialize, *args, &block)
|
47
|
+
|
48
|
+
proxy
|
49
|
+
end
|
50
|
+
|
51
|
+
# Create a supervisor which ensures an instance of an actor will restart
|
52
|
+
# an actor if it fails
|
53
|
+
def supervise(*args, &block)
|
54
|
+
Celluloid::Supervisor.supervise(self, *args, &block)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Create a supervisor which ensures an instance of an actor will restart
|
58
|
+
# an actor if it fails, and keep the actor registered under a given name
|
59
|
+
def supervise_as(name, *args, &block)
|
60
|
+
Celluloid::Supervisor.supervise_as(name, self, *args, &block)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Trap errors from actors we're linked to when they exit
|
64
|
+
def trap_exit(callback)
|
65
|
+
@exit_handler = callback.to_sym
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Instance methods added to the public API
|
70
|
+
module InstanceMethods
|
71
|
+
# Is this object functioning as an actor?
|
72
|
+
def actor?
|
73
|
+
!!@mailbox
|
74
|
+
end
|
75
|
+
|
76
|
+
# Is this actor alive?
|
77
|
+
def alive?
|
78
|
+
@thread.alive?
|
79
|
+
end
|
80
|
+
|
81
|
+
# Raise an exception in caller context, but stay running
|
82
|
+
def abort(cause)
|
83
|
+
raise AbortError.new(cause)
|
84
|
+
end
|
85
|
+
|
86
|
+
def inspect
|
87
|
+
return super unless actor?
|
88
|
+
str = "#<Celluloid::Actor(#{self.class}:0x#{self.object_id.to_s(16)})"
|
89
|
+
|
90
|
+
ivars = []
|
91
|
+
instance_variables.each do |ivar|
|
92
|
+
next if %w(@mailbox @links @proxy @thread).include? ivar.to_s
|
93
|
+
ivars << "#{ivar}=#{instance_variable_get(ivar).inspect}"
|
94
|
+
end
|
95
|
+
|
96
|
+
str << " " << ivars.join(' ') unless ivars.empty?
|
97
|
+
str << ">"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Internal methods not intended as part of the public API
|
102
|
+
module InternalMethods
|
103
|
+
# Actor-specific initialization and startup
|
104
|
+
def __start_actor(*args, &block)
|
105
|
+
@mailbox = Mailbox.new
|
106
|
+
@links = Links.new
|
107
|
+
@proxy = ActorProxy.new(self, @mailbox)
|
108
|
+
|
109
|
+
@thread = Thread.new do
|
110
|
+
Thread.current[:actor] = self
|
111
|
+
Thread.current[:mailbox] = @mailbox
|
112
|
+
__run_actor
|
113
|
+
end
|
114
|
+
|
115
|
+
@proxy
|
116
|
+
end
|
117
|
+
|
118
|
+
# Run the actor
|
119
|
+
def __run_actor
|
120
|
+
__process_messages
|
121
|
+
rescue Exception => ex
|
122
|
+
__handle_crash(ex)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Process incoming messages
|
126
|
+
def __process_messages
|
127
|
+
while true # instead of loop, for speed!
|
128
|
+
begin
|
129
|
+
call = @mailbox.receive
|
130
|
+
rescue ExitEvent => event
|
131
|
+
__handle_exit(event)
|
132
|
+
retry
|
133
|
+
end
|
134
|
+
|
135
|
+
call.dispatch(self)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Handle exit events received by this actor
|
140
|
+
def __handle_exit(exit_event)
|
141
|
+
exit_handler = self.class.exit_handler
|
142
|
+
raise exit_event.reason unless exit_handler
|
143
|
+
|
144
|
+
send exit_handler, exit_event.actor, exit_event.reason
|
145
|
+
end
|
146
|
+
|
147
|
+
# Handle any exceptions that occur within a running actor
|
148
|
+
def __handle_crash(exception)
|
149
|
+
__log_error(exception)
|
150
|
+
@mailbox.cleanup
|
151
|
+
|
152
|
+
# Report the exit event to all actors we're linked to
|
153
|
+
exit_event = ExitEvent.new(@proxy, exception)
|
154
|
+
|
155
|
+
# Propagate the error to all linked actors
|
156
|
+
@links.each do |actor|
|
157
|
+
actor.mailbox.system_event exit_event
|
158
|
+
end
|
159
|
+
rescue Exception => handler_exception
|
160
|
+
__log_error(handler_exception, "/!\\ EXCEPTION IN ERROR HANDLER /!\\")
|
161
|
+
ensure
|
162
|
+
Thread.current.exit
|
163
|
+
end
|
164
|
+
|
165
|
+
# Log errors when an actor crashes
|
166
|
+
# FIXME: This should probably thunk to a real logger
|
167
|
+
def __log_error(ex, prefix = "!!! CRASH")
|
168
|
+
puts "#{prefix} #{self.class}: #{ex.class}: #{ex.to_s}\n#{ex.backtrace.join("\n")}"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def self.included(klass)
|
173
|
+
klass.extend ClassMethods
|
174
|
+
klass.send :include, InstanceMethods
|
175
|
+
klass.send :include, InternalMethods
|
176
|
+
klass.send :include, Linking
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Celluloid
|
2
|
+
# A proxy object returned from Celluloid::Actor.spawn/spawn_link which
|
3
|
+
# dispatches calls and casts to normal Ruby objects which are running inside
|
4
|
+
# of their own threads.
|
5
|
+
class ActorProxy
|
6
|
+
# FIXME: not nearly enough methods are delegated here
|
7
|
+
attr_reader :mailbox
|
8
|
+
|
9
|
+
def initialize(actor, mailbox)
|
10
|
+
@actor, @mailbox = actor, mailbox
|
11
|
+
end
|
12
|
+
|
13
|
+
def send(meth, *args, &block)
|
14
|
+
__call :send, meth, *args, &block
|
15
|
+
end
|
16
|
+
|
17
|
+
def respond_to?(meth)
|
18
|
+
__call :respond_to?, meth
|
19
|
+
end
|
20
|
+
|
21
|
+
def methods(include_ancestors = true)
|
22
|
+
__call :methods, include_ancestors
|
23
|
+
end
|
24
|
+
|
25
|
+
def alive?
|
26
|
+
@actor.alive?
|
27
|
+
end
|
28
|
+
|
29
|
+
def inspect
|
30
|
+
return __call :inspect if alive?
|
31
|
+
"#<Celluloid::Actor(#{@actor.class}:0x#{@actor.object_id.to_s(16)}) dead>"
|
32
|
+
end
|
33
|
+
|
34
|
+
# method_missing black magic to call bang predicate methods asynchronously
|
35
|
+
def method_missing(meth, *args, &block)
|
36
|
+
# bang methods are async calls
|
37
|
+
if meth.to_s.match(/!$/)
|
38
|
+
unbanged_meth = meth.to_s.sub(/!$/, '')
|
39
|
+
our_mailbox = Thread.current.mailbox
|
40
|
+
|
41
|
+
begin
|
42
|
+
@mailbox << AsyncCall.new(our_mailbox, unbanged_meth, args, block)
|
43
|
+
rescue MailboxError
|
44
|
+
# Silently swallow asynchronous calls to dead actors. There's no way
|
45
|
+
# to reliably generate DeadActorErrors for async calls, so users of
|
46
|
+
# async calls should find other ways to deal with actors dying
|
47
|
+
# during an async call (i.e. linking/supervisors)
|
48
|
+
end
|
49
|
+
|
50
|
+
return # casts are async and return immediately
|
51
|
+
end
|
52
|
+
|
53
|
+
__call(meth, *args, &block)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Make a synchronous call to the actor we're proxying to
|
57
|
+
def __call(meth, *args, &block)
|
58
|
+
our_mailbox = Thread.current.mailbox
|
59
|
+
call = SyncCall.new(our_mailbox, meth, args, block)
|
60
|
+
|
61
|
+
begin
|
62
|
+
@mailbox << call
|
63
|
+
rescue MailboxError
|
64
|
+
raise DeadActorError, "attempted to call a dead actor"
|
65
|
+
end
|
66
|
+
|
67
|
+
response = our_mailbox.receive do |msg|
|
68
|
+
msg.is_a? Response and msg.call == call
|
69
|
+
end
|
70
|
+
|
71
|
+
case response
|
72
|
+
when SuccessResponse
|
73
|
+
response.value
|
74
|
+
when ErrorResponse
|
75
|
+
raise response.value
|
76
|
+
else
|
77
|
+
raise "don't know how to handle #{response.class} messages!"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Celluloid
|
2
|
+
# Calls represent requests to an actor
|
3
|
+
class Call
|
4
|
+
attr_reader :caller, :method, :arguments, :block
|
5
|
+
|
6
|
+
def initialize(caller, method, arguments, block)
|
7
|
+
@caller, @method, @arguments, @block = caller, method, arguments, block
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# Synchronous calls wait for a response
|
12
|
+
class SyncCall < Call
|
13
|
+
def dispatch(obj)
|
14
|
+
unless obj.respond_to? @method
|
15
|
+
exception = NoMethodError.new("undefined method `#{@method}' for #{obj.inspect}")
|
16
|
+
@caller << ErrorResponse.new(self, exception)
|
17
|
+
return
|
18
|
+
end
|
19
|
+
|
20
|
+
begin
|
21
|
+
result = obj.send @method, *@arguments, &@block
|
22
|
+
rescue AbortError => exception
|
23
|
+
# Aborting indicates a protocol error on the part of the caller
|
24
|
+
# It should crash the caller, but the exception isn't reraised
|
25
|
+
@caller << ErrorResponse.new(self, exception.cause)
|
26
|
+
return
|
27
|
+
rescue Exception => exception
|
28
|
+
# Exceptions that occur during synchronous calls are reraised in the
|
29
|
+
# context of the caller
|
30
|
+
@caller << ErrorResponse.new(self, exception)
|
31
|
+
|
32
|
+
# They should also crash the actor where they occurred
|
33
|
+
raise exception
|
34
|
+
end
|
35
|
+
|
36
|
+
@caller << SuccessResponse.new(self, result)
|
37
|
+
true
|
38
|
+
end
|
39
|
+
|
40
|
+
def cleanup
|
41
|
+
exception = DeadActorError.new("attempted to call a dead actor")
|
42
|
+
@caller << ErrorResponse.new(self, exception)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Asynchronous calls don't wait for a response
|
47
|
+
class AsyncCall < Call
|
48
|
+
def dispatch(obj)
|
49
|
+
obj.send(@method, *@arguments, &@block) if obj.respond_to? @method
|
50
|
+
rescue AbortError
|
51
|
+
# Swallow aborted async calls, as they indicate the caller made a mistake
|
52
|
+
# FIXME: this should probably get logged
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Celluloid
|
2
|
+
# Exceptional system events which need to be processed out of band
|
3
|
+
class SystemEvent < Exception; end
|
4
|
+
|
5
|
+
# An actor has exited for the given reason
|
6
|
+
class ExitEvent < SystemEvent
|
7
|
+
attr_reader :actor, :reason
|
8
|
+
|
9
|
+
def initialize(actor, reason)
|
10
|
+
@actor, @reason = actor, reason
|
11
|
+
super reason.to_s
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Celluloid
|
2
|
+
def self.Future(*args, &block)
|
3
|
+
future = Celluloid::Future.spawn(*args, &block)
|
4
|
+
future.run!
|
5
|
+
future
|
6
|
+
end
|
7
|
+
|
8
|
+
class Future
|
9
|
+
include Celluloid::Actor
|
10
|
+
|
11
|
+
def initialize(*args, &block)
|
12
|
+
@args, @block = args, block
|
13
|
+
end
|
14
|
+
|
15
|
+
def run
|
16
|
+
@called = true
|
17
|
+
@value = @block[*@args]
|
18
|
+
rescue Exception => error
|
19
|
+
@error = error
|
20
|
+
end
|
21
|
+
|
22
|
+
def value
|
23
|
+
raise "not run yet" unless @called
|
24
|
+
abort @error if @error
|
25
|
+
@value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module Celluloid
|
5
|
+
# Thread safe storage of inter-actor links
|
6
|
+
class Links
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@links = Set.new
|
11
|
+
@lock = Mutex.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def <<(actor)
|
15
|
+
@lock.synchronize do
|
16
|
+
@links << actor
|
17
|
+
end
|
18
|
+
actor
|
19
|
+
end
|
20
|
+
|
21
|
+
def include?(actor)
|
22
|
+
@lock.synchronize do
|
23
|
+
@links.include? actor
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete(actor)
|
28
|
+
@lock.synchronize do
|
29
|
+
@links.delete actor
|
30
|
+
end
|
31
|
+
actor
|
32
|
+
end
|
33
|
+
|
34
|
+
def each(&block)
|
35
|
+
@lock.synchronize do
|
36
|
+
@links.each(&block)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def inspect
|
41
|
+
@lock.synchronize do
|
42
|
+
links = @links.to_a.map { |l| "#{l.class}:#{l.object_id}" }.join(',')
|
43
|
+
"#<Celluloid::Links[#{links}]>"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Support for linking actors together so they can crash or react to errors
|
49
|
+
module Linking
|
50
|
+
# Link this actor to another, allowing it to crash or react to errors
|
51
|
+
def link(actor)
|
52
|
+
actor.notify_link(@proxy)
|
53
|
+
self.notify_link(actor)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Remove links to another actor
|
57
|
+
def unlink(actor)
|
58
|
+
actor.notify_unlink(@proxy)
|
59
|
+
self.notify_unlink(actor)
|
60
|
+
end
|
61
|
+
|
62
|
+
def notify_link(actor)
|
63
|
+
@links << actor
|
64
|
+
end
|
65
|
+
|
66
|
+
def notify_unlink(actor)
|
67
|
+
@links.delete actor
|
68
|
+
end
|
69
|
+
|
70
|
+
# Is this actor linked to another?
|
71
|
+
def linked_to?(actor)
|
72
|
+
@links.include? actor
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|