celluloid 0.6.2 → 0.7.0
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/README.md +37 -482
- data/lib/celluloid.rb +45 -18
- data/lib/celluloid/actor.rb +64 -22
- data/lib/celluloid/actor_pool.rb +1 -1
- data/lib/celluloid/actor_proxy.rb +1 -1
- data/lib/celluloid/application.rb +1 -1
- data/lib/celluloid/calls.rb +2 -2
- data/lib/celluloid/fiber.rb +2 -31
- data/lib/celluloid/fsm.rb +141 -0
- data/lib/celluloid/future.rb +10 -13
- data/lib/celluloid/logger.rb +8 -3
- data/lib/celluloid/mailbox.rb +16 -4
- data/lib/celluloid/receivers.rb +49 -19
- data/lib/celluloid/registry.rb +2 -2
- data/lib/celluloid/signals.rb +12 -10
- data/lib/celluloid/supervisor.rb +7 -4
- data/lib/celluloid/task.rb +53 -0
- data/lib/celluloid/tcp_server.rb +2 -1
- data/lib/celluloid/timers.rb +109 -0
- data/lib/celluloid/version.rb +1 -1
- data/spec/support/actor_examples.rb +453 -0
- data/spec/support/mailbox_examples.rb +52 -0
- metadata +11 -11
- data/lib/celluloid/io.rb +0 -24
- data/lib/celluloid/io/mailbox.rb +0 -65
- data/lib/celluloid/io/reactor.rb +0 -63
- data/lib/celluloid/io/waker.rb +0 -43
data/lib/celluloid.rb
CHANGED
@@ -20,23 +20,35 @@ module Celluloid
|
|
20
20
|
def current_actor
|
21
21
|
actor = Thread.current[:actor]
|
22
22
|
raise NotActorError, "not in actor scope" unless actor
|
23
|
-
|
24
23
|
actor.proxy
|
25
24
|
end
|
25
|
+
alias_method :current, :current_actor
|
26
26
|
|
27
27
|
# Receive an asynchronous message
|
28
|
-
def receive(&block)
|
28
|
+
def receive(timeout = nil, &block)
|
29
|
+
actor = Thread.current[:actor]
|
30
|
+
if actor
|
31
|
+
actor.receive(timeout, &block)
|
32
|
+
else
|
33
|
+
Thread.mailbox.receive(timeout, &block)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Sleep letting the actor continue processing messages
|
38
|
+
def sleep(interval)
|
29
39
|
actor = Thread.current[:actor]
|
30
40
|
if actor
|
31
|
-
actor.
|
41
|
+
actor.sleep(interval)
|
32
42
|
else
|
33
|
-
|
43
|
+
Kernel.sleep interval
|
34
44
|
end
|
35
45
|
end
|
36
46
|
|
37
|
-
#
|
38
|
-
def
|
39
|
-
|
47
|
+
# Obtain a hash of active tasks to their current activities
|
48
|
+
def tasks
|
49
|
+
actor = Thread.current[:actor]
|
50
|
+
raise NotActorError, "not in actor scope" unless actor
|
51
|
+
actor.tasks
|
40
52
|
end
|
41
53
|
end
|
42
54
|
|
@@ -44,7 +56,7 @@ module Celluloid
|
|
44
56
|
module ClassMethods
|
45
57
|
# Create a new actor
|
46
58
|
def new(*args, &block)
|
47
|
-
proxy =
|
59
|
+
proxy = Actor.new(allocate).proxy
|
48
60
|
proxy.send(:initialize, *args, &block)
|
49
61
|
proxy
|
50
62
|
end
|
@@ -55,7 +67,7 @@ module Celluloid
|
|
55
67
|
current_actor = Celluloid.current_actor
|
56
68
|
raise NotActorError, "can't link outside actor context" unless current_actor
|
57
69
|
|
58
|
-
proxy =
|
70
|
+
proxy = Actor.new(allocate).proxy
|
59
71
|
current_actor.link proxy
|
60
72
|
proxy.send(:initialize, *args, &block)
|
61
73
|
proxy
|
@@ -65,13 +77,13 @@ module Celluloid
|
|
65
77
|
# Create a supervisor which ensures an instance of an actor will restart
|
66
78
|
# an actor if it fails
|
67
79
|
def supervise(*args, &block)
|
68
|
-
|
80
|
+
Supervisor.supervise(self, *args, &block)
|
69
81
|
end
|
70
82
|
|
71
83
|
# Create a supervisor which ensures an instance of an actor will restart
|
72
84
|
# an actor if it fails, and keep the actor registered under a given name
|
73
85
|
def supervise_as(name, *args, &block)
|
74
|
-
|
86
|
+
Supervisor.supervise_as(name, self, *args, &block)
|
75
87
|
end
|
76
88
|
|
77
89
|
# Trap errors from actors we're linked to when they exit
|
@@ -136,6 +148,11 @@ module Celluloid
|
|
136
148
|
Celluloid.current_actor
|
137
149
|
end
|
138
150
|
|
151
|
+
# Obtain the running tasks for this actor
|
152
|
+
def tasks
|
153
|
+
Celluloid.tasks
|
154
|
+
end
|
155
|
+
|
139
156
|
# Obtain the Ruby object the actor is wrapping. This should ONLY be used
|
140
157
|
# for a limited set of use cases like runtime metaprogramming. Interacting
|
141
158
|
# directly with the wrapped object foregoes any kind of thread safety that
|
@@ -174,8 +191,18 @@ module Celluloid
|
|
174
191
|
end
|
175
192
|
|
176
193
|
# Receive an asynchronous message via the actor protocol
|
177
|
-
def receive(&block)
|
178
|
-
Celluloid.receive(&block)
|
194
|
+
def receive(timeout = nil, &block)
|
195
|
+
Celluloid.receive(timeout, &block)
|
196
|
+
end
|
197
|
+
|
198
|
+
# Sleep while letting the actor continue to receive messages
|
199
|
+
def sleep(interval)
|
200
|
+
Celluloid.sleep(interval)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Call a block after a given interval
|
204
|
+
def after(interval, &block)
|
205
|
+
Thread.current[:actor].after(interval, &block)
|
179
206
|
end
|
180
207
|
|
181
208
|
# Perform a blocking or computationally intensive action inside an
|
@@ -183,8 +210,8 @@ module Celluloid
|
|
183
210
|
# messages in its mailbox in the meantime
|
184
211
|
def async(&block)
|
185
212
|
# This implementation relies on the present implementation of
|
186
|
-
# Celluloid::Future, which uses
|
187
|
-
|
213
|
+
# Celluloid::Future, which uses an Actor to run the block
|
214
|
+
Future.new(&block).value
|
188
215
|
end
|
189
216
|
|
190
217
|
# Process async calls via method_missing
|
@@ -216,6 +243,7 @@ require 'celluloid/calls'
|
|
216
243
|
require 'celluloid/core_ext'
|
217
244
|
require 'celluloid/events'
|
218
245
|
require 'celluloid/fiber'
|
246
|
+
require 'celluloid/fsm'
|
219
247
|
require 'celluloid/links'
|
220
248
|
require 'celluloid/logger'
|
221
249
|
require 'celluloid/mailbox'
|
@@ -223,12 +251,11 @@ require 'celluloid/receivers'
|
|
223
251
|
require 'celluloid/registry'
|
224
252
|
require 'celluloid/responses'
|
225
253
|
require 'celluloid/signals'
|
254
|
+
require 'celluloid/task'
|
255
|
+
require 'celluloid/timers'
|
226
256
|
|
227
257
|
require 'celluloid/actor'
|
228
258
|
require 'celluloid/actor_pool'
|
229
259
|
require 'celluloid/supervisor'
|
230
260
|
require 'celluloid/future'
|
231
261
|
require 'celluloid/application'
|
232
|
-
|
233
|
-
require 'celluloid/io'
|
234
|
-
require 'celluloid/tcp_server'
|
data/lib/celluloid/actor.rb
CHANGED
@@ -36,8 +36,7 @@ module Celluloid
|
|
36
36
|
end
|
37
37
|
|
38
38
|
if Celluloid.actor?
|
39
|
-
|
40
|
-
response = Fiber.yield(call)
|
39
|
+
response = Thread.current[:actor].wait [:call, call.id]
|
41
40
|
else
|
42
41
|
# Otherwise we're inside a normal thread, so block
|
43
42
|
response = Thread.mailbox.receive do |msg|
|
@@ -67,15 +66,15 @@ module Celluloid
|
|
67
66
|
if subject.respond_to? :mailbox_factory
|
68
67
|
@mailbox = subject.mailbox_factory
|
69
68
|
else
|
70
|
-
@mailbox =
|
69
|
+
@mailbox = Mailbox.new
|
71
70
|
end
|
72
71
|
|
73
72
|
@links = Links.new
|
74
73
|
@signals = Signals.new
|
75
74
|
@receivers = Receivers.new
|
75
|
+
@timers = Timers.new
|
76
76
|
@proxy = ActorProxy.new(@mailbox, self.class.to_s)
|
77
77
|
@running = true
|
78
|
-
@pending_calls = {}
|
79
78
|
|
80
79
|
@thread = Pool.get do
|
81
80
|
Thread.current[:actor] = self
|
@@ -107,21 +106,27 @@ module Celluloid
|
|
107
106
|
end
|
108
107
|
|
109
108
|
# Receive an asynchronous message
|
110
|
-
def receive(&block)
|
111
|
-
@receivers.receive(&block)
|
109
|
+
def receive(timeout = nil, &block)
|
110
|
+
@receivers.receive(timeout, &block)
|
112
111
|
end
|
113
112
|
|
114
113
|
# Run the actor loop
|
115
114
|
def run
|
116
115
|
while @running
|
117
116
|
begin
|
118
|
-
message = @mailbox.receive
|
117
|
+
message = @mailbox.receive(timeout)
|
119
118
|
rescue ExitEvent => exit_event
|
120
|
-
|
119
|
+
Task.new(:exit_handler) { handle_exit_event exit_event; nil }.resume
|
121
120
|
retry
|
122
121
|
end
|
123
122
|
|
124
|
-
|
123
|
+
if message
|
124
|
+
handle_message message
|
125
|
+
else
|
126
|
+
# No message indicates a timeout
|
127
|
+
@timers.fire
|
128
|
+
@receivers.fire_timers
|
129
|
+
end
|
125
130
|
end
|
126
131
|
|
127
132
|
cleanup ExitEvent.new(@proxy)
|
@@ -135,24 +140,61 @@ module Celluloid
|
|
135
140
|
Pool.put @thread
|
136
141
|
end
|
137
142
|
|
138
|
-
#
|
139
|
-
def
|
140
|
-
|
141
|
-
|
143
|
+
# How long to wait until the next timer fires
|
144
|
+
def timeout
|
145
|
+
i1 = @timers.wait_interval
|
146
|
+
i2 = @receivers.wait_interval
|
147
|
+
|
148
|
+
if i1 and i2
|
149
|
+
i1 < i2 ? i1 : i2
|
150
|
+
elsif i1
|
151
|
+
i1
|
152
|
+
else
|
153
|
+
i2
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Obtain a hash of tasks that are currently waiting
|
158
|
+
def tasks
|
159
|
+
# A hash of tasks to what they're waiting on is more meaningful to the
|
160
|
+
# end-user, and lets us make a copy of the tasks table, rather than
|
161
|
+
# handing them the one we're using internally across threads, a definite
|
162
|
+
# thread safety shared state no-no
|
163
|
+
tasks = {}
|
164
|
+
current_task = Thread.current[:task]
|
165
|
+
tasks[current_task] = :running if current_task
|
166
|
+
|
167
|
+
@signals.waiting.each do |waitable, task|
|
168
|
+
tasks[task] = waitable
|
169
|
+
end
|
170
|
+
|
171
|
+
tasks
|
172
|
+
end
|
173
|
+
|
174
|
+
# Schedule a block to run at the given time
|
175
|
+
def after(interval)
|
176
|
+
@timers.add(interval) do
|
177
|
+
Task.new(:timer) { yield; nil }.resume
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Sleep for the given amount of time
|
182
|
+
def sleep(interval)
|
183
|
+
task = Task.current
|
184
|
+
@timers.add(interval) { task.resume }
|
185
|
+
Task.suspend
|
142
186
|
end
|
143
187
|
|
144
188
|
# Handle an incoming message
|
145
189
|
def handle_message(message)
|
146
190
|
case message
|
147
191
|
when Call
|
148
|
-
|
192
|
+
Task.new(:message_handler) { message.dispatch(@subject); nil }.resume
|
149
193
|
when Response
|
150
|
-
|
194
|
+
handled_successfully = signal [:call, message.call_id], message
|
151
195
|
|
152
|
-
|
153
|
-
|
154
|
-
else
|
155
|
-
Celluloid::Logger.debug("spurious response to call #{message.call_id}")
|
196
|
+
unless handled_successfully
|
197
|
+
Logger.debug("anomalous message! spurious response to call #{message.call_id}")
|
156
198
|
end
|
157
199
|
else
|
158
200
|
@receivers.handle_message(message)
|
@@ -174,10 +216,10 @@ module Celluloid
|
|
174
216
|
|
175
217
|
# Handle any exceptions that occur within a running actor
|
176
218
|
def handle_crash(exception)
|
177
|
-
|
219
|
+
Logger.crash("#{@subject.class} crashed!", exception)
|
178
220
|
cleanup ExitEvent.new(@proxy, exception)
|
179
221
|
rescue Exception => ex
|
180
|
-
|
222
|
+
Logger.crash("#{@subject.class}: ERROR HANDLER CRASHED!", ex)
|
181
223
|
end
|
182
224
|
|
183
225
|
# Handle cleaning up this actor after it exits
|
@@ -188,7 +230,7 @@ module Celluloid
|
|
188
230
|
begin
|
189
231
|
@subject.finalize if @subject.respond_to? :finalize
|
190
232
|
rescue Exception => ex
|
191
|
-
|
233
|
+
Logger.crash("#{@subject.class}#finalize crashed!", ex)
|
192
234
|
end
|
193
235
|
end
|
194
236
|
end
|
data/lib/celluloid/actor_pool.rb
CHANGED
@@ -41,7 +41,7 @@ module Celluloid
|
|
41
41
|
|
42
42
|
# Create a Celluloid::Future which calls a given method
|
43
43
|
def future(method_name, *args, &block)
|
44
|
-
|
44
|
+
Future.new { Actor.call @mailbox, method_name, *args, &block }
|
45
45
|
end
|
46
46
|
|
47
47
|
# Terminate the associated actor
|
@@ -21,7 +21,7 @@ module Celluloid
|
|
21
21
|
# Take five, toplevel supervisor
|
22
22
|
sleep 5 while supervisor.alive?
|
23
23
|
|
24
|
-
|
24
|
+
Logger.error "!!! Celluloid::Application #{self} crashed. Restarting..."
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
data/lib/celluloid/calls.rb
CHANGED
@@ -80,14 +80,14 @@ module Celluloid
|
|
80
80
|
begin
|
81
81
|
check_signature(obj)
|
82
82
|
rescue Exception => ex
|
83
|
-
|
83
|
+
Logger.crash("#{obj.class}: async call failed!", ex)
|
84
84
|
return
|
85
85
|
end
|
86
86
|
|
87
87
|
obj.send(@method, *@arguments, &@block)
|
88
88
|
rescue AbortError => ex
|
89
89
|
# Swallow aborted async calls, as they indicate the caller made a mistake
|
90
|
-
|
90
|
+
Logger.crash("#{obj.class}: async call aborted!", ex)
|
91
91
|
end
|
92
92
|
end
|
93
93
|
end
|
data/lib/celluloid/fiber.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# Fibers are hard... let's go shopping!
|
2
2
|
begin
|
3
3
|
require 'fiber'
|
4
4
|
rescue LoadError => ex
|
@@ -30,33 +30,4 @@ rescue LoadError => ex
|
|
30
30
|
else
|
31
31
|
raise ex
|
32
32
|
end
|
33
|
-
end
|
34
|
-
|
35
|
-
module Celluloid
|
36
|
-
class Fiber < ::Fiber
|
37
|
-
def initialize(*args)
|
38
|
-
actor = Thread.current[:actor]
|
39
|
-
mailbox = Thread.current[:mailbox]
|
40
|
-
|
41
|
-
super do
|
42
|
-
Thread.current[:actor] = actor
|
43
|
-
Thread.current[:mailbox] = mailbox
|
44
|
-
|
45
|
-
yield(*args)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
def resume(value = nil)
|
50
|
-
result = super
|
51
|
-
actor = Thread.current[:actor]
|
52
|
-
return result unless actor
|
53
|
-
|
54
|
-
if result.is_a? Celluloid::Call
|
55
|
-
actor.register_fiber result, self
|
56
|
-
elsif result
|
57
|
-
Celluloid::Logger.debug("non-call returned from fiber: #{result.class}")
|
58
|
-
end
|
59
|
-
nil
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
33
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module Celluloid
|
2
|
+
# Turn concurrent objects into finite state machines
|
3
|
+
# Inspired by Erlang's gen_fsm. See http://www.erlang.org/doc/man/gen_fsm.html
|
4
|
+
module FSM
|
5
|
+
DEFAULT_STATE = :default # Default state name unless one is explicitly set
|
6
|
+
|
7
|
+
# Included hook to extend class methods
|
8
|
+
def self.included(klass)
|
9
|
+
klass.send :include, Celluloid
|
10
|
+
klass.send :extend, ClassMethods
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
# Ensure FSMs transition into the default state after they're initialized
|
15
|
+
def new(*args, &block)
|
16
|
+
fsm = super
|
17
|
+
fsm.transition default_state
|
18
|
+
fsm
|
19
|
+
end
|
20
|
+
|
21
|
+
# Ensure FSMs transition into the default state after they're initialized
|
22
|
+
def new_link(*args, &block)
|
23
|
+
fsm = super
|
24
|
+
fsm.transition default_state
|
25
|
+
fsm
|
26
|
+
end
|
27
|
+
|
28
|
+
# Obtain or set the default state
|
29
|
+
# Passing a state name sets the default state
|
30
|
+
def default_state(new_default = nil)
|
31
|
+
if new_default
|
32
|
+
@default_state = new_default.to_sym
|
33
|
+
else
|
34
|
+
defined?(@default_state) ? @default_state : DEFAULT_STATE
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Obtain the valid states for this FSM
|
39
|
+
def states
|
40
|
+
@states ||= {}
|
41
|
+
end
|
42
|
+
|
43
|
+
# Declare an FSM state and optionally provide a callback block to fire
|
44
|
+
# Options:
|
45
|
+
# * to: a state or array of states this state can transition to
|
46
|
+
def state(*args, &block)
|
47
|
+
if args.last.is_a? Hash
|
48
|
+
options = args.pop.inject({}) { |h,(k,v)| h[k.to_s] = v; h }
|
49
|
+
else
|
50
|
+
options = {}
|
51
|
+
end
|
52
|
+
|
53
|
+
args.each do |name|
|
54
|
+
name = name.to_sym
|
55
|
+
states[name] = State.new(name, options['to'], &block)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Obtain the current state of the FSM
|
61
|
+
def current_state
|
62
|
+
defined?(@state) ? @state : @state = self.class.default_state
|
63
|
+
end
|
64
|
+
alias_method :state, :current_state
|
65
|
+
|
66
|
+
# Transition to another state
|
67
|
+
# Options:
|
68
|
+
# * delay: don't transition immediately, wait the given number of seconds.
|
69
|
+
# This will return a Celluloid::Timer object you can use to
|
70
|
+
# cancel the pending state transition.
|
71
|
+
#
|
72
|
+
# Note: making additional state transitions will cancel delayed transitions
|
73
|
+
def transition(state_name, options = {})
|
74
|
+
state_name = state_name.to_sym
|
75
|
+
current_state = self.class.states[@state]
|
76
|
+
|
77
|
+
return if current_state && current_state.name == state_name
|
78
|
+
|
79
|
+
if current_state and not current_state.valid_transition? state_name
|
80
|
+
valid = current_state.transitions.map(&:to_s).join(", ")
|
81
|
+
raise ArgumentError, "#{self.class} can't change state from '#{@state}' to '#{state_name}', only to: #{valid}"
|
82
|
+
end
|
83
|
+
|
84
|
+
new_state = self.class.states[state_name]
|
85
|
+
|
86
|
+
if !new_state and state_name == self.class.default_state
|
87
|
+
# FIXME This probably isn't thread safe... or wise
|
88
|
+
new_state = self.class.states[state_name] = State.new(state_name)
|
89
|
+
end
|
90
|
+
|
91
|
+
if new_state
|
92
|
+
if options[:delay]
|
93
|
+
@delayed_transition.cancel if @delayed_transition
|
94
|
+
|
95
|
+
@delayed_transition = after(options[:delay]) do
|
96
|
+
transition! new_state.name
|
97
|
+
new_state.call(self)
|
98
|
+
end
|
99
|
+
|
100
|
+
return @delayed_transition
|
101
|
+
end
|
102
|
+
|
103
|
+
if defined?(@delayed_transition) and @delayed_transition
|
104
|
+
@delayed_transition.cancel
|
105
|
+
@delayed_transition = nil
|
106
|
+
end
|
107
|
+
|
108
|
+
transition! new_state.name
|
109
|
+
new_state.call(self)
|
110
|
+
else
|
111
|
+
raise ArgumentError, "invalid state for #{self.class}: #{state_name}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Immediate state transition with no sanity checks. "Dangerous!"
|
116
|
+
def transition!(state_name)
|
117
|
+
@state = state_name
|
118
|
+
end
|
119
|
+
|
120
|
+
# FSM states as declared by Celluloid::FSM.state
|
121
|
+
class State
|
122
|
+
attr_reader :name, :transitions
|
123
|
+
|
124
|
+
def initialize(name, transitions = nil, &block)
|
125
|
+
@name, @block = name, block
|
126
|
+
@transitions = Array(transitions).map { |t| t.to_sym } if transitions
|
127
|
+
end
|
128
|
+
|
129
|
+
def call(obj)
|
130
|
+
obj.instance_eval(&@block) if @block
|
131
|
+
end
|
132
|
+
|
133
|
+
def valid_transition?(new_state)
|
134
|
+
# All transitions are allowed unless expressly
|
135
|
+
return true unless @transitions
|
136
|
+
|
137
|
+
@transitions.include? new_state.to_sym
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|