celluloid 0.12.0 → 0.13.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.
Files changed (40) hide show
  1. data/README.md +36 -23
  2. data/lib/celluloid/actor.rb +74 -47
  3. data/lib/celluloid/autostart.rb +16 -0
  4. data/lib/celluloid/calls.rb +52 -52
  5. data/lib/celluloid/condition.rb +70 -0
  6. data/lib/celluloid/core_ext.rb +1 -1
  7. data/lib/celluloid/cpu_counter.rb +7 -1
  8. data/lib/celluloid/fsm.rb +49 -16
  9. data/lib/celluloid/future.rb +5 -2
  10. data/lib/celluloid/internal_pool.rb +62 -44
  11. data/lib/celluloid/legacy.rb +46 -0
  12. data/lib/celluloid/links.rb +0 -5
  13. data/lib/celluloid/logger.rb +14 -4
  14. data/lib/celluloid/logging/incident.rb +21 -0
  15. data/lib/celluloid/logging/incident_logger.rb +129 -0
  16. data/lib/celluloid/logging/incident_reporter.rb +48 -0
  17. data/lib/celluloid/logging/log_event.rb +20 -0
  18. data/lib/celluloid/logging/ring_buffer.rb +65 -0
  19. data/lib/celluloid/logging.rb +5 -0
  20. data/lib/celluloid/mailbox.rb +2 -1
  21. data/lib/celluloid/method.rb +5 -0
  22. data/lib/celluloid/pool_manager.rb +21 -8
  23. data/lib/celluloid/proxies/actor_proxy.rb +11 -11
  24. data/lib/celluloid/proxies/async_proxy.rb +5 -1
  25. data/lib/celluloid/responses.rb +8 -6
  26. data/lib/celluloid/stack_dump.rb +94 -0
  27. data/lib/celluloid/supervision_group.rb +5 -3
  28. data/lib/celluloid/system_events.rb +11 -0
  29. data/lib/celluloid/tasks/task_fiber.rb +17 -9
  30. data/lib/celluloid/tasks/task_thread.rb +24 -14
  31. data/lib/celluloid/tasks.rb +59 -0
  32. data/lib/celluloid/thread_handle.rb +10 -1
  33. data/lib/celluloid/version.rb +1 -1
  34. data/lib/celluloid.rb +146 -87
  35. data/spec/support/actor_examples.rb +242 -114
  36. data/spec/support/example_actor_class.rb +17 -5
  37. data/spec/support/task_examples.rb +11 -2
  38. metadata +58 -14
  39. data/lib/celluloid/boot.rb +0 -9
  40. data/lib/celluloid/task.rb +0 -22
@@ -6,7 +6,8 @@ module Celluloid
6
6
  # Don't use this class directly. Instead use MyKlass.pool
7
7
  class PoolManager
8
8
  include Celluloid
9
- trap_exit :crash_handler
9
+ trap_exit :__crash_handler__
10
+ finalizer :__shutdown__
10
11
 
11
12
  def initialize(worker_class, options = {})
12
13
  @size = options[:size] || [Celluloid.cores, 2].max
@@ -22,7 +23,7 @@ module Celluloid
22
23
  @busy = []
23
24
  end
24
25
 
25
- def finalize
26
+ def __shutdown__
26
27
  terminators = (@idle + @busy).each do |actor|
27
28
  begin
28
29
  actor.future(:terminate)
@@ -34,13 +35,13 @@ module Celluloid
34
35
  end
35
36
 
36
37
  def _send_(method, *args, &block)
37
- worker = __provision_worker
38
+ worker = __provision_worker__
38
39
 
39
40
  begin
40
41
  worker._send_ method, *args, &block
41
42
  rescue DeadActorError # if we get a dead actor out of the pool
42
43
  wait :respawn_complete
43
- worker = __provision_worker
44
+ worker = __provision_worker__
44
45
  retry
45
46
  rescue Exception => ex
46
47
  abort ex
@@ -76,12 +77,24 @@ module Celluloid
76
77
  _send_ :inspect
77
78
  end
78
79
 
