aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport
diff options
context:
space:
mode:
authorEmilio Tagua <miloops@gmail.com>2009-09-22 15:08:45 -0300
committerEmilio Tagua <miloops@gmail.com>2009-09-22 15:08:45 -0300
commit5f9540e4830ace3459b4018006573bad7fb30b53 (patch)
treed8bdf97e712fec925bf6fe5ddccf4a7e9683c7a0 /activesupport
parenta294d8362bfb62b7133ad0799ae1327cd5ddd1e4 (diff)
parent24074796031d76d3ac2f5f0078d75699cce1bc54 (diff)
downloadrails-5f9540e4830ace3459b4018006573bad7fb30b53.tar.gz
rails-5f9540e4830ace3459b4018006573bad7fb30b53.tar.bz2
rails-5f9540e4830ace3459b4018006573bad7fb30b53.zip
Merge commit 'rails/master'
Diffstat (limited to 'activesupport')
-rw-r--r--activesupport/lib/active_support/autoload.rb1
-rw-r--r--activesupport/lib/active_support/cache.rb60
-rw-r--r--activesupport/lib/active_support/cache/file_store.rb46
-rw-r--r--activesupport/lib/active_support/cache/mem_cache_store.rb31
-rw-r--r--activesupport/lib/active_support/cache/memory_store.rb25
-rw-r--r--activesupport/lib/active_support/new_callbacks.rb159
-rw-r--r--activesupport/lib/active_support/orchestra.rb103
-rw-r--r--activesupport/test/new_callback_inheritance_test.rb2
-rw-r--r--activesupport/test/orchestra_test.rb161
9 files changed, 461 insertions, 127 deletions
diff --git a/activesupport/lib/active_support/autoload.rb b/activesupport/lib/active_support/autoload.rb
index 75706855d6..423d5448c3 100644
--- a/activesupport/lib/active_support/autoload.rb
+++ b/activesupport/lib/active_support/autoload.rb
@@ -17,6 +17,7 @@ module ActiveSupport
autoload :Multibyte, 'active_support/multibyte'
autoload :NewCallbacks, 'active_support/new_callbacks'
autoload :OptionMerger, 'active_support/option_merger'
+ autoload :Orchestra, 'active_support/orchestra'
autoload :OrderedHash, 'active_support/ordered_hash'
autoload :OrderedOptions, 'active_support/ordered_options'
autoload :Rescuable, 'active_support/rescuable'
diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb
index e28df8efa5..25f9555388 100644
--- a/activesupport/lib/active_support/cache.rb
+++ b/activesupport/lib/active_support/cache.rb
@@ -107,16 +107,14 @@ module ActiveSupport
class Store
cattr_accessor :logger
- attr_reader :silence, :logger_off
+ attr_reader :silence
+ alias :silence? :silence
def silence!
@silence = true
self
end
- alias silence? silence
- alias logger_off? logger_off
-
# Fetches data from the cache, using the given key. If there is data in
# the cache with the given key, then that data is returned.
#
@@ -157,26 +155,13 @@ module ActiveSupport
# cache.fetch("foo") # => "bar"
# sleep(6)
# cache.fetch("foo") # => nil
- def fetch(key, options = {})
- @logger_off = true
+ def fetch(key, options = {}, &block)
if !options[:force] && value = read(key, options)
- @logger_off = false
- log("hit", key, options)
value
elsif block_given?
- @logger_off = false
- log("miss", key, options)
-
- value = nil
- ms = Benchmark.ms { value = yield }
-
- @logger_off = true
- write(key, value, options)
- @logger_off = false
-
- log('write (will save %.2fms)' % ms, key, nil)
-
- value
+ result = instrument(:generate, key, options, &block)
+ write(key, result, options)
+ result
end
end
@@ -191,8 +176,8 @@ module ActiveSupport
# For example, FileStore supports the +:expires_in+ option, which
# makes the method return nil for cache items older than the specified
# period.
- def read(key, options = nil)
- log("read", key, options)
+ def read(key, options = nil, &block)
+ instrument(:read, key, options, &block)
end
# Writes the given value to the cache, with the given key.
@@ -210,20 +195,20 @@ module ActiveSupport
# cache.read("foo") # => "bar"
# sleep(6)
# cache.read("foo") # => nil
- def write(key, value, options = nil)
- log("write", key, options)
+ def write(key, value, options = nil, &block)
+ instrument(:write, key, options, &block)
end
- def delete(key, options = nil)
- log("delete", key, options)
+ def delete(key, options = nil, &block)
+ instrument(:delete, key, options, &block)
end
- def delete_matched(matcher, options = nil)
- log("delete matched", matcher.inspect, options)
+ def delete_matched(matcher, options = nil, &block)
+ instrument(:delete_matched, matcher.inspect, options, &block)
end
- def exist?(key, options = nil)
- log("exist?", key, options)
+ def exist?(key, options = nil, &block)
+ instrument(:exist?, key, options, &block)
end
def increment(key, amount = 1)
@@ -247,14 +232,21 @@ module ActiveSupport
private
def expires_in(options)
expires_in = options && options[:expires_in]
-
raise ":expires_in must be a number" if expires_in && !expires_in.is_a?(Numeric)
-
expires_in || 0
end
+ def instrument(operation, key, options, &block)
+ payload = { :key => key }
+ payload.merge!(options) if options.is_a?(Hash)
+
+ event = ActiveSupport::Orchestra.instrument(:"cache_#{operation}", payload, &block)
+ log("#{operation} (%.1fms)" % event.duration, key, options)
+ event.result
+ end
+
def log(operation, key, options)
- logger.debug("Cache #{operation}: #{key}#{options ? " (#{options.inspect})" : ""}") if logger && !silence? && !logger_off?
+ logger.debug("Cache #{operation}: #{key}#{options ? " (#{options.inspect})" : ""}") if logger && !silence?
end
end
end
diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb
index 75eed5ed94..7521efe7c5 100644
--- a/activesupport/lib/active_support/cache/file_store.rb
+++ b/activesupport/lib/active_support/cache/file_store.rb
@@ -16,49 +16,53 @@ module ActiveSupport
# - +:expires_in+ - the number of seconds that this value may stay in
# the cache.
def read(name, options = nil)
- super
+ super do
+ file_name = real_file_path(name)
+ expires = expires_in(options)
- file_name = real_file_path(name)
- expires = expires_in(options)
-
- if File.exist?(file_name) && (expires <= 0 || Time.now - File.mtime(file_name) < expires)
- File.open(file_name, 'rb') { |f| Marshal.load(f) }
+ if File.exist?(file_name) && (expires <= 0 || Time.now - File.mtime(file_name) < expires)
+ File.open(file_name, 'rb') { |f| Marshal.load(f) }
+ end
end
end
# Writes a value to the cache.
def write(name, value, options = nil)
- super
- ensure_cache_path(File.dirname(real_file_path(name)))
- File.atomic_write(real_file_path(name), cache_path) { |f| Marshal.dump(value, f) }
- value
+ super do
+ ensure_cache_path(File.dirname(real_file_path(name)))
+ File.atomic_write(real_file_path(name), cache_path) { |f| Marshal.dump(value, f) }
+ value
+ end
rescue => e
logger.error "Couldn't create cache directory: #{name} (#{e.message})" if logger
end
def delete(name, options = nil)
- super
- File.delete(real_file_path(name))
+ super do
+ File.delete(real_file_path(name))
+ end
rescue SystemCallError => e
# If there's no cache, then there's nothing to complain about
end
def delete_matched(matcher, options = nil)
- super
- search_dir(@cache_path) do |f|
- if f =~ matcher
- begin
- File.delete(f)
- rescue SystemCallError => e
- # If there's no cache, then there's nothing to complain about
+ super do
+ search_dir(@cache_path) do |f|
+ if f =~ matcher
+ begin
+ File.delete(f)
+ rescue SystemCallError => e
+ # If there's no cache, then there's nothing to complain about
+ end
end
end
end
end
def exist?(name, options = nil)
- super
- File.exist?(real_file_path(name))
+ super do
+ File.exist?(real_file_path(name))
+ end
end
private
diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb
index 7c97b05261..ea38baa9ad 100644
--- a/activesupport/lib/active_support/cache/mem_cache_store.rb
+++ b/activesupport/lib/active_support/cache/mem_cache_store.rb
@@ -54,8 +54,9 @@ module ActiveSupport
end
def read(key, options = nil) # :nodoc:
- super
- @data.get(key, raw?(options))
+ super do
+ @data.get(key, raw?(options))
+ end
rescue MemCache::MemCacheError => e
logger.error("MemCacheError (#{e}): #{e.message}")
nil
@@ -69,22 +70,24 @@ module ActiveSupport
# - <tt>:expires_in</tt> - the number of seconds that this value may stay in
# the cache. See ActiveSupport::Cache::Store#write for an example.
def write(key, value, options = nil)
- super
- method = options && options[:unless_exist] ? :add : :set
- # memcache-client will break the connection if you send it an integer
- # in raw mode, so we convert it to a string to be sure it continues working.
- value = value.to_s if raw?(options)
- response = @data.send(method, key, value, expires_in(options), raw?(options))
- response == Response::STORED
+ super do
+ method = options && options[:unless_exist] ? :add : :set
+ # memcache-client will break the connection if you send it an integer
+ # in raw mode, so we convert it to a string to be sure it continues working.
+ value = value.to_s if raw?(options)
+ response = @data.send(method, key, value, expires_in(options), raw?(options))
+ response == Response::STORED
+ end
rescue MemCache::MemCacheError => e
logger.error("MemCacheError (#{e}): #{e.message}")
false
end
def delete(key, options = nil) # :nodoc:
- super
- response = @data.delete(key, expires_in(options))
- response == Response::DELETED
+ super do
+ response = @data.delete(key, expires_in(options))
+ response == Response::DELETED
+ end
rescue MemCache::MemCacheError => e
logger.error("MemCacheError (#{e}): #{e.message}")
false
@@ -94,7 +97,9 @@ module ActiveSupport
# Doesn't call super, cause exist? in memcache is in fact a read
# But who cares? Reading is very fast anyway
# Local cache is checked first, if it doesn't know then memcache itself is read from
- !read(key, options).nil?
+ super do
+ !read(key, options).nil?
+ end
end
def increment(key, amount = 1) # :nodoc:
diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb
index 21ba79cf3d..66ce1bc93a 100644
--- a/activesupport/lib/active_support/cache/memory_store.rb
+++ b/activesupport/lib/active_support/cache/memory_store.rb
@@ -20,28 +20,33 @@ module ActiveSupport
end
def read(name, options = nil)
- super
- @data[name]
+ super do
+ @data[name]
+ end
end
def write(name, value, options = nil)
- super
- @data[name] = (value.duplicable? ? value.dup : value).freeze
+ super do
+ @data[name] = (value.duplicable? ? value.dup : value).freeze
+ end
end
def delete(name, options = nil)
- super
- @data.delete(name)
+ super do
+ @data.delete(name)
+ end
end
def delete_matched(matcher, options = nil)
- super
- @data.delete_if { |k,v| k =~ matcher }
+ super do
+ @data.delete_if { |k,v| k =~ matcher }
+ end
end
def exist?(name,options = nil)
- super
- @data.has_key?(name)
+ super do
+ @data.has_key?(name)
+ end
end
def clear
diff --git a/activesupport/lib/active_support/new_callbacks.rb b/activesupport/lib/active_support/new_callbacks.rb
index e78a3b2b6f..2f0853d84a 100644
--- a/activesupport/lib/active_support/new_callbacks.rb
+++ b/activesupport/lib/active_support/new_callbacks.rb
@@ -92,10 +92,10 @@ module ActiveSupport
class Callback
@@_callback_sequence = 0
- attr_accessor :name, :filter, :kind, :options, :per_key, :klass
+ attr_accessor :chain, :filter, :kind, :options, :per_key, :klass
- def initialize(name, filter, kind, options, klass)
- @name, @kind, @klass = name, kind, klass
+ def initialize(chain, filter, kind, options, klass)
+ @chain, @kind, @klass = chain, kind, klass
normalize_options!(options)
@per_key = options.delete(:per_key)
@@ -107,9 +107,9 @@ module ActiveSupport
_compile_per_key_options
end
- def clone(klass)
+ def clone(chain, klass)
obj = super()
- obj.name = name
+ obj.chain = chain
obj.klass = klass
obj.per_key = @per_key.dup
obj.options = @options.dup
@@ -117,7 +117,6 @@ module ActiveSupport
obj.per_key[:unless] = @per_key[:unless].dup
obj.options[:if] = @options[:if].dup
obj.options[:unless] = @options[:unless].dup
- obj.options[:scope] = @options[:scope].dup
obj
end
@@ -125,14 +124,15 @@ module ActiveSupport
options[:if] = Array.wrap(options[:if])
options[:unless] = Array.wrap(options[:unless])
- options[:scope] ||= [:kind]
- options[:scope] = Array.wrap(options[:scope])
-
options[:per_key] ||= {}
options[:per_key][:if] = Array.wrap(options[:per_key][:if])
options[:per_key][:unless] = Array.wrap(options[:per_key][:unless])
end
+ def name
+ chain.name
+ end
+
def next_id
@@_callback_sequence += 1
end
@@ -168,15 +168,12 @@ module ActiveSupport
# This will supply contents for before and around filters, and no
# contents for after filters (for the forward pass).
- def start(key = nil, options = {})
- object, terminator = (options || {}).values_at(:object, :terminator)
+ def start(key=nil, object=nil)
return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
- terminator ||= false
-
# options[0] is the compiled form of supplied conditions
# options[1] is the "end" for the conditional
-
+ #
if @kind == :before || @kind == :around
if @kind == :before
# if condition # before_save :filter_name, :if => :condition
@@ -185,7 +182,7 @@ module ActiveSupport
filter = <<-RUBY_EVAL
unless halted
result = #{@filter}
- halted = (#{terminator})
+ halted = (#{chain.config[:terminator]})
end
RUBY_EVAL
@@ -226,8 +223,7 @@ module ActiveSupport
# This will supply contents for around and after filters, but not
# before filters (for the backward pass).
- def end(key = nil, options = {})
- object = (options || {})[:object]
+ def end(key=nil, object=nil)
return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
if @kind == :around || @kind == :after
@@ -295,14 +291,15 @@ module ActiveSupport
"(#{filter})"
when Proc
@klass.send(:define_method, method_name, &filter)
- return method_name if filter.arity == 0
+ return method_name if filter.arity <= 0
method_name << (filter.arity == 1 ? "(self)" : " self, Proc.new ")
else
@klass.send(:define_method, "#{method_name}_object") { filter }
_normalize_legacy_filter(kind, filter)
- method_to_call = @options[:scope].map{ |s| s.is_a?(Symbol) ? send(s) : s }.join("_")
+ scopes = Array.wrap(chain.config[:scope])
+ method_to_call = scopes.map{ |s| s.is_a?(Symbol) ? send(s) : s }.join("_")
@klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{method_name}(&blk)
@@ -331,37 +328,52 @@ module ActiveSupport
# An Array with a compile method
class CallbackChain < Array
- attr_reader :symbol, :config
-
- def initialize(symbol, config)
- @symbol = symbol
- @config = config
+ attr_reader :name, :config
+
+ def initialize(name, config)
+ @name = name
+ @config = {
+ :terminator => "false",
+ :rescuable => false,
+ :scope => [ :kind ]
+ }.merge(config)
end
- def compile(key=nil, options={})
- options = config.merge(options)
-
+ def compile(key=nil, object=nil)
method = []
method << "value = nil"
method << "halted = false"
each do |callback|
- method << callback.start(key, options)
+ method << callback.start(key, object)
+ end
+
+ if config[:rescuable]
+ method << "rescued_error = nil"
+ method << "begin"
end
method << "value = yield if block_given? && !halted"
+ if config[:rescuable]
+ method << "rescue Exception => e"
+ method << "rescued_error = e"
+ method << "end"
+ end
+
reverse_each do |callback|
- method << callback.end(key, options)
+ method << callback.end(key, object)
end
+ method << "raise rescued_error if rescued_error" if config[:rescuable]
method << "halted ? false : (block_given? ? value : true)"
method.compact.join("\n")
end
def clone(klass)
- chain = CallbackChain.new(@symbol, @config.dup)
- chain.push(*map {|c| c.clone(klass)})
+ chain = CallbackChain.new(@name, @config.dup)
+ callbacks = map { |c| c.clone(chain, klass) }
+ chain.push(*callbacks)
end
end
@@ -374,7 +386,7 @@ module ActiveSupport
# save
# end
#
- # The _run_set_callback :save,s method can optionally take a key, which
+ # The _run_set_callback :save method can optionally take a key, which
# will be used to compile an optimized callback method for each
# key. See #define_callbacks for more information.
#
@@ -407,32 +419,35 @@ module ActiveSupport
# key. It creates a new callback method for the key, calculating
# which callbacks can be omitted because of per_key conditions.
#
- def __create_keyed_callback(name, kind, obj, &blk) #:nodoc:
+ def __create_keyed_callback(name, kind, object, &blk) #:nodoc:
@_keyed_callbacks ||= {}
@_keyed_callbacks[name] ||= begin
- str = send("_#{kind}_callbacks").compile(name, :object => obj)
+ str = send("_#{kind}_callbacks").compile(name, object)
class_eval "def #{name}() #{str} end", __FILE__, __LINE__
true
end
end
- def __update_callbacks(name, filters = CallbackChain.new(name, {}), block = nil)
+ # This is used internally to append, prepend and skip callbacks to the
+ # CallbackChain.
+ #
+ def __update_callbacks(name, filters = [], block = nil) #:nodoc:
type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before
options = filters.last.is_a?(Hash) ? filters.pop : {}
filters.unshift(block) if block
- callbacks = send("_#{name}_callbacks")
- yield callbacks, type, filters, options if block_given?
+ chain = send("_#{name}_callbacks")
+ yield chain, type, filters, options if block_given?
__define_runner(name)
end
- # Define callbacks.
+ # Set callbacks for a previously defined callback.
#
# Syntax:
# set_callback :save, :before, :before_meth
# set_callback :save, :after, :after_meth, :if => :condition
- # set_callback :save, :around {|r| stuff; yield; stuff }
+ # set_callback :save, :around, lambda { |r| stuff; yield; stuff }
#
# It also updates the _run_<name>_callbacks method, which is the public
# API to run the callbacks. Use skip_callback to skip any defined one.
@@ -448,43 +463,91 @@ module ActiveSupport
# Per-Key conditions are evaluated only once per use of a given key.
# In the case of the above example, you would do:
#
- # run_dispatch_callbacks(action_name) { ... dispatch stuff ... }
+ # _run_dispatch_callbacks(action_name) { ... dispatch stuff ... }
#
# In that case, each action_name would get its own compiled callback
# method that took into consideration the per_key conditions. This
# is a speed improvement for ActionPack.
#
def set_callback(name, *filters, &block)
- __update_callbacks(name, filters, block) do |callbacks, type, filters, options|
+ __update_callbacks(name, filters, block) do |chain, type, filters, options|
filters.map! do |filter|
- callbacks.delete_if {|c| c.matches?(type, filter) }
- Callback.new(name, filter, type, options.merge(callbacks.config), self)
+ chain.delete_if {|c| c.matches?(type, filter) }
+ Callback.new(chain, filter, type, options.dup, self)
end
- options[:prepend] ? callbacks.unshift(*filters) : callbacks.push(*filters)
+ options[:prepend] ? chain.unshift(*filters) : chain.push(*filters)
end
end
+ # Skip a previously defined callback for a given type.
+ #
def skip_callback(name, *filters, &block)
- __update_callbacks(name, filters, block) do |callbacks, type, filters, options|
+ __update_callbacks(name, filters, block) do |chain, type, filters, options|
+ chain = send("_#{name}_callbacks=", chain.clone(self))
+
filters.each do |filter|
- callbacks = send("_#{name}_callbacks=", callbacks.clone(self))
- filter = callbacks.find {|c| c.matches?(type, filter) }
+ filter = chain.find {|c| c.matches?(type, filter) }
if filter && options.any?
filter.recompile!(options, options[:per_key] || {})
else
- callbacks.delete(filter)
+ chain.delete(filter)
end
end
end
end
+ # Reset callbacks for a given type.
+ #
def reset_callbacks(symbol)
send("_#{symbol}_callbacks").clear
__define_runner(symbol)
end
+ # Define callbacks types.
+ #
+ # ==== Example
+ #
+ # define_callbacks :validate
+ #
+ # ==== Options
+ #
+ # * <tt>:terminator</tt> - Indicates when a before filter is considered
+ # to be halted.
+ #
+ # define_callbacks :validate, :terminator => "result == false"
+ #
+ # In the example above, if any before validate callbacks returns false,
+ # other callbacks are not executed. Defaults to "false".
+ #
+ # * <tt>:rescuable</tt> - By default, after filters are not executed if
+ # the given block or an before_filter raises an error. Supply :rescuable => true
+ # to change this behavior.
+ #
+ # * <tt>:scope</tt> - Show which methods should be executed when a class
+ # is giben as callback:
+ #
+ # define_callbacks :filters, :scope => [ :kind ]
+ #
+ # When a class is given:
+ #
+ # before_filter MyFilter
+ #
+ # It will call the type of the filter in the given class, which in this
+ # case, is "before".
+ #
+ # If, for instance, you supply the given scope:
+ #
+ # define_callbacks :validate, :scope => [ :kind, :name ]
+ #
+ # It will call "#{kind}_#{name}" in the given class. So "before_validate"
+ # will be called in the class below:
+ #
+ # before_validate MyValidation
+ #
+ # Defaults to :kind.
+ #
def define_callbacks(*symbols)
config = symbols.last.is_a?(Hash) ? symbols.pop : {}
symbols.each do |symbol|
diff --git a/activesupport/lib/active_support/orchestra.rb b/activesupport/lib/active_support/orchestra.rb
new file mode 100644
index 0000000000..efe30669d8
--- /dev/null
+++ b/activesupport/lib/active_support/orchestra.rb
@@ -0,0 +1,103 @@
+require 'thread'
+
+module ActiveSupport
+ # Orchestra provides an instrumentation API for Ruby. To instrument an action
+ # in Ruby you just need to:
+ #
+ # ActiveSupport::Orchestra.instrument(:render, :extra => :information) do
+ # render :text => "Foo"
+ # end
+ #
+ # Those actions are consumed by listeners. A listener is anything that responds
+ # to push. You can even register an array:
+ #
+ # @listener = []
+ # ActiveSupport::Orchestra.register @listener
+ #
+ # ActiveSupport::Orchestra.instrument(:render, :extra => :information) do
+ # render :text => "Foo"
+ # end
+ #
+ # event #=> ActiveSupport::Orchestra::Event
+ # event.name #=> :render
+ # event.duration #=> 10 (in miliseconds)
+ # event.result #=> "Foo"
+ # event.payload #=> { :extra => :information }
+ #
+ # Orchestra ships with a default listener implementation which puts events in
+ # a stream and consume them in a Thread. This implementation is thread safe
+ # and is available at ActiveSupport::Orchestra::Listener.
+ #
+ module Orchestra
+ @stacked_events = Hash.new { |h,k| h[k] = [] }
+ @listeners = []
+
+ def self.instrument(name, payload=nil)
+ stack = @stacked_events[Thread.current.object_id]
+ event = Event.new(name, stack.last, payload)
+ stack << event
+ event.result = yield
+ event
+ ensure
+ event.finish!
+ stack.delete(event)
+ @listeners.each { |s| s.push(event) }
+ end
+
+ def self.register(listener)
+ @listeners << listener
+ end
+
+ def self.unregister(listener)
+ @listeners.delete(listener)
+ end
+
+ class Event
+ attr_reader :name, :time, :duration, :parent, :thread_id, :payload
+ attr_accessor :result
+
+ def initialize(name, parent=nil, payload=nil)
+ @name = name
+ @time = Time.now
+ @thread_id = Thread.current.object_id
+ @parent = parent
+ @payload = payload
+ end
+
+ def finish!
+ @duration = 1000 * (Time.now.to_f - @time.to_f)
+ end
+ end
+
+ class Listener
+ attr_reader :mutex, :signaler, :thread
+
+ def initialize
+ @mutex, @signaler = Mutex.new, ConditionVariable.new
+ @stream = []
+ @thread = Thread.new do
+ loop do
+ (event = @stream.shift) ? consume(event) : wait
+ end
+ end
+ end
+
+ def wait
+ @mutex.synchronize do
+ @signaler.wait(@mutex)
+ end
+ end
+
+ def push(event)
+ @mutex.synchronize do
+ @stream.push(event)
+ @signaler.broadcast
+ end
+ end
+
+ def consume(event)
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/activesupport/test/new_callback_inheritance_test.rb b/activesupport/test/new_callback_inheritance_test.rb
index da0c19eaea..fe316ae3da 100644
--- a/activesupport/test/new_callback_inheritance_test.rb
+++ b/activesupport/test/new_callback_inheritance_test.rb
@@ -112,4 +112,4 @@ class InheritedCallbacksTest2 < Test::Unit::TestCase
def test_crazy_mix_off
assert_equal %w(before1 before2 update after2 after1), @update2.log
end
-end \ No newline at end of file
+end
diff --git a/activesupport/test/orchestra_test.rb b/activesupport/test/orchestra_test.rb
new file mode 100644
index 0000000000..683cc36f6a
--- /dev/null
+++ b/activesupport/test/orchestra_test.rb
@@ -0,0 +1,161 @@
+require 'abstract_unit'
+
+class OrchestraEventTest < Test::Unit::TestCase
+ def setup
+ @parent = ActiveSupport::Orchestra::Event.new(:parent)
+ end
+
+ def test_initialization_with_name_and_parent_and_payload
+ event = ActiveSupport::Orchestra::Event.new(:awesome, @parent, :payload => "orchestra")
+ assert_equal(:awesome, event.name)
+ assert_equal(@parent, event.parent)
+ assert_equal({ :payload => "orchestra" }, event.payload)
+ end
+
+ def test_thread_id_is_set_on_initialization
+ event = ActiveSupport::Orchestra::Event.new(:awesome)
+ assert_equal Thread.current.object_id, event.thread_id
+ end
+
+ def test_current_time_is_set_on_initialization
+ previous_time = Time.now.utc
+ event = ActiveSupport::Orchestra::Event.new(:awesome)
+ assert_kind_of Time, event.time
+ assert event.time.to_f >= previous_time.to_f
+ end
+
+ def test_duration_is_set_when_event_finishes
+ event = ActiveSupport::Orchestra::Event.new(:awesome)
+ sleep(0.1)
+ event.finish!
+ assert_in_delta 100, event.duration, 30
+ end
+end
+
+class OrchestraMainTest < Test::Unit::TestCase
+ def setup
+ @listener = []
+ ActiveSupport::Orchestra.register @listener
+ end
+
+ def teardown
+ ActiveSupport::Orchestra.unregister @listener
+ end
+
+ def test_orchestra_allows_any_action_to_be_instrumented
+ event = ActiveSupport::Orchestra.instrument(:awesome, "orchestra") do
+ sleep(0.1)
+ end
+
+ assert_equal :awesome, event.name
+ assert_equal "orchestra", event.payload
+ assert_in_delta 100, event.duration, 30
+ end
+
+ def test_block_result_is_stored
+ event = ActiveSupport::Orchestra.instrument(:awesome, "orchestra") do
+ 1 + 1
+ end
+
+ assert_equal 2, event.result
+ end
+
+ def test_events_are_published_to_a_listener
+ event = ActiveSupport::Orchestra.instrument(:awesome, "orchestra") do
+ 1 + 1
+ end
+
+ assert_equal 1, @listener.size
+ assert_equal :awesome, @listener.last.name
+ assert_equal "orchestra", @listener.last.payload
+ end
+
+ def test_nested_events_can_be_instrumented
+ ActiveSupport::Orchestra.instrument(:awesome, "orchestra") do
+ ActiveSupport::Orchestra.instrument(:wot, "child") do
+ sleep(0.1)
+ end
+
+ assert_equal 1, @listener.size
+ assert_equal :wot, @listener.first.name
+ assert_equal "child", @listener.first.payload
+
+ assert_nil @listener.first.parent.duration
+ assert_in_delta 100, @listener.first.duration, 30
+ end
+
+ assert_equal 2, @listener.size
+ assert_equal :awesome, @listener.last.name
+ assert_equal "orchestra", @listener.last.payload
+ assert_in_delta 100, @listener.first.parent.duration, 30
+ end
+
+ def test_event_is_pushed_even_if_block_fails
+ ActiveSupport::Orchestra.instrument(:awesome, "orchestra") do
+ raise "OMG"
+ end rescue RuntimeError
+
+ assert_equal 1, @listener.size
+ assert_equal :awesome, @listener.last.name
+ assert_equal "orchestra", @listener.last.payload
+ end
+end
+
+class OrchestraListenerTest < Test::Unit::TestCase
+ class MyListener < ActiveSupport::Orchestra::Listener
+ attr_reader :consumed
+
+ def consume(event)
+ @consumed ||= []
+ @consumed << event
+ end
+ end
+
+ def setup
+ @listener = MyListener.new
+ ActiveSupport::Orchestra.register @listener
+ end
+
+ def teardown
+ ActiveSupport::Orchestra.unregister @listener
+ end
+
+ def test_thread_is_exposed_by_listener
+ assert_kind_of Thread, @listener.thread
+ end
+
+ def test_event_is_consumed_when_an_action_is_instrumented
+ ActiveSupport::Orchestra.instrument(:sum) do
+ 1 + 1
+ end
+ sleep 0.1
+ assert_equal 1, @listener.consumed.size
+ assert_equal :sum, @listener.consumed.first.name
+ assert_equal 2, @listener.consumed.first.result
+ end
+
+ def test_with_sevaral_consumers_and_several_events
+ @another = MyListener.new
+ ActiveSupport::Orchestra.register @another
+
+ 1.upto(100) do |i|
+ ActiveSupport::Orchestra.instrument(:value) do
+ i
+ end
+ end
+
+ sleep 0.1
+
+ assert_equal 100, @listener.consumed.size
+ assert_equal :value, @listener.consumed.first.name
+ assert_equal 1, @listener.consumed.first.result
+ assert_equal 100, @listener.consumed.last.result
+
+ assert_equal 100, @another.consumed.size
+ assert_equal :value, @another.consumed.first.name
+ assert_equal 1, @another.consumed.first.result
+ assert_equal 100, @another.consumed.last.result
+ ensure
+ ActiveSupport::Orchestra.unregister @another
+ end
+end