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
data/lib/celluloid/fsm.rb CHANGED
@@ -84,30 +84,71 @@ module Celluloid
84
84
  #
85
85
  # Note: making additional state transitions will cancel delayed transitions
86
86
  def transition(state_name, options = {})
87
+ new_state = validate_and_sanitize_new_state(state_name)
88
+ return unless new_state
89
+
90
+ if handle_delayed_transitions(new_state, options[:delay])
91
+ return @delayed_transition
92
+ end
93
+
94
+ transition_with_callbacks!(new_state)
95
+ end
96
+
97
+ # Immediate state transition with no sanity checks, or callbacks. "Dangerous!"
98
+ def transition!(state_name)
99
+ @state = state_name
100
+ end
101
+
102
+ protected
103
+
104
+ def validate_and_sanitize_new_state(state_name)
87
105
  state_name = state_name.to_sym
88
- current_state = self.class.states[@state]
89
106
 
90
- return if current_state && current_state.name == state_name
107
+ return if current_state_name == state_name
91
108
 
92
109
  if current_state and not current_state.valid_transition? state_name
93
110
  valid = current_state.transitions.map(&:to_s).join(", ")
94
111
  raise ArgumentError, "#{self.class} can't change state from '#{@state}' to '#{state_name}', only to: #{valid}"
95
112
  end
96
113
 
97
- new_state = self.class.states[state_name]
114
+ new_state = states[state_name]
98
115
 
99
116
  unless new_state
100
- return if state_name == self.class.default_state
117
+ return if state_name == default_state
101
118
  raise ArgumentError, "invalid state for #{self.class}: #{state_name}"
102
119
  end
103
120
 
104
- if options[:delay]
121
+ new_state
122
+ end
123
+
124
+ def transition_with_callbacks!(state_name)
125
+ transition! state_name.name
126
+ state_name.call(self)
127
+ end
128
+
129
+ def states
130
+ self.class.states
131
+ end
132
+
133
+ def default_state
134
+ self.class.default_state
135
+ end
136
+
137
+ def current_state
138
+ states[@state]
139
+ end
140
+
141
+ def current_state_name
142
+ current_state && current_state.name || ''
143
+ end
144
+
145
+ def handle_delayed_transitions(new_state, delay)
146
+ if delay
105
147
  raise UnattachedError, "can't delay unless attached" unless @actor
106
148
  @delayed_transition.cancel if @delayed_transition
107
149
 
108
- @delayed_transition = @actor.after(options[:delay]) do
109
- transition! new_state.name
110
- new_state.call(self)
150
+ @delayed_transition = @actor.after(delay) do
151
+ transition_with_callbacks!(new_state)
111
152
  end
112
153
 
113
154
  return @delayed_transition
@@ -117,14 +158,6 @@ module Celluloid
117
158
  @delayed_transition.cancel
118
159
  @delayed_transition = nil
119
160
  end
120
-
121
- transition! new_state.name
122
- new_state.call(self)
123
- end
124
-
125
- # Immediate state transition with no sanity checks. "Dangerous!"
126
- def transition!(state_name)
127
- @state = state_name
128
161
  end
129
162
 
130
163
  # FSM states as declared by Celluloid::FSM.state
@@ -4,8 +4,11 @@ module Celluloid
4
4
  # Celluloid::Future objects allow methods and blocks to run in the
5
5
  # background, their values requested later
6
6
  class Future
7
+ attr_reader :address
8
+
7
9
  # Create a future bound to a given receiver, or with a block to compute
8
10
  def initialize(*args, &block)
11
+ @address = Celluloid.uuid
9
12
  @mutex = Mutex.new
10
13
  @ready = false
11
14
  @result = nil
@@ -13,7 +16,7 @@ module Celluloid
13
16
 
14
17
  if block
15
18
  @call = SyncCall.new(self, :call, args)
16
- InternalPool.get do
19
+ Celluloid.internal_pool.get do
17
20
  begin
18
21
  @call.dispatch(block)
19
22
  rescue
@@ -74,7 +77,7 @@ module Celluloid
74
77
  if result
75
78
  result.value
76
79
  else
77
- raise "Timed out"
80
+ raise TimeoutError, "Timed out"
78
81
  end
79
82
  end
80
83
  alias_method :call, :value