80
+ def size
81
+ @size
82
+ end
83
+
84
+ def busy_size
85
+ @busy.length
86
+ end
87
+
88
+ def idle_size
89
+ @idle.length
90
+ end
91
+
79
92
  # Provision a new worker
80
- def __provision_worker
93
+ def __provision_worker__
81
94
  while @idle.empty?
82
95
  # Wait for responses from one of the busy workers
83
96
  response = exclusive { receive { |msg| msg.is_a?(Response) } }
84
- Thread.current[:actor].handle_message(response)
97
+ Thread.current[:celluloid_actor].handle_message(response)
85
98
  end
86
99
 
87
100
  worker = @idle.shift
@@ -91,7 +104,7 @@ module Celluloid
91
104
  end
92
105
 
93
106
  # Spawn a new worker for every crashed one
94
- def crash_handler(actor, reason)
107
+ def __crash_handler__(actor, reason)
95
108
  @busy.delete actor
96
109
  @idle.delete actor
97
110
  return unless reason
@@ -100,7 +113,7 @@ module Celluloid
100
113
  signal :respawn_complete
101
114
  end
102
115
 
103
- def respond_to?(method)
116
+ def respond_to?(method, include_private = false)
104
117
  super || @worker_class.instance_methods.include?(method.to_sym)
105
118
  end
106
119
 
@@ -1,7 +1,7 @@
1
1
  module Celluloid
2
2
  # A proxy object returned from Celluloid::Actor.new/new_link which converts
3
3
  # the normal Ruby method protocol into an inter-actor message protocol
4
- class ActorProxy < AbstractProxy
4
+ class ActorProxy
5
5
  attr_reader :mailbox, :thread
6
6
 
7
7
  def initialize(actor)
@@ -10,17 +10,24 @@ module Celluloid
10
10
  @async_proxy = AsyncProxy.new(actor)
11
11
  @future_proxy = FutureProxy.new(actor)
12
12
  end
13
-
13
+
14
+ # allow querying the real class
15
+ alias :__class__ :class
16
+
14
17
  def class
15
18
  Actor.call @mailbox, :__send__, :class
16
19
  end
17
20
 
21
+ def send(meth, *args, &block)
22
+ Actor.call @mailbox, :send, meth, *args, &block
23
+ end
24
+
18
25
  def _send_(meth, *args, &block)
19
26
  Actor.call @mailbox, :__send__, meth, *args, &block
20
27
  end
21
28
 
22
29
  def inspect
23
- Actor.call @mailbox, :inspect
30
+ Actor.call(@mailbox, :inspect)
24
31
  rescue DeadActorError
25
32
  "#<Celluloid::Actor(#{@klass}) dead>"
26
33
  end
@@ -90,14 +97,7 @@ module Celluloid
90
97
 
91
98
  # method_missing black magic to call bang predicate methods asynchronously
92
99
  def method_missing(meth, *args, &block)
93
- # bang methods are async calls
94
- if meth.match(/!$/)
95
- unbanged_meth = meth.to_s
96
- unbanged_meth.slice!(-1, 1)
97
- Actor.async @mailbox, unbanged_meth, *args, &block
98
- else
99
- Actor.call @mailbox, meth, *args, &block
100
- end
100
+ Actor.call @mailbox, meth, *args, &block
101
101
  end
102
102
  end
103
103
  end
@@ -13,7 +13,11 @@ module Celluloid
13
13
 
14
14
  # method_missing black magic to call bang predicate methods asynchronously
15
15
  def method_missing(meth, *args, &block)
16
- Actor.async @mailbox, meth, *args, &block
16
+ if @mailbox == ::Thread.current[:celluloid_mailbox]
17
+ Actor.async @mailbox, :__send__, meth, *args, &block
18
+ else
19
+ Actor.async @mailbox, meth, *args, &block
20
+ end
17
21
  end
18
22
  end
19
23
  end
@@ -18,13 +18,15 @@ module Celluloid
18
18
  # Call was aborted due to caller error
19
19
  class ErrorResponse < Response
20
20
  def value
