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.
@@ -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.receive(&block)
41
+ actor.sleep(interval)
32
42
  else
33
- Thread.mailbox.receive(&block)
43
+ Kernel.sleep interval
34
44
  end
35
45
  end
36
46
 
37
- # Resume a fiber that participates in the Celluloid protocol
38
- def resume_fiber(fiber, value = nil)
39
- fiber.resume value
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 = Celluloid::Actor.new(allocate).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 = Celluloid::Actor.new(allocate).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
- Celluloid::Supervisor.supervise(self, *args, &block)
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
- Celluloid::Supervisor.supervise_as(name, self, *args, &block)
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 a Celluloid::Actor to run the block
187
- Celluloid::Future.new(&block).value
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'
@@ -36,8 +36,7 @@ module Celluloid
36
36
  end
37
37
 
38
38
  if Celluloid.actor?
39
- # Yield to the actor scheduler, which resumes us when we get a response
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 = Celluloid::Mailbox.new
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
- Celluloid::Fiber.new { handle_exit_event exit_event; nil }.resume
119
+ Task.new(:exit_handler) { handle_exit_event exit_event; nil }.resume
121
120
  retry
122
121
  end
123
122
 
124
- handle_message message
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
- # Register a fiber waiting for the response to a Celluloid::Call
139
- def register_fiber(call, fiber)
140
- raise ArgumentError, "attempted to register a dead fiber" unless fiber.alive?
141
- @pending_calls[call.id] = fiber
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
- Celluloid::Fiber.new { message.dispatch(@subject); nil }.resume
192
+ Task.new(:message_handler) { message.dispatch(@subject); nil }.resume
149
193
  when Response
150
- fiber = @pending_calls.delete(message.call_id)
194
+ handled_successfully = signal [:call, message.call_id], message
151
195
 
152
- if fiber
153
- fiber.resume message
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
- Celluloid::Logger.crash("#{@subject.class} crashed!", exception)
219
+ Logger.crash("#{@subject.class} crashed!", exception)
178
220
  cleanup ExitEvent.new(@proxy, exception)
179
221
  rescue Exception => ex
180
- Celluloid::Logger.crash("#{@subject.class}: ERROR HANDLER CRASHED!", ex)
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
- Celluloid::Logger.crash("#{@subject.class}#finalize crashed!", ex)
233
+ Logger.crash("#{@subject.class}#finalize crashed!", ex)
192
234
  end
193
235
  end
194
236
  end
@@ -42,7 +42,7 @@ module Celluloid
42
42
  func.call
43
43
  end
44
44
  rescue Exception => ex
45
- Celluloid::Logger.crash("#{self} internal failure", ex)
45
+ Logger.crash("#{self} internal failure", ex)
46
46
  end
47
47
  end
48
48
  thread[:queue] = queue
@@ -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
- Celluloid::Future.new { Actor.call @mailbox, method_name, *args, &block }
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
- Celluloid::Logger.error "!!! Celluloid::Application #{self} crashed. Restarting..."
24
+ Logger.error "!!! Celluloid::Application #{self} crashed. Restarting..."
25
25
  end
26
26
  end
27
27
 
@@ -80,14 +80,14 @@ module Celluloid
80
80
  begin
81
81
  check_signature(obj)
82
82
  rescue Exception => ex
83
- Celluloid::Logger.crash("#{obj.class}: async call failed!", ex)
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
- Celluloid::Logger.crash("#{obj.class}: async call aborted!", ex)
90
+ Logger.crash("#{obj.class}: async call aborted!", ex)
91
91
  end
92
92
  end
93
93
  end
@@ -1,4 +1,4 @@
1
- # Every time I look at this code a little part of me dies...
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