@@ -2,61 +2,79 @@ require 'thread'
2
2
 
3
3
  module Celluloid
4
4
  # Maintain a thread pool FOR SPEED!!
5
- module InternalPool
6
- @pool = []
7
- @mutex = Mutex.new
5
+ class InternalPool
6
+ attr_accessor :busy_size, :idle_size, :max_idle
8
7
 
9
- # TODO: should really adjust this based on usage
10
- @max_idle = 16
8
+ def initialize
9
+ @pool = []
10
+ @mutex = Mutex.new
11
+ @busy_size = @idle_size = 0
11
12
 
12
- class << self
13
- attr_accessor :max_idle
14
-
15
- # Get a thread from the pool, running the given block
16
- def get(&block)
17
- @mutex.synchronize do
18
- begin
19
- if @pool.empty?
20
- thread = create
21
- else
22
- thread = @pool.shift
23
- end
24
- end until thread.status # handle crashed threads
25
-
26
- thread[:queue] << block
27
- thread
28
- end
29
- end
13
+ # TODO: should really adjust this based on usage
14
+ @max_idle = 16
15
+ end
30
16
 
31
- # Return a thread to the pool
32
- def put(thread)
33
- @mutex.synchronize do
34
- if @pool.size >= @max_idle
35
- thread[:queue] << nil
17
+ # Get a thread from the pool, running the given block
18
+ def get(&block)
19
+ @mutex.synchronize do
20
+ begin
21
+ if @pool.empty?
22
+ thread = create
36
23
  else
37
- @pool << thread
24
+ thread = @pool.shift
25
+ @idle_size -= 1
38
26
  end
27
+ end until thread.status # handle crashed threads
28
+
29
+ @busy_size += 1
30
+ thread[:celluloid_queue] << block
31
+ thread
32
+ end
33
+ end
34
+
35
+ # Return a thread to the pool
36
+ def put(thread)
37
+ @mutex.synchronize do
38
+ if @pool.size >= @max_idle
39
+ thread[:celluloid_queue] << nil
40
+ else
41
+ clean_thread_locals(thread)
42
+ @pool << thread
43
+ @idle_size += 1
44
+ @busy_size -= 1
39
45
  end
40
46
  end
47
+ end
41
48
 
42
- # Create a new thread with an associated queue of procs to run
43
- def create
44
- queue = Queue.new
45
- thread = Thread.new do
46
- while proc = queue.pop
47
- begin
48
- proc.call
49
- rescue => ex
50
- Logger.crash("thread crashed", ex)
51
- end
52
-
53
- put thread
49
+ # Create a new thread with an associated queue of procs to run
50
+ def create
51
+ queue = Queue.new
52
+ thread = Thread.new do
53
+ while proc = queue.pop
54
+ begin
55
+ proc.call
56
+ rescue => ex
57
+ Logger.crash("thread crashed", ex)
54
58
  end
59
+
60
+ put thread
55
61
  end
62
+ end
56
63
 
57
- thread[:queue] = queue
58
- thread
64
+ thread[:celluloid_queue] = queue
65
+ thread
66
+ end
67
+
68
+ # Clean the thread locals of an incoming thread
69
+ def clean_thread_locals(thread)
70
+ thread.keys.each do |key|
71
+ next if key == :celluloid_queue
72
+
73
+ # Ruby seems to lack an API for deleting thread locals. WTF, Ruby?
74
+ thread[key] = nil
59
75
  end
60
76
  end
61
77
  end
