celluloid 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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