21
- if super.is_a? AbortError
22
- # Aborts are caused by caller error, so ensure they capture the
23
- # caller's backtrace instead of the receiver's
24
- raise super.cause.exception
25
- else
26
- raise super
21
+ ex = super
22
+ ex = ex.cause if ex.is_a? AbortError
23
+
24
+ if ex.backtrace
25
+ ex.backtrace << "(celluloid):0:in `remote procedure call'"
26
+ ex.backtrace.concat(caller)
27
27
  end
28
+
29
+ raise ex
28
30
  end
29
31
  end
30
32
  end
@@ -0,0 +1,94 @@
1
+ module Celluloid
2
+ class StackDump
3
+
4
+ class TaskState < Struct.new(:task_class, :status)
5
+ end
6
+
7
+ class ActorState
8
+ attr_accessor :subject_id, :subject_class, :name
9
+ attr_accessor :status, :tasks
10
+ attr_accessor :backtrace
11
+ end
12
+
13
+ class ThreadState < Struct.new(:thread_id, :backtrace)
14
+ end
15
+
16
+ attr_accessor :actors, :threads
17
+
18
+ def initialize
19
+ @actors = []
20
+ @threads = []
21
+
22
+ snapshot
23
+ end
24
+
25
+ def snapshot
26
+ Thread.list.each do |thread|
27
+ if actor = thread[:celluloid_actor]
28
+ @actors << snapshot_actor(actor)
29
+ else
30
+ @threads << snapshot_thread(thread)
31
+ end
32
+ end
33
+ end
34
+
35
+ def snapshot_actor(actor)
36
+ state = ActorState.new
37
+ state.subject_id = actor.subject.object_id
38
+ state.subject_class = actor.subject.class
39
+
40
+ tasks = actor.tasks
41
+ if tasks.empty?
42
+ state.status = :idle
43
+ else
44
+ state.status = :running
45
+ state.tasks = tasks.collect { |t| TaskState.new(t.class, t.status) }
46
+ end
47
+
48
+ state.backtrace = actor.thread.backtrace if actor.thread
49
+ state
50
+ end
51
+
52
+ def snapshot_thread(thread)
53
+ ThreadState.new(thread.object_id, thread.backtrace)
54
+ end
55
+
56
+ def dump(output = STDERR)
57
+ @actors.each do |actor|
58
+ string = ""
59
+ string << "Celluloid::Actor 0x#{actor.subject_id.to_s(16)}: #{actor.subject_class}"
60
+ string << " [#{actor.name}]" if actor.name
61
+ string << "\n"
62
+
63
+ if actor.status == :idle
64
+ string << "State: Idle (waiting for messages)\n"
65
+ else
66
+ string << "State: Running (executing tasks)\n"
67
+ string << "Tasks:\n"
68
+
69
+ actor.tasks.each_with_index do |task, i|
70
+ string << " #{i+1}) #{task.task_class}: #{task.status}\n"
71
+ end
72
+ end
73
+
74
+ display_backtrace actor.backtrace, string
75
+ output.print string
76
+ end
77
+
78
+ @threads.each do |thread|
79
+ string = ""
80
+ string << "Thread 0x#{thread.thread_id.to_s(16)}:\n"
81
+ display_backtrace thread.backtrace, string
82
+ output.print string
83
+ end
84
+ end
85
+
86
+ def display_backtrace(backtrace, output)
87
+ if backtrace
88
+ output << "\t" << backtrace.join("\n\t") << "\n\n"
89
+ else
90
+ output << "EMPTY BACKTRACE\n\n"
91
+ end
92
+ end
93
+ end
94
+ end
@@ -60,7 +60,7 @@ module Celluloid
60
60
  @members = []
61
61
  @registry = registry || Registry.root
62
62
 
63
- yield self if block_given?
63
+ yield current_actor if block_given?
64
64
  end
65
65
 
66
66
  def supervise(klass, *args, &block)
@@ -86,9 +86,11 @@ module Celluloid
86
86
  @members.map(&:actor)
87
87
  end
88
88
 