62
- end
78
+
79
+ self.internal_pool = InternalPool.new
80
+ end
@@ -0,0 +1,46 @@
1
+ module Celluloid
2
+ class ActorProxy
3
+ # method_missing black magic to call bang predicate methods asynchronously
4
+ def method_missing(meth, *args, &block)
5
+ # bang methods are async calls
6
+ if meth.match(/!$/)
7
+ Logger.deprecate("'bang method'-style async syntax is deprecated and will be removed in Celluloid 1.0." +
8
+ "Call async methods with 'actor.async.method'.")
9
+
10
+ unbanged_meth = meth.to_s
11
+ unbanged_meth.slice!(-1, 1)
12
+ Actor.async @mailbox, unbanged_meth, *args, &block
13
+ else
14
+ Actor.call @mailbox, meth, *args, &block
15
+ end
16
+ end
17
+ end
18
+
19
+ module InstanceMethods
20
+ # Process async calls via method_missing
21
+ def method_missing(meth, *args, &block)
22
+ # bang methods are async calls
23
+ if meth.to_s.match(/!$/)
24
+ Logger.deprecate("'bang method'-style async syntax is deprecated and will be removed in Celluloid 1.0." +
25
+ "Call async methods with 'actor.async.method'.")
26
+
27
+ unbanged_meth = meth.to_s.sub(/!$/, '')
28
+ args.unshift unbanged_meth
29
+
30
+ call = AsyncCall.new(:__send__, args, block)
31
+ begin
32
+ Thread.current[:celluloid_actor].mailbox << call
33
+ rescue MailboxError
34
+ # Silently swallow asynchronous calls to dead actors. There's no way
35
+ # to reliably generate DeadActorErrors for async calls, so users of
36
+ # async calls should find other ways to deal with actors dying
37
+ # during an async call (i.e. linking/supervisors)
38
+ end
39
+
40
+ return
41
+ end
42
+
43
+ super
44
+ end
45
+ end
46
+ end
@@ -27,11 +27,6 @@ module Celluloid
27
27
  @links.each { |_, actor| yield(actor) }
28
28
  end
29
29
 
30
- # Send an event message to all actors
31
- def send_event(event)
32
- each { |actor| actor.mailbox << event }
33
- end
34
-
35
30
  # Generate a string representation
36
31
  def inspect
37
32
  links = self.map(&:inspect).join(',')
@@ -37,10 +37,10 @@ module Celluloid
37
37
  end
38
38
  end
39
39
 
40
- # Format an exception message
41
- def format_exception(exception)
42
- str = "#{exception.class}: #{exception.to_s}\n"
43
- str << exception.backtrace.join("\n")
40
+ # Note a deprecation
41
+ def deprecate(message)
42
+ trace = caller.join("\n\t")
43
+ warn "DEPRECATION WARNING: #{message}\n\t#{trace}"
44
44
  end
45
45
 
46
46
  # Define an exception handler
@@ -49,5 +49,15 @@ module Celluloid
49
49
  @exception_handlers << block
50
50
  nil
51
51
  end
52
+
53
+ # Format an exception message
54
+ def format_exception(exception)
55
+ str = "#{exception.class}: #{exception.to_s}\n\t"
56
+ if exception.backtrace
57
+ str << exception.backtrace.join("\n\t")
58
+ else
59
+ str << "EMPTY BACKTRACE\n\t"
60
+ end
61
+ end
52
62
  end
53
63
  end
