diff options
-rw-r--r-- | actionpack/lib/action_dispatch/journey/route.rb | 2 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/inspector.rb | 2 | ||||
-rw-r--r-- | actionview/lib/action_view/digestor.rb | 147 | ||||
-rw-r--r-- | actionview/lib/action_view/lookup_context.rb | 2 | ||||
-rw-r--r-- | actionview/lib/action_view/tasks/dependencies.rake | 4 | ||||
-rw-r--r-- | actionview/test/template/digestor_test.rb | 14 | ||||
-rw-r--r-- | activerecord/CHANGELOG.md | 7 | ||||
-rw-r--r-- | activerecord/lib/active_record/suppressor.rb | 6 | ||||
-rw-r--r-- | activerecord/test/cases/suppressor_test.rb | 13 | ||||
-rw-r--r-- | activerecord/test/models/notification.rb | 1 | ||||
-rw-r--r-- | guides/source/api_app.md | 17 | ||||
-rw-r--r-- | guides/source/caching_with_rails.md | 2 |
12 files changed, 127 insertions, 90 deletions
diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb index fee08fc3db..cfd6681dd1 100644 --- a/actionpack/lib/action_dispatch/journey/route.rb +++ b/actionpack/lib/action_dispatch/journey/route.rb @@ -82,7 +82,7 @@ module ActionDispatch end def requirements # :nodoc: - # needed for rails `rake routes` + # needed for rails `rails routes` @defaults.merge(path.requirements).delete_if { |_,v| /.+?/ == v } diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index 6f651a5689..5d30a545a2 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -51,7 +51,7 @@ module ActionDispatch ## # This class is just used for displaying route information when someone - # executes `rake routes` or looks at the RoutingError page. + # executes `rails routes` or looks at the RoutingError page. # People should not use this class. class RoutesInspector # :nodoc: def initialize(routes) diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index f3c5d6c8df..b99d1af998 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -4,7 +4,7 @@ require 'monitor' module ActionView class Digestor - @@digest_monitor = Monitor.new + @@digest_mutex = Mutex.new class PerRequestDigestCacheExpiry < Struct.new(:app) # :nodoc: def call(env) @@ -20,111 +20,104 @@ module ActionView # * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt> # * <tt>dependencies</tt> - An array of dependent views # * <tt>partial</tt> - Specifies whether the template is a partial - def digest(name:, finder:, **options) - options.assert_valid_keys(:dependencies, :partial) - - cache_key = ([ name ].compact + Array.wrap(options[:dependencies])).join('.') + def digest(name:, finder:, dependencies: []) + dependencies ||= [] + cache_key = ([ name ].compact + dependencies).join('.') # this is a correctly done double-checked locking idiom # (Concurrent::Map's lookups have volatile semantics) - finder.digest_cache[cache_key] || @@digest_monitor.synchronize do + finder.digest_cache[cache_key] || @@digest_mutex.synchronize do finder.digest_cache.fetch(cache_key) do # re-check under lock - compute_and_store_digest(cache_key, name, finder, options) + partial = name.include?("/_") + root = tree(name, finder, partial) + dependencies.each do |injected_dep| + root.children << Injected.new(injected_dep, nil, nil) + end + finder.digest_cache[cache_key] = root.digest(finder) end end end - private - def compute_and_store_digest(cache_key, name, finder, options) # called under @@digest_monitor lock - klass = if options[:partial] || name.include?("/_") - # Prevent re-entry or else recursive templates will blow the stack. - # There is no need to worry about other threads seeing the +false+ value, - # as they will then have to wait for this thread to let go of the @@digest_monitor lock. - pre_stored = finder.digest_cache.put_if_absent(cache_key, false).nil? # put_if_absent returns nil on insertion - PartialDigestor - else - Digestor - end - - finder.digest_cache[cache_key] = stored_digest = klass.new(name, finder, options).digest - ensure - # something went wrong or ActionView::Resolver.caching? is false, make sure not to corrupt the @@cache - finder.digest_cache.delete_pair(cache_key, false) if pre_stored && !stored_digest - end - end - - attr_reader :name, :finder, :options + def logger + ActionView::Base.logger || NullLogger + end - def initialize(name, finder, options = {}) - @name, @finder = name, finder - @options = options - end + # Create a dependency tree for template named +name+. + def tree(name, finder, partial = false, seen = {}) + logical_name = name.gsub(%r|/_|, "/") - def digest - Digest::MD5.hexdigest("#{source}-#{dependency_digest}").tap do |digest| - logger.debug " Cache digest for #{template.inspect}: #{digest}" - end - rescue ActionView::MissingTemplate - logger.error " Couldn't find template for digesting: #{name}" - '' - end + if finder.disable_cache { finder.exists?(logical_name, [], partial) } + template = finder.disable_cache { finder.find(logical_name, [], partial) } - def dependencies - DependencyTracker.find_dependencies(name, template, finder.view_paths) - rescue ActionView::MissingTemplate - logger.error " '#{name}' file doesn't exist, so no dependencies" - [] - end + if node = seen[template.identifier] # handle cycles in the tree + node + else + node = seen[template.identifier] = Node.create(name, logical_name, template, partial) - def nested_dependencies - dependencies.collect do |dependency| - dependencies = PartialDigestor.new(dependency, finder).nested_dependencies - dependencies.any? ? { dependency => dependencies } : dependency + deps = DependencyTracker.find_dependencies(name, template, finder.view_paths) + deps.uniq { |n| n.gsub(%r|/_|, "/") }.each do |dep_file| + node.children << tree(dep_file, finder, true, seen) + end + node + end + else + logger.error " '#{name}' file doesn't exist, so no dependencies" + logger.error " Couldn't find template for digesting: #{name}" + seen[name] ||= Missing.new(name, logical_name, nil) + end end end - private - class NullLogger - def self.debug(_); end - def self.error(_); end - end + class Node + attr_reader :name, :logical_name, :template, :children - def logger - ActionView::Base.logger || NullLogger + def self.create(name, logical_name, template, partial) + klass = partial ? Partial : Node + klass.new(name, logical_name, template, []) end - def logical_name - name.gsub(%r|/_|, "/") + def initialize(name, logical_name, template, children = []) + @name = name + @logical_name = logical_name + @template = template + @children = children end - def partial? - false + def digest(finder, stack = []) + Digest::MD5.hexdigest("#{template.source}-#{dependency_digest(finder, stack)}") end - def template - @template ||= finder.disable_cache { finder.find(logical_name, [], partial?) } + def dependency_digest(finder, stack) + children.map do |node| + if stack.include?(node) + false + else + finder.digest_cache[node.name] ||= begin + stack.push node + node.digest(finder, stack).tap { stack.pop } + end + end + end.join("-") end - def source - template.source + def to_dep_map + children.any? ? { name => children.map(&:to_dep_map) } : name end + end - def dependency_digest - template_digests = dependencies.collect do |template_name| - Digestor.digest(name: template_name, finder: finder, partial: true) - end + class Partial < Node; end - (template_digests + injected_dependencies).join("-") - end + class Missing < Node + def digest(finder, _ = []) '' end + end - def injected_dependencies - Array.wrap(options[:dependencies]) - end - end + class Injected < Node + def digest(finder, _ = []) name end + end - class PartialDigestor < Digestor # :nodoc: - def partial? - true + class NullLogger + def self.debug(_); end + def self.error(_); end end end end diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index 86afedaa2d..4163e69a72 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -70,8 +70,6 @@ module ActionView @details_keys.clear end - def self.empty?; @details_keys.empty?; end - def self.digest_caches @details_keys.values.map(&:digest_cache) end diff --git a/actionview/lib/action_view/tasks/dependencies.rake b/actionview/lib/action_view/tasks/dependencies.rake index 9932ff8b6d..045bdf5691 100644 --- a/actionview/lib/action_view/tasks/dependencies.rake +++ b/actionview/lib/action_view/tasks/dependencies.rake @@ -2,13 +2,13 @@ namespace :cache_digests do desc 'Lookup nested dependencies for TEMPLATE (like messages/show or comments/_comment.html)' task :nested_dependencies => :environment do abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present? - puts JSON.pretty_generate ActionView::Digestor.new(CacheDigests.template_name, CacheDigests.finder).nested_dependencies + puts JSON.pretty_generate ActionView::Digestor.tree(CacheDigests.template_name, CacheDigests.finder).children.map(&:to_dep_map) end desc 'Lookup first-level dependencies for TEMPLATE (like messages/show or comments/_comment.html)' task :dependencies => :environment do abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present? - puts JSON.pretty_generate ActionView::Digestor.new(CacheDigests.template_name, CacheDigests.finder).dependencies + puts JSON.pretty_generate ActionView::Digestor.tree(CacheDigests.template_name, CacheDigests.finder).children.map(&:name) end class CacheDigests diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb index a9a6de250c..d4c5048bde 100644 --- a/actionview/test/template/digestor_test.rb +++ b/actionview/test/template/digestor_test.rb @@ -146,6 +146,11 @@ class TemplateDigestorTest < ActionView::TestCase end end + def test_nested_template_deps + nested_deps = ["messages/header", {"comments/comments"=>["comments/comment"]}, "messages/actions/move", "events/event", "messages/something_missing", "messages/something_missing_1", "messages/message", "messages/form"] + assert_equal nested_deps, nested_dependencies("messages/show") + end + def test_recursion_in_renders assert digest("level/recursion") # assert recursion is possible assert_not_nil digest("level/recursion") # assert digest is stored @@ -313,16 +318,17 @@ class TemplateDigestorTest < ActionView::TestCase options = options.dup finder.variants = options.delete(:variants) || [] - - ActionView::Digestor.digest({ name: template_name, finder: finder }.merge(options)) + ActionView::Digestor.digest(name: template_name, finder: finder, dependencies: (options[:dependencies] || [])) end def dependencies(template_name) - ActionView::Digestor.new(template_name, finder).dependencies + tree = ActionView::Digestor.tree(template_name, finder) + tree.children.map(&:name) end def nested_dependencies(template_name) - ActionView::Digestor.new(template_name, finder).nested_dependencies + tree = ActionView::Digestor.tree(template_name, finder) + tree.children.map(&:to_dep_map) end def finder diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index c1a8803457..39fe217777 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,10 @@ +* Ensure that the Suppressor runs before validations. + + This moves the suppressor up to be run before validations rather than after + validations. There's no reason to validate a record you aren't planning on saving. + + *Eileen M. Uchitelle* + ## Rails 5.0.0.beta3 (February 24, 2016) ## * Ensure that mutations of the array returned from `ActiveRecord::Relation#to_a` diff --git a/activerecord/lib/active_record/suppressor.rb b/activerecord/lib/active_record/suppressor.rb index b3644bf569..8ec4b48d31 100644 --- a/activerecord/lib/active_record/suppressor.rb +++ b/activerecord/lib/active_record/suppressor.rb @@ -37,7 +37,11 @@ module ActiveRecord end end - def create_or_update(*args) # :nodoc: + def save(*) # :nodoc: + SuppressorRegistry.suppressed[self.class.name] ? true : super + end + + def save!(*) # :nodoc: SuppressorRegistry.suppressed[self.class.name] ? true : super end end diff --git a/activerecord/test/cases/suppressor_test.rb b/activerecord/test/cases/suppressor_test.rb index 72c5c16555..7d44e36419 100644 --- a/activerecord/test/cases/suppressor_test.rb +++ b/activerecord/test/cases/suppressor_test.rb @@ -46,7 +46,18 @@ class SuppressorTest < ActiveRecord::TestCase Notification.suppress { UserWithNotification.create! } assert_difference -> { Notification.count } do - Notification.create! + Notification.create!(message: "New Comment") + end + end + + def test_suppresses_validations_on_create + assert_no_difference -> { Notification.count } do + Notification.suppress do + User.create + User.create! + User.new.save + User.new.save! + end end end end diff --git a/activerecord/test/models/notification.rb b/activerecord/test/models/notification.rb index b4b4b8f1b6..82edc64b68 100644 --- a/activerecord/test/models/notification.rb +++ b/activerecord/test/models/notification.rb @@ -1,2 +1,3 @@ class Notification < ActiveRecord::Base + validates_presence_of :message end diff --git a/guides/source/api_app.md b/guides/source/api_app.md index 0598b9c7fa..8dba914923 100644 --- a/guides/source/api_app.md +++ b/guides/source/api_app.md @@ -166,6 +166,23 @@ class definition: config.api_only = true ``` +In `config/environments/development.rb`, set `config.debug_exception_response_format` +to configure the format used in responses when errors occur in development mode. + +To render an HTML page with debugging information, use the value `:default`. + +```ruby +config.debug_exception_response_format = :default +``` + +To render debugging information preserving the response format, use the value `:api`. + +```ruby +config.debug_exception_response_format = :api +``` + +By default, `config.debug_exception_response_format` is set to `:api`. + Finally, inside `app/controllers/application_controller.rb`, instead of: ```ruby diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md index 2df478b5aa..f26019c72e 100644 --- a/guides/source/caching_with_rails.md +++ b/guides/source/caching_with_rails.md @@ -523,7 +523,7 @@ end ### A note on weak ETags -Etags generated by Rails are weak by default. Weak etags allow symantically equivalent responses to have the same etags, even if their bodies do not match exactly. This is useful when we don't want the page regenerated for minor changes in response body. If you absolutely need to generate a strong etag, it can be assigned to the header directly. +Etags generated by Rails are weak by default. Weak etags allow symantically equivalent responses to have the same etags, even if their bodies do not match exactly. This is useful when we don't want the page to be regenerated for minor changes in response body. If you absolutely need to generate a strong etag, it can be assigned to the header directly. ```ruby response.add_header "ETag", Digest::MD5.hexdigest(response.body) |