89
+ finalizer :finalize
90
+
89
91
  # Terminate the group
90
92
  def finalize
91
- @members.each(&:terminate)
93
+ @members.reverse_each(&:terminate)
92
94
  end
93
95
 
94
96
  # Restart a crashed actor
@@ -147,7 +149,7 @@ module Celluloid
147
149
 
148
150
  def terminate
149
151
  @actor.terminate if @actor
150
- rescue DeadActorError
152
+ rescue DeadActorError, MailboxError
151
153
  end
152
154
  end
153
155
  end
@@ -51,4 +51,15 @@ module Celluloid
51
51
 
52
52
  # Request for an actor to terminate
53
53
  class TerminationRequest < SystemEvent; end
54
+
55
+ # Signal a condition
56
+ class SignalConditionRequest < SystemEvent
57
+ def initialize(task, value)
58
+ @task, @value = task, value
59
+ end
60
+
61
+ def call
62
+ @task.resume(@value)
63
+ end
64
+ end
54
65
  end
@@ -1,21 +1,27 @@
1
1
  module Celluloid
2
+ class FiberStackError < StandardError; end
3
+
2
4
  # Tasks with a Fiber backend
3
- class TaskFiber
5
+ class TaskFiber < Task
4
6
  attr_reader :type, :status
5
7
 
6
8
  # Run the given block within a task
7
9
  def initialize(type)
8
- @type = type
9
- @status = :new
10
+ super
11
+
12
+ actor = Thread.current[:celluloid_actor]
13
+ mailbox = Thread.current[:celluloid_mailbox]
14
+ chain_id = Thread.current[:celluloid_chain_id]
10
15
 
11
- actor, mailbox = Thread.current[:actor], Thread.current[:mailbox]
12
16
  raise NotActorError, "can't create tasks outside of actors" unless actor
13
17
 
14
18
  @fiber = Fiber.new do
15
19
  @status = :running
16
- Thread.current[:actor] = actor
17
- Thread.current[:mailbox] = mailbox
18
- Thread.current[:task] = self
20
+ Thread.current[:celluloid_actor] = actor
21
+ Thread.current[:celluloid_mailbox] = mailbox
22
+ Thread.current[:celluloid_task] = self
23
+ Thread.current[:celluloid_chain_id] = chain_id
24
+
19
25
  actor.tasks << self
20
26
 
21
27
  begin
@@ -43,8 +49,10 @@ module Celluloid
43
49
  def resume(value = nil)
44
50
  @fiber.resume value
45
51
  nil
46
- rescue FiberError
47
- raise DeadTaskError, "cannot resume a dead task"
52
+ rescue SystemStackError => ex
53
+ raise FiberStackError, "#{ex} (please see https://github.com/celluloid/celluloid/wiki/Fiber-stack-errors)"
54
+ rescue FiberError => ex
55
+ raise DeadTaskError, "cannot resume a dead task (#{ex})"
48
56
  end
49
57
 
50
58
  # Terminate this task
@@ -1,33 +1,40 @@
1
1
  module Celluloid
2
2
  # Tasks with a Thread backend
3
- class TaskThread
3
+ class TaskThread < Task
4
4
  attr_reader :type, :status
5
5
 
6
6
  # Run the given block within a task
7
7
  def initialize(type)
8
- @type = type
9
- @status = :new
8
+ super
10
9
 
11
10
  @resume_queue = Queue.new
11
+ @exception_queue = Queue.new
12
12
  @yield_mutex = Mutex.new
13
13
  @yield_cond = ConditionVariable.new
14
14
 
15
- actor, mailbox = Thread.current[:actor], Thread.current[:mailbox]
15
+ actor = Thread.current[:celluloid_actor]
16
+ mailbox = Thread.current[:celluloid_mailbox]
17
+ chain_id = Thread.current[:celluloid_chain_id]
18
+
16
19
  raise NotActorError, "can't create tasks outside of actors" unless actor
17
20
 
18
- @thread = InternalPool.get do
21
+ @thread = Celluloid.internal_pool.get do
19
22
  begin