@@ -0,0 +1,21 @@
1
+ module Celluloid
2
+ # Wraps all events and context for a single incident.
3
+ class Incident
4
+ attr_accessor :pid
5
+ attr_accessor :events, :triggering_event
6
+
7
+ def initialize(events, triggering_event=nil)
8
+ @events = events
9
+ @triggering_event = triggering_event
10
+ @pid = $$
11
+ end
12
+
13
+ # Merge two incidents together. This may be useful if two incidents occur at the same time.
14
+ def merge(*other_incidents)
15
+ merged_events = other_incidents.flatten.inject(events) do |events, incident|
16
+ events += incident.events
17
+ end
18
+ Incident.new(merged_events.sort, triggering_event)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,129 @@
1
+ require 'logger'
2
+ module Celluloid
3
+ # A logger that holds all messages in circular buffers, then flushes the buffers
4
+ # when an event occurs at a configurable severity threshold.
5
+ #
6
+ # Unlike ruby's Logger, this class only supports a single progname.
7
+ class IncidentLogger
8
+ module Severity
9
+ include ::Logger::Severity
10
+
11
+ TRACE = -1
12
+
13
+ def severity_to_string(severity)
14
+ case severity
15
+ when TRACE then 'TRACE'
16
+ when DEBUG then 'DEBUG'
17
+ when INFO then 'INFO'
18
+ when WARN then 'WARN'
19
+ when ERROR then 'ERROR'
20
+ when FATAL then 'FATAL'
21
+ when UNKNOWN then 'UNKNOWN'
22
+ end
23
+ end
24
+
25
+ end
26
+ include Severity
27
+
28
+ # The progname (facility) for this instance.
29
+ attr_accessor :progname
30
+
31
+ # The logging level. Messages below this severity will not be logged at all.
32
+ attr_accessor :level
33
+
34
+ # The incident threshold. Messages at or above this severity will generate an
35
+ # incident and be published to incident reporters.
36
+ attr_accessor :threshold
37
+
38
+ # The buffer size limit. Each log level will retain this number of messages
39
+ # at maximum.
40
+ attr_accessor :sizelimit
41
+
42
+ attr_accessor :buffers
43
+
44
+ # Create a new IncidentLogger.
45
+ def initialize(progname=nil, options={})
46
+ @progname = progname || "default"
47
+ @level = options[:level] || DEBUG
48
+ @threshold = options[:threshold] || ERROR
49
+ @sizelimit = options[:sizelimit] || 100
50
+
51
+ @buffer_mutex = Mutex.new
52
+ @buffers = Hash.new do |progname_hash, progname|
53
+ @buffer_mutex.synchronize do
54
+ progname_hash[progname] = Hash.new do |severity_hash, severity|
55
+ severity_hash[severity] = RingBuffer.new(@sizelimit)
56
+ end
57
+ end
58
+ end
59
+
60
+ # When the IncidentLogger itself encounters an error, it falls back to logging to stderr
61
+ @fallback_logger = ::Logger.new(STDERR)
62
+ @fallback_logger.progname = "FALLBACK"
63
+ end
64
+
65
+ # add an event.
66
+ def add(severity, message=nil, progname=nil, &block)
67
+ progname ||= @progname
68
+ severity ||= UNKNOWN
69
+
70
+ if severity < @level
71
+ return event.id
72
+ end
73
+
74
+ if message.nil? && !block_given?
75
+ message = progname
76
+ progname = @progname
77
+ end
78
+
79
+ event = LogEvent.new(severity, message, progname, &block)
80
+
81
+ @buffers[progname][severity] << event
82
+
83
+ if severity >= @threshold
84
+ begin
85
+ Celluloid::Notifications.notifier.async.publish(incident_topic, create_incident(event))
86
+ rescue => ex
87
+ @fallback_logger.error(ex)
88
+ end
89
+ end
90
+ event.id
91
+ end
92
+ alias :log :add
93
+
94
+ # See docs for Logger#info
95
+ def trace (progname=nil, &block); add(TRACE, nil, progname, &block); end
96
+ def debug (progname=nil, &block); add(DEBUG, nil, progname, &block); end
97
+ def info (progname=nil, &block); add(INFO, nil, progname, &block); end
98
+ def warn (progname=nil, &block); add(WARN, nil, progname, &block); end
99
+ def error (progname=nil, &block); add(ERROR, nil, progname, &block); end
100
+ def fatal (progname=nil, &block); add(FATAL, nil, progname, &block); end
101
+ def unknown (progname=nil, &block); add(UNKNOWN, nil, progname, &block); end
102
+
103
+ def flush
104
+ messages = []
105
+ @buffer_mutex.synchronize do
106
+ @buffers.each do |progname, severities|
107
+ severities.each do |severity, buffer|
108
+ messages += buffer.flush
109
+ end
110
+ end
111
+ end
112
+ messages.sort
113
+ end
114
+
115
+ def clear
116
+ @buffer_mutex.synchronize do
117
+ @buffers.each { |buffer| buffer.clear }
118
+ end
119
+ end
120
+
121
+ def create_incident(event=nil)
122
+ Incident.new(flush, event)
123
+ end
124
+
125
+ def incident_topic
126
+ "log.incident.#{@progname}"
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,48 @@
1
+ require 'logger'
2
+ module Celluloid
3
+ # Subscribes to log incident topics to report on them.
4
+ class IncidentReporter
5
+ include Celluloid
6
+ include Celluloid::Notifications
7
+
8
+ # get the time from the event
9
+ class Formatter < ::Logger::Formatter
10
+ def call(severity, time, progname, msg)
11
+ super(severity, msg.time, progname, msg.message)
12
+ end
13
+ end
14
+
15
+ def initialize(*args)
16
+ subscribe(/log\.incident/, :report)
17
+ @logger = ::Logger.new(*args)
18
+ @logger.formatter = Formatter.new
19
+ @silenced = false
20
+ end
21
+
22
+ def report(topic, incident)
23
+ return if @silenced
24
+
25
+ header = "INCIDENT"
26
+ header << " AT #{incident.triggering_event.time}" if incident.triggering_event
27
+ @logger << header
28
+ @logger << "\n"
29
+ @logger << "====================\n"
30
+ incident.events.each do |event|
31
+ @logger.add(event.severity, event, event.progname)
32
+ end
33
+ @logger << "====================\n"
34
+ end
35
+
36
+ def silence
37
+ @silenced = true
38
+ end
39
+
40
+ def unsilence
41
+ @silenced = false
42
+ end
43
+
44
+ def silenced?
45
+ @silenced
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,20 @@
1
+ module Celluloid
2
+ # Wraps a single log event.
3
+ class LogEvent
4
+ attr_accessor :id, :severity, :message, :progname, :time
5
+
6
+ def initialize(severity, message, progname, time=Time.now, &block)
7
+ # This id should be ordered. For now relies on Celluloid::UUID to be ordered.
8
+ # May want to use a generation/counter strategy for independence of uuid.
9
+ @id = Celluloid::UUID.generate
10
+ @severity = severity
11
+ @message = block_given? ? yield : message
12
+ @progname = progname
13
+ @time = time
14
+ end
15
+
16
+ def <=>(other)
17
+ @id <=> other.id
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,65 @@
1
+ module Celluloid
2
+ class RingBuffer
3
+ def initialize(size)
4
+ @size = size
5
+ @start = 0
6
+ @count = 0
7
+ @buffer = Array.new(size)
8
+ @mutex = Mutex.new
9
+ end
10
+
11
+ def full?
12
+ @count == @size
13
+ end
14
+
15
+ def empty?
16
+ @count == 0
17
+ end
18
+
19
+ def push(value)
20
+ @mutex.synchronize do
21
+ stop = (@start + @count) % @size
22
+ @buffer[stop] = value
23
+ if full?
24
+ @start = (@start + 1) % @size
25
+ else
26
+ @count += 1
27
+ end
28
+ value
29
+ end
30
+ end
31
+ alias :<< :push
32
+
33
+ def shift
34
+ @mutex.synchronize do
35
+ remove_element
36
+ end
37
+ end
38
+
39
+ def flush
40
+ values = []
41
+ @mutex.synchronize do
42
+ while !empty?
43
+ values << remove_element
44
+ end
45
+ end
46
+ values
47
+ end
48
+
49
+ def clear
50
+ @buffer = Array.new(@size)
51
+ @start = 0
52
+ @count = 0
53
+ end
54
+
55
+ private
56
+
57
+ def remove_element
58
+ return nil if empty?
59
+ value, @buffer[@start] = @buffer[@start], nil
60
+ @start = (@start + 1) % @size
61
+ @count -= 1
62
+ value
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,5 @@
1
+ require 'celluloid/logging/log_event'
2
+ require 'celluloid/logging/incident'
3
+ require 'celluloid/logging/ring_buffer'
4
+ require 'celluloid/logging/incident_logger'
5
+ require 'celluloid/logging/incident_reporter'
@@ -10,9 +10,10 @@ module Celluloid
10
10
  include Enumerable
11
11
 
12
12
  # A unique address at which this mailbox can be found
13
- alias_method :address, :object_id
13
+ attr_reader :address
14
14
 
15
15
  def initialize
16
+ @address = Celluloid.uuid
16
17
  @messages = []
17
18
  @mutex = Mutex.new
18
19
  @dead = false
@@ -1,6 +1,7 @@
1
1
  module Celluloid
2
2
  # Method handles that route through an actor proxy
3
3
  class Method
4
+
4
5
  def initialize(actor, name)
5
6
  raise NameError, "undefined method `#{name}'" unless actor.respond_to? name
6
7
 
@@ -8,6 +9,10 @@ module Celluloid
8
9
  @klass = @actor.class
9
10
  end
10
11
 
12
+ def arity
13
+ @actor._send_(:method, @name).arity
14
+ end
15
+
11
16
  def call(*args, &block)
12
17
  @actor._send_(@name, *args, &block)
13
18
  end