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