20
- unless @resume_queue.pop.is_a?(Task::TerminatedError)
21
- @status = :running
22
- Thread.current[:actor] = actor
23
- Thread.current[:mailbox] = mailbox
24
- Thread.current[:task] = self
25
- actor.tasks << self
26
-
27
- yield
28
- end
23
+ ex = @resume_queue.pop
24
+ raise ex if ex.is_a?(Task::TerminatedError)
25
+
26
+ @status = :running
27
+ Thread.current[:celluloid_actor] = actor
28
+ Thread.current[:celluloid_mailbox] = mailbox
29
+ Thread.current[:celluloid_task] = self
30
+ Thread.current[:celluloid_chain_id] = chain_id
31
+
32
+ actor.tasks << self
33
+ yield
29
34
  rescue Task::TerminatedError
30
35
  # Task was explicitly terminated
36
+ rescue Exception => ex
37
+ @exception_queue << ex
31
38
  ensure
32
39
  @status = :dead
33
40
  actor.tasks.delete self
@@ -55,6 +62,9 @@ module Celluloid
55
62
  @yield_mutex.synchronize do
56
63
  @resume_queue.push(value)
57
64
  @yield_cond.wait(@yield_mutex)
65
+ while @exception_queue.size > 0
66
+ raise @exception_queue.pop
67
+ end
58
68
  end
59
69
 
60
70
  nil
@@ -0,0 +1,59 @@
1
+ module Celluloid
2
+ # Asked to do task-related things outside a task
3
+ class NotTaskError < StandardError; end
4
+
5
+ # Trying to resume a dead task
6
+ class DeadTaskError < StandardError; end
7
+
8
+ # Tasks are interruptable/resumable execution contexts used to run methods
9
+ class Task
10
+ class TerminatedError < StandardError; end # kill a running task
11
+
12
+ # Obtain the current task
13
+ def self.current
14
+ Thread.current[:celluloid_task] or raise NotTaskError, "not within a task context"
15
+ end
16
+
17
+ # Suspend the running task, deferring to the scheduler
18
+ def self.suspend(status)
19
+ Task.current.suspend(status)
20
+ end
21
+
22
+ # Create a new task
23
+ def initialize(type)
24
+ @type = type
25
+ @status = :new
26
+ end
27
+ end
28
+
29
+ class TaskSet
30
+ include Enumerable
31
+
32
+ def initialize
33
+ @tasks = Set.new
34
+ end
35
+
36
+ def <<(task)
37
+ @tasks += [task]
38
+ end
39
+
40
+ def delete(task)
41
+ @tasks -= [task]
42
+ end
43
+
44
+ def each(&blk)
45
+ @tasks.each &blk
46
+ end
47
+
48
+ def first
49
+ @tasks.first
50
+ end
51
+
52
+ def empty?
53
+ @tasks.empty?
54
+ end
55
+ end
56
+ end
57
+
58
+ require 'celluloid/tasks/task_fiber'
59
+ require 'celluloid/tasks/task_thread'
@@ -7,7 +7,7 @@ module Celluloid
7
7
  @mutex = Mutex.new
8
8
  @join = ConditionVariable.new
9
9
 
10
- @thread = InternalPool.get do
10
+ @thread = Celluloid.internal_pool.get do
11
11
  begin
12
12
  yield
13
13
  ensure
@@ -35,5 +35,14 @@ module Celluloid
35
35
  @mutex.synchronize { @join.wait(@mutex) if @thread }
36
36
  self
37
37
  end
38
+
39
+ # Obtain the backtrace for this thread
40
+ def backtrace
41
+ @thread.backtrace
42
+ rescue NoMethodError
43
+ # undefined method `backtrace' for nil:NilClass
44
+ # Swallow this in case this ThreadHandle was terminated and @thread was
45
+ # set to nil
46
+ end
38
47
  end
39
48
  end
@@ -1,4 +1,4 @@
1
1
  module Celluloid
2
- VERSION = '0.12.0'
2
+ VERSION = '0.13.0'
3
3
  def self.version; VERSION; end
4
4
  end