celluloid 0.11.1 → 0.12.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 CHANGED
@@ -1,4 +1,4 @@
1
- ![Celluloid](https://github.com/celluloid/celluloid/raw/master/logo.png)
1
+ ![Celluloid](https://raw.github.com/celluloid/celluloid-logos/master/celluloid/celluloid.png)
2
2
  =========
3
3
  [![Build Status](https://secure.travis-ci.org/celluloid/celluloid.png?branch=master)](http://travis-ci.org/celluloid/celluloid)
4
4
  [![Dependency Status](https://gemnasium.com/celluloid/celluloid.png)](https://gemnasium.com/celluloid/celluloid)
@@ -17,7 +17,7 @@ Much of the difficulty with building concurrent programs in Ruby arises because
17
17
  the object-oriented mechanisms for structuring code, such as classes and
18
18
  inheritance, are separate from the concurrency mechanisms, such as threads and
19
19
  locks. Celluloid combines these into a single structure, an active object
20
- running within a thread, called an "actor".
20
+ running within a thread, called an "actor", or in Celluloid vernacular, a "cell".
21
21
 
22
22
  By combining concurrency with object oriented programming, Celluloid frees you
23
23
  up from worry about where to use threads and locks. Celluloid combines them
@@ -72,15 +72,32 @@ for more detailed documentation and usage notes.
72
72
  Like Celluloid? [Join the Google Group](http://groups.google.com/group/celluloid-ruby)
73
73
  or visit us on IRC at #celluloid on freenode
74
74
 
75
- ### Is it any good?
76
-
77
- [Yes.](http://news.ycombinator.com/item?id=3067434)
78
-
79
75
  ### Is It "Production Ready™"?
80
76
 
81
77
  Yes, many users are now running Celluloid in production by using
82
78
  [Sidekiq](https://github.com/mperham/sidekiq)
83
79
 
80
+ Installation
81
+ ------------
82
+
83
+ Add this line to your application's Gemfile:
84
+
85
+ gem 'celluloid'
86
+
87
+ And then execute:
88
+
89
+ $ bundle
90
+
91
+ Or install it yourself as:
92
+
93
+ $ gem install celluloid
94
+
95
+ Inside of your Ruby program do:
96
+
97
+ require 'celluloid'
98
+
99
+ ...to pull it in as a dependency.
100
+
84
101
  Supported Platforms
85
102
  -------------------
86
103
 
@@ -7,6 +7,9 @@ module Celluloid
7
7
  # Trying to do something to a dead actor
8
8
  class DeadActorError < StandardError; end
9
9
 
10
+ # A timeout occured before the given request could complete
11
+ class TimeoutError < StandardError; end
12
+
10
13
  # The caller made an error, not the current actor
11
14
  class AbortError < StandardError
12
15
  attr_reader :cause
@@ -17,6 +20,8 @@ module Celluloid
17
20
  end
18
21
  end
19
22
 
23
+ LINKING_TIMEOUT = 5 # linking times out after 5 seconds
24
+
20
25
  # Actors are Celluloid's concurrency primitive. They're implemented as
21
26
  # normal Ruby objects wrapped in threads which communicate with asynchronous
22
27
  # messages.
@@ -64,9 +69,13 @@ module Celluloid
64
69
  # The current task will be automatically resumed when we get a response
65
70
  Task.suspend(:callwait).value
66
71
  else
67
- # Otherwise we're inside a normal thread, so block
68
- response = Thread.mailbox.receive do |msg|
69
- msg.respond_to?(:call) and msg.call == call
72
+ # Otherwise we're inside a normal thread or exclusive, so block
73
+ response = loop do
74
+ message = Thread.mailbox.receive do |msg|
75
+ msg.respond_to?(:call) and msg.call == call
76
+ end
77
+ break message unless message.is_a?(SystemEvent)
78
+ Thread.current[:actor].handle_system_event(message)
70
79
  end
71
80
 
72
81
  response.value
@@ -97,18 +106,68 @@ module Celluloid
97
106
  actors = []
98
107
  Thread.list.each do |t|
99
108
  actor = t[:actor]
100
- actors << actor.proxy if actor
109
+ actors << actor.proxy if actor and actor.respond_to?(:proxy)
101
110
  end
102
111
  actors
103
112
  end
113
+
114
+ # Watch for exit events from another actor
115
+ def monitor(actor)
116
+ raise NotActorError, "can't link outside actor context" unless Celluloid.actor?
117
+ Thread.current[:actor].linking_request(actor, :link)
118
+ end
119
+
120
+ # Stop waiting for exit events from another actor
121
+ def unmonitor(actor)
122
+ raise NotActorError, "can't link outside actor context" unless Celluloid.actor?
123
+ Thread.current[:actor].linking_request(actor, :unlink)
124
+ end
125
+
126
+ # Link to another actor
127
+ def link(actor)
128
+ monitor actor
129
+ Thread.current[:actor].links << actor
130
+ end
131
+
132
+ # Unlink from another actor
133
+ def unlink(actor)
134
+ unmonitor actor
135
+ Thread.current[:actor].links.delete actor
136
+ end
137
+
138
+ # Are we monitoring the given actor?
139
+ def monitoring?(actor)
140
+ actor.links.include? Actor.current
141
+ end
142
+
143
+ # Are we bidirectionally linked to the given actor?
144
+ def linked_to?(actor)
145
+ monitoring?(actor) && Thread.current[:actor].links.include?(actor)
146
+ end
147
+
148
+ # Forcibly kill a given actor
149
+ def kill(actor)
150
+ actor.thread.kill
151
+ begin
152
+ actor.mailbox.shutdown
153
+ rescue DeadActorError
154
+ end
155
+ end
156
+
157
+ # Wait for an actor to terminate
158
+ def join(actor)
159
+ actor.thread.join
160
+ actor
161
+ end
104
162
  end
105
163
 
106
164
  # Wrap the given subject with an Actor
107
- def initialize(subject)
165
+ def initialize(subject, options = {})
108
166
  @subject = subject
109
- @mailbox = subject.class.mailbox_factory
110
- @exit_handler = subject.class.exit_handler
111
- @exclusives = subject.class.exclusive_methods
167
+ @mailbox = options[:mailbox] || Mailbox.new
168
+ @exit_handler = options[:exit_handler]
169
+ @exclusives = options[:exclusive_methods]
170
+ @task_class = options[:task_class] || Celluloid.task_class
112
171
 
113
172
  @tasks = Set.new
114
173
  @links = Links.new
@@ -128,6 +187,33 @@ module Celluloid
128
187
  @proxy = ActorProxy.new(self)
129
188
  end
130
189
 
190
+ # Run the actor loop
191
+ def run
192
+ begin
193
+ while @running
194
+ if message = @mailbox.receive(timeout_interval)
195
+ handle_message message
196
+ else
197
+ # No message indicates a timeout
198
+ @timers.fire
199
+ @receivers.fire_timers
200
+ end
201
+ end
202
+ rescue MailboxShutdown
203
+ # If the mailbox detects shutdown, exit the actor
204
+ end
205
+
206
+ shutdown
207
+ rescue Exception => ex
208
+ handle_crash(ex)
209
+ raise unless ex.is_a? StandardError
210
+ end
211
+
212
+ # Terminate this actor
213
+ def terminate
214
+ @running = false
215
+ end
216
+
131
217
  # Is this actor running in exclusive mode?
132
218
  def exclusive?
133
219
  @exclusive
@@ -141,9 +227,33 @@ module Celluloid
141
227
  @exclusive = false
142
228
  end
143
229
 
144
- # Terminate this actor
145
- def terminate
146
- @running = false
230
+ # Perform a linking request with another actor
231
+ def linking_request(receiver, type)
232
+ exclusive do
233
+ start_time = Time.now
234
+
235
+ receiver.mailbox << LinkingRequest.new(Actor.current, type)
236
+ system_events = []
237
+ loop do
238
+ wait_interval = start_time + LINKING_TIMEOUT - Time.now
239
+ message = @mailbox.receive(wait_interval) do |msg|
240
+ msg.is_a?(LinkingResponse) && msg.actor == receiver && msg.type == type
241
+ end
242
+
243
+ case message
244
+ when LinkingResponse
245
+ # We're done!
246
+ system_events.each { |ev| handle_system_event(ev) }
247
+ return
248
+ when NilClass
249
+ raise TimeoutError, "linking timeout of #{LINKING_TIMEOUT} seconds exceeded"
250
+ when SystemEvent
251
+ # Queue up pending system events to be processed after we've successfully linked
252
+ system_events << message
253
+ else raise 'wtf'
254
+ end
255
+ end
256
+ end
147
257
  end
148
258
 
149
259
  # Send a signal with the given name to all waiting methods
@@ -158,41 +268,16 @@ module Celluloid
158
268
 
159
269
  # Receive an asynchronous message
160
270
  def receive(timeout = nil, &block)
161
- begin
162
- @receivers.receive(timeout, &block)
163
- rescue SystemEvent => event
164
- handle_system_event(event)
165
- retry
166
- end
167
- end
271
+ loop do
272
+ message = @receivers.receive(timeout, &block)
273
+ break message unless message.is_a?(SystemEvent)
168
274
 
169
- # Run the actor loop
170
- def run
171
- begin
172
- while @running
173
- if message = @mailbox.receive(timeout)
174
- handle_message message
175
- else
176
- # No message indicates a timeout
177
- @timers.fire
178
- @receivers.fire_timers
179
- end
180
- end
181
- rescue SystemEvent => event
182
- handle_system_event event
183
- retry
184
- rescue MailboxShutdown
185
- # If the mailbox detects shutdown, exit the actor
275
+ handle_system_event(message)
186
276
  end
187
-
188
- shutdown
189
- rescue Exception => ex
190
- handle_crash(ex)
191
- raise unless ex.is_a? StandardError
192
277
  end
193
278
 
194
279
  # How long to wait until the next timer fires
195
- def timeout
280
+ def timeout_interval
196
281
  i1 = @timers.wait_interval
197
282
  i2 = @receivers.wait_interval
198
283
 
@@ -206,17 +291,13 @@ module Celluloid
206
291
  end
207
292
 
208
293
  # Schedule a block to run at the given time
209
- def after(interval)
210
- @timers.after(interval) do
211
- Task.new(:timer) { yield }.resume
212
- end
294
+ def after(interval, &block)
295
+ @timers.after(interval) { task(:timer, &block) }
213
296
  end
214
297
 
215
298
  # Schedule a block to run at the given time
216
- def every(interval)
217
- @timers.every(interval) do
218
- Task.new(:timer) { yield }.resume
219
- end
299
+ def every(interval, &block)
300
+ @timers.every(interval) { task(:timer, &block) }
220
301
  end
221
302
 
222
303
  # Sleep for the given amount of time
@@ -233,12 +314,10 @@ module Celluloid
233
314
  # Handle standard low-priority messages
234
315
  def handle_message(message)
235
316
  case message
317
+ when SystemEvent
318
+ handle_system_event message
236
319
  when Call
237
- if @exclusives && @exclusives.include?(message.method)
238
- exclusive { message.dispatch(@subject) }
239
- else
240
- Task.new(:message_handler) { message.dispatch(@subject) }.resume
241
- end
320
+ task(:message_handler, message.method) { message.dispatch(@subject) }
242
321
  when Response
243
322
  message.dispatch
244
323
  else
@@ -251,7 +330,9 @@ module Celluloid
251
330
  def handle_system_event(event)
252
331
  case event
253
332
  when ExitEvent
254
- Task.new(:exit_handler) { handle_exit_event event }.resume
333
+ task(:exit_handler, @exit_handler) { handle_exit_event event }
334
+ when LinkingRequest
335
+ event.process(links)
255
336
  when NamingRequest
256
337
  @name = event.name
257
338
  when TerminationRequest
@@ -289,7 +370,7 @@ module Celluloid
289
370
  # Run the user-defined finalizer, if one is set
290
371
  def run_finalizer
291
372
  return unless @subject.respond_to? :finalize
292
- Task.new(:finalizer) { @subject.finalize }.resume
373
+ task(:finalizer, :finalize) { @subject.finalize }
293
374
  rescue => ex
294
375
  Logger.crash("#{@subject.class}#finalize crashed!", ex)
295
376
  end
@@ -302,5 +383,14 @@ module Celluloid
302
383
  rescue => ex
303
384
  Logger.crash("#{@subject.class}: CLEANUP CRASHED!", ex)
304
385
  end
386
+
387
+ # Run a method inside a task unless it's exclusive
388
+ def task(task_type, method_name = nil, &block)
389
+ if @exclusives && (@exclusives == :all || @exclusives.include?(method_name))
390
+ exclusive { block.call }
391
+ else
392
+ @task_class.new(:message_handler, &block).resume
393
+ end
394
+ end
305
395
  end
306
396
  end
@@ -0,0 +1,9 @@
1
+ # Things to run after Celluloid is fully loaded
2
+
3
+ # Configure default systemwide settings
4
+ Celluloid.logger = Logger.new STDERR
5
+ Celluloid.task_class = Celluloid::TaskFiber
6
+
7
+ # Launch the notifications fanout actor
8
+ # FIXME: We should set up the supervision hierarchy here
9
+ Celluloid::Notifications::Fanout.supervise_as :notifications_fanout
@@ -9,7 +9,7 @@ module Celluloid
9
9
 
10
10
  def check_signature(obj)
11
11
  unless obj.respond_to? @method
12
- raise NoMethodError, "undefined method `#{@method}' for #{obj.inspect}"
12
+ raise NoMethodError, "undefined method `#{@method}' for #{obj.to_s}"
13
13
  end
14
14
 
15
15
  begin
@@ -39,7 +39,7 @@ module Celluloid
39
39
  class SyncCall < Call
40
40
  attr_reader :caller, :task
41
41
 
42
- def initialize(caller, method, arguments = [], block = nil, task = Fiber.current.task)
42
+ def initialize(caller, method, arguments = [], block = nil, task = Thread.current[:task])
43
43
  super(method, arguments, block)
44
44
  @caller = caller
45
45
  @task = task
@@ -99,7 +99,7 @@ module Celluloid
99
99
  obj.send(@method, *@arguments, &@block)
100
100
  rescue AbortError => ex
101
101
  # Swallow aborted async calls, as they indicate the caller made a mistake
102
- Logger.crash("#{obj.class}: async call `#{@method}' aborted!", ex)
102
+ Logger.crash("#{obj.class}: async call `#{@method}' aborted!", ex.cause)
103
103
  end
104
104
  end
105
105
  end
@@ -17,9 +17,4 @@ class Thread
17
17
  mailbox.receive(timeout, &block)
18
18
  end
19
19
  end
20
- end
21
-
22
- class Fiber
23
- # Celluloid::Task associated with this Fiber
24
- attr_accessor :task
25
- end
20
+ end
@@ -7,6 +7,8 @@ module Celluloid
7
7
  @cores = Integer(`sysctl hw.ncpu`[/\d+/])
8
8
  when 'linux'
9
9
  @cores = File.read("/proc/cpuinfo").scan(/core id\s+: \d+/).uniq.size
10
+ when 'mingw', 'mswin'
11
+ @cores = Integer(`SET NUMBER_OF_PROCESSORS`[/\d+/])
10
12
  else
11
13
  @cores = nil
12
14
  end
@@ -35,6 +35,11 @@ module Celluloid
35
35
  receiver << @call
36
36
  end
37
37
 
38
+ # Check if this future has a value yet
39
+ def ready?
40
+ @ready
41
+ end
42
+
38
43
  # Obtain the value for this Future
39
44
  def value(timeout = nil)
40
45
  ready = result = nil
@@ -1,5 +1,3 @@
1
- require 'thread'
2
-
3
1
  module Celluloid
4
2
  # Linked actors send each other system events
5
3
  class Links
@@ -31,7 +29,7 @@ module Celluloid
31
29
 
32
30
  # Send an event message to all actors
33
31
  def send_event(event)
34
- each { |actor| actor.mailbox.system_event event }
32
+ each { |actor| actor.mailbox << event }
35
33
  end
36
34
 
37
35
  # Generate a string representation
@@ -13,9 +13,9 @@ module Celluloid
13
13
  alias_method :address, :object_id
14
14
 
15
15
  def initialize
16
- @messages = []
17
- @mutex = Mutex.new
18
- @dead = false
16
+ @messages = []
17
+ @mutex = Mutex.new
18
+ @dead = false
19
19
  @condition = ConditionVariable.new
20
20
  end
21
21
 
@@ -23,9 +23,18 @@ module Celluloid
23
23
  def <<(message)
24
24
  @mutex.lock
25
25
  begin
26
- raise MailboxError, "dead recipient" if @dead
26
+ if message.is_a?(SystemEvent)
27
+ # Silently swallow system events sent to dead actors
28
+ return if @dead
29
+
30
+ # SystemEvents are high priority messages so they get added to the
31
+ # head of our message queue instead of the end
32
+ @messages.unshift message
33
+ else
34
+ raise MailboxError, "dead recipient" if @dead
35
+ @messages << message
36
+ end
27
37
 
28
- @messages << message
29
38
  @condition.signal
30
39
  nil
31
40
  ensure
@@ -33,20 +42,6 @@ module Celluloid
33
42
  end
34
43
  end
35
44
 
36
- # Add a high-priority system event to the Mailbox
37
- def system_event(event)
38
- @mutex.lock
39
- begin
40
- unless @dead # Silently fail if messages are sent to dead actors
41
- @messages.unshift event
42
- @condition.signal
43
- end
44
- nil
45
- ensure
46
- @mutex.unlock rescue nil
47
- end
48
- end
49
-
50
45
  # Receive a message from the Mailbox
51
46
  def receive(timeout = nil, &block)
52
47
  message = nil
@@ -92,7 +87,6 @@ module Celluloid
92
87
  message = @messages.shift
93
88
  end
94
89
 
95
- raise message if message.is_a? SystemEvent
96
90
  message
97
91
  end
98
92
 
@@ -0,0 +1,19 @@
1
+ module Celluloid
2
+ # Method handles that route through an actor proxy
3
+ class Method
4
+ def initialize(actor, name)
5
+ raise NameError, "undefined method `#{name}'" unless actor.respond_to? name
6
+
7
+ @actor, @name = actor, name
8
+ @klass = @actor.class
9
+ end
10
+
11
+ def call(*args, &block)
12
+ @actor._send_(@name, *args, &block)
13
+ end
14
+
15
+ def inspect
16
+ "#<Celluloid::Method #{@klass}#{@name}>"
17
+ end
18
+ end
19
+ end
@@ -1,5 +1,21 @@
1
1
  module Celluloid
2
2
  module Notifications
3
+ def self.notifier
4
+ Actor[:notifications_fanout] or raise DeadActorError, "notifications fanout actor not running"
5
+ end
6
+
7
+ def publish(pattern, *args)
8
+ Celluloid::Notifications.notifier.publish(pattern, *args)
9
+ end
10
+
11
+ def subscribe(pattern, method)
12
+ Celluloid::Notifications.notifier.subscribe(Actor.current, pattern, method)
13
+ end
14
+
15
+ def unsubscribe(*args)
16
+ Celluloid::Notifications.notifier.unsubscribe(*args)
17
+ end
18
+
3
19
  class Fanout
4
20
  include Celluloid
5
21
  trap_exit :prune
@@ -63,22 +79,5 @@ module Celluloid
63
79
  @pattern && @pattern === subscriber_or_pattern
64
80
  end
65
81
  end
66
-
67
- class << self
68
- attr_accessor :notifier
69
- end
70
- self.notifier = Fanout.new
71
-
72
- def publish(pattern, *args)
73
- Celluloid::Notifications.notifier.publish(pattern, *args)
74
- end
75
-
76
- def subscribe(pattern, method)
77
- Celluloid::Notifications.notifier.subscribe(Actor.current, pattern, method)
78
- end
79
-
80
- def unsubscribe(*args)
81
- Celluloid::Notifications.notifier.unsubscribe(*args)
82
- end
83
82
  end
84
83
  end
@@ -1,3 +1,5 @@
1
+ require 'set'
2
+
1
3
  module Celluloid
2
4
  # Manages a fixed-size pool of workers
3
5
  # Delegates work (i.e. methods) and supervises workers
@@ -7,14 +9,28 @@ module Celluloid
7
9
  trap_exit :crash_handler
8
10
 
9
11
  def initialize(worker_class, options = {})
10
- @size = options[:size]
11
- raise ArgumentError, "minimum pool size is 2" if @size && @size < 2
12
+ @size = options[:size] || [Celluloid.cores, 2].max
13
+ raise ArgumentError, "minimum pool size is 2" if @size < 2
12
14
 
13
- @size ||= [Celluloid.cores, 2].max
15
+ @worker_class = worker_class
14
16
  @args = options[:args] ? Array(options[:args]) : []
15
17
 
16
- @worker_class = worker_class
17
18
  @idle = @size.times.map { worker_class.new_link(*@args) }
19
+
20
+ # FIXME: Another data structure (e.g. Set) would be more appropriate
21
+ # here except it causes MRI to crash :o
22
+ @busy = []
23
+ end
24
+
25
+ def finalize
26
+ terminators = (@idle + @busy).each do |actor|
27
+ begin
28
+ actor.future(:terminate)
29
+ rescue DeadActorError, MailboxError
30
+ end
31
+ end
32
+
33
+ terminators.compact.each { |terminator| terminator.value rescue nil }
18
34
  end
19
35
 
20
36
  def _send_(method, *args, &block)
@@ -22,10 +38,17 @@ module Celluloid
22
38
 
23
39
  begin
24
40
  worker._send_ method, *args, &block
41
+ rescue DeadActorError # if we get a dead actor out of the pool
42
+ wait :respawn_complete
43
+ worker = __provision_worker
44
+ retry
25
45
  rescue Exception => ex
26
46
  abort ex
27
47
  ensure
28
- @idle << worker if worker.alive?
48
+ if worker.alive?
49
+ @idle << worker
50
+ @busy.delete worker
51
+ end
29
52
  end
30
53
  end
31
54
 
@@ -56,23 +79,29 @@ module Celluloid
56
79
  # Provision a new worker
57
80
  def __provision_worker
58
81
  while @idle.empty?
59
- # Using exclusive mode blocks incoming messages, so they don't pile
60
- # up as waiting Celluloid::Tasks
61
- response = exclusive { receive { |msg| msg.is_a? Response } }
82
+ # Wait for responses from one of the busy workers
83
+ response = exclusive { receive { |msg| msg.is_a?(Response) } }
62
84
  Thread.current[:actor].handle_message(response)
63
85
  end
64
- @idle.shift
86
+
87
+ worker = @idle.shift
88
+ @busy << worker
89
+
90
+ worker
65
91
  end
66
92
 
67
93
  # Spawn a new worker for every crashed one
68
94
  def crash_handler(actor, reason)
95
+ @busy.delete actor
69
96
  @idle.delete actor
70
- return unless reason # don't restart workers that exit cleanly
97
+ return unless reason
98
+
71
99
  @idle << @worker_class.new_link(*@args)
100
+ signal :respawn_complete
72
101
  end
73
102
 
74
103
  def respond_to?(method)
75
- super || (@worker_class ? @worker_class.instance_methods.include?(method.to_sym) : false)
104
+ super || @worker_class.instance_methods.include?(method.to_sym)
76
105
  end
77
106
 
78
107
  def method_missing(method, *args, &block)
@@ -0,0 +1,17 @@
1
+ module Celluloid
2
+ # Base class of all Celluloid proxies
3
+ class AbstractProxy < BasicObject
4
+ # Needed for storing proxies in data structures
5
+ needed = [:object_id, :__id__, :hash] - instance_methods
6
+ if needed.any?
7
+ include ::Kernel.dup.module_eval {
8
+ undef_method *(instance_methods - needed)
9
+ self
10
+ }
11
+
12
+ # rubinius bug? These methods disappear when we include hacked kernel
13
+ define_method :==, ::BasicObject.instance_method(:==) unless instance_methods.include?(:==)
14
+ alias_method(:equal?, :==) unless instance_methods.include?(:equal?)
15
+ end
16
+ end
17
+ end