Skip to content

Commit 9180e61

Browse files
authored
Merge pull request rails#47347 from jhawthorn/view_cache_main_reloader
Use "main" reloader and cache resolvers from {append,prepend}_view_path
2 parents a579f4f + 72abd63 commit 9180e61

File tree

10 files changed

+189
-76
lines changed

10 files changed

+189
-76
lines changed

actionpack/lib/action_dispatch/middleware/exception_wrapper.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -245,11 +245,9 @@ def spot(exc)
245245
def build_backtrace
246246
built_methods = {}
247247

248-
ActionView::ViewPaths.all_view_paths.each do |path_set|
249-
path_set.each do |resolver|
250-
resolver.built_templates.each do |template|
251-
built_methods[template.method_name] = template
252-
end
248+
ActionView::ViewPaths::Registry.all_resolvers.each do |resolver|
249+
resolver.built_templates.each do |template|
250+
built_methods[template.method_name] = template
253251
end
254252
end
255253

actionview/lib/action_view/cache_expiry.rb

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,65 @@
11
# frozen_string_literal: true
22

33
module ActionView
4-
class CacheExpiry
5-
class Executor
6-
def initialize(watcher:)
7-
@execution_lock = Concurrent::ReentrantReadWriteLock.new
8-
@cache_expiry = ViewModificationWatcher.new(watcher: watcher) do
9-
clear_cache
4+
module CacheExpiry # :nodoc: all
5+
class ViewReloader
6+
def initialize(watcher:, &block)
7+
@mutex = Mutex.new
8+
@watcher_class = watcher
9+
@watched_dirs = nil
10+
@watcher = nil
11+
@previous_change = false
12+
13+
rebuild_watcher
14+
15+
_self = self
16+
ActionView::ViewPaths::Registry.singleton_class.set_callback(:build_file_system_resolver, :after) do
17+
_self.send(:rebuild_watcher)
1018
end
1119
end
1220

13-
def run
14-
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
15-
@cache_expiry.execute_if_updated
16-
@execution_lock.acquire_read_lock
17-
end
21+
def updated?
22+
@previous_change || @watcher.updated?
1823
end
1924

20-
def complete(_)
21-
@execution_lock.release_read_lock
25+
def execute
26+
watcher = nil
27+
@mutex.synchronize do
28+
@previous_change = false
29+
watcher = @watcher
30+
end
31+
watcher.execute
2232
end
2333

2434
private
25-
def clear_cache
26-
@execution_lock.with_write_lock do
27-
ActionView::LookupContext::DetailsKey.clear
28-
end
35+
def reload!
36+
ActionView::LookupContext::DetailsKey.clear
2937
end
30-
end
3138

32-
class ViewModificationWatcher
33-
def initialize(watcher:, &block)
34-
@watched_dirs = nil
35-
@watcher_class = watcher
36-
@watcher = nil
37-
@mutex = Mutex.new
38-
@block = block
39-
end
39+
def rebuild_watcher
40+
@mutex.synchronize do
41+
old_watcher = @watcher
4042

41-
def execute_if_updated
42-
@mutex.synchronize do
43-
watched_dirs = dirs_to_watch
44-
return if watched_dirs.empty?
43+
if @watched_dirs != dirs_to_watch
44+
@watched_dirs = dirs_to_watch
45+
new_watcher = @watcher_class.new([], @watched_dirs) do
46+
reload!
47+
end
48+
@watcher = new_watcher
4549

46-
if watched_dirs != @watched_dirs
47-
@watched_dirs = watched_dirs
48-
@watcher = @watcher_class.new([], watched_dirs, &@block)
49-
@watcher.execute
50-
else
51-
@watcher.execute_if_updated
50+
# We must check the old watcher after initializing the new one to
51+
# ensure we don't miss any events
52+
@previous_change ||= old_watcher&.updated?
53+
end
5254
end
5355
end
54-
end
5556

56-
private
5757
def dirs_to_watch
58-
all_view_paths.grep(FileSystemResolver).map!(&:path).tap(&:uniq!).sort!
58+
all_view_paths.uniq.sort
5959
end
6060

6161
def all_view_paths
62-
ActionView::ViewPaths.all_view_paths.flat_map(&:paths)
62+
ActionView::ViewPaths::Registry.all_file_system_resolvers.map(&:path)
6363
end
6464
end
6565
end

actionview/lib/action_view/lookup_context.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ def self.details_cache_key(details)
7171
end
7272

7373
def self.clear
74-
ActionView::ViewPaths.all_view_paths.each do |path_set|
75-
path_set.each(&:clear_cache)
74+
ActionView::ViewPaths::Registry.all_resolvers.each do |resolver|
75+
resolver.clear_cache
7676
end
7777
@view_context_class = nil
7878
@details_keys.clear

actionview/lib/action_view/path_set.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def typecast(paths)
6767
paths.map do |path|
6868
case path
6969
when Pathname, String
70-
FileSystemResolver.new path.to_s
70+
ViewPaths::Registry.file_system_resolver(path.to_s)
7171
when Resolver
7272
path
7373
else

actionview/lib/action_view/railtie.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,14 @@ class Railtie < Rails::Engine # :nodoc:
107107
end
108108

109109
unless enable_caching
110-
app.executor.register_hook ActionView::CacheExpiry::Executor.new(watcher: app.config.file_watcher)
110+
view_reloader = ActionView::CacheExpiry::ViewReloader.new(watcher: app.config.file_watcher)
111+
112+
app.reloaders << view_reloader
113+
view_reloader.execute
114+
app.reloader.to_run do
115+
require_unload_lock!
116+
view_reloader.execute
117+
end
111118
end
112119
end
113120

actionview/lib/action_view/view_paths.rb

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,19 @@ module ViewPaths
55
extend ActiveSupport::Concern
66

77
included do
8-
ViewPaths.set_view_paths(self, ActionView::PathSet.new.freeze)
8+
ViewPaths::Registry.set_view_paths(self, ActionView::PathSet.new.freeze)
99
end
1010

1111
delegate :template_exists?, :any_templates?, :view_paths, :formats, :formats=,
1212
:locale, :locale=, to: :lookup_context
1313

1414
module ClassMethods
1515
def _view_paths
16-
ViewPaths.get_view_paths(self)
16+
ViewPaths::Registry.get_view_paths(self)
1717
end
1818

1919
def _view_paths=(paths)
20-
ViewPaths.set_view_paths(self, paths)
20+
ViewPaths::Registry.set_view_paths(self, paths)
2121
end
2222

2323
def _prefixes # :nodoc:
@@ -70,21 +70,46 @@ def local_prefixes
7070
end
7171
end
7272

73-
# :stopdoc:
74-
@all_view_paths = {}
73+
module Registry # :nodoc:
74+
@view_paths_by_class = {}
75+
@file_system_resolvers = Concurrent::Map.new
7576

76-
def self.get_view_paths(klass)
77-
@all_view_paths[klass] || get_view_paths(klass.superclass)
78-
end
77+
class << self
78+
include ActiveSupport::Callbacks
79+
define_callbacks :build_file_system_resolver
80+
end
7981

80-
def self.set_view_paths(klass, paths)
81-
@all_view_paths[klass] = paths
82-
end
82+
def self.get_view_paths(klass)
83+
@view_paths_by_class[klass] || get_view_paths(klass.superclass)
84+
end
85+
86+
def self.set_view_paths(klass, paths)
87+
@view_paths_by_class[klass] = paths
88+
end
8389

84-
def self.all_view_paths
85-
@all_view_paths.values.uniq
90+
def self.file_system_resolver(path)
91+
path = File.expand_path(path)
92+
resolver = @file_system_resolvers[path]
93+
unless resolver
94+
run_callbacks(:build_file_system_resolver) do
95+
resolver = @file_system_resolvers.fetch_or_store(path) do
96+
FileSystemResolver.new(path)
97+
end
98+
end
99+
end
100+
resolver
101+
end
102+
103+
def self.all_resolvers
104+
resolvers = [all_file_system_resolvers]
105+
resolvers.concat @view_paths_by_class.values.map(&:to_a)
106+
resolvers.flatten.uniq
107+
end
108+
109+
def self.all_file_system_resolvers
110+
@file_system_resolvers.values
111+
end
86112
end
87-
# :startdoc:
88113

89114
# The prefixes used in render "foo" shortcuts.
90115
def _prefixes # :nodoc:

actionview/test/actionpack/controller/render_test.rb

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1471,35 +1471,40 @@ def test_render_call_to_partial_with_layout_in_main_layout_and_within_content_fo
14711471
assert_equal "Before (Anthony)\nInside from partial (Anthony)\nAfter\nBefore (David)\nInside from partial (David)\nAfter\nBefore (Ramm)\nInside from partial (Ramm)\nAfter", @response.body
14721472
end
14731473

1474-
def test_template_annotations
1474+
def with_annotations_enabled
14751475
ActionView::Base.annotate_rendered_view_with_filenames = true
1476+
ActionView::LookupContext::DetailsKey.clear
1477+
yield
1478+
ensure
1479+
ActionView::Base.annotate_rendered_view_with_filenames = false
1480+
ActionView::LookupContext::DetailsKey.clear
1481+
end
14761482

1477-
get :greeting
1483+
def test_template_annotations
1484+
with_annotations_enabled do
1485+
get :greeting
1486+
end
14781487

14791488
assert_includes @response.body, "<!-- BEGIN"
14801489
assert_includes @response.body, "<!-- END"
14811490
assert_includes @response.body, "test/fixtures/actionpack/test/greeting.html.erb"
14821491
assert_includes @response.body, "This is grand!"
1483-
ensure
1484-
ActionView::Base.annotate_rendered_view_with_filenames = false
14851492
end
14861493

14871494
def test_template_annotations_do_not_render_for_non_html_format
1488-
ActionView::Base.annotate_rendered_view_with_filenames = true
1489-
1490-
get :render_with_explicit_template_with_locals
1495+
with_annotations_enabled do
1496+
get :render_with_explicit_template_with_locals
1497+
end
14911498

14921499
assert_not_includes @response.body, "BEGIN"
14931500
assert_equal @response.body.split("\n").length, 1
1494-
ensure
1495-
ActionView::Base.annotate_rendered_view_with_filenames = false
14961501
end
14971502

14981503
def test_line_offset_with_annotations_enabled
1499-
ActionView::Base.annotate_rendered_view_with_filenames = true
1500-
15011504
exc = assert_raises ActionView::Template::Error do
1502-
get :render_line_offset
1505+
with_annotations_enabled do
1506+
get :render_line_offset
1507+
end
15031508
end
15041509
line = exc.backtrace.first
15051510
assert(line =~ %r{:(\d+):})

actionview/test/actionpack/controller/view_paths_test.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,28 @@ def test_template_prepends_view_path_correctly
102102
assert_paths TestController, *class_view_paths
103103
end
104104

105+
def test_append_view_path_does_not_bust_template_cache
106+
template_instances = 2.times.map do
107+
controller = Test::SubController.new
108+
controller.append_view_path "#{FIXTURE_LOAD_PATH}/override2"
109+
controller.lookup_context.find_all("layouts/test/sub")
110+
end
111+
112+
object_ids = template_instances.flatten.map(&:object_id)
113+
assert_equal 1, object_ids.uniq.count
114+
end
115+
116+
def test_prepend_view_path_does_not_bust_template_cache
117+
template_instances = 2.times.map do
118+
controller = TestController.new
119+
controller.prepend_view_path "#{FIXTURE_LOAD_PATH}/override"
120+
controller.lookup_context.find_all("hello_world", "test")
121+
end
122+
123+
object_ids = template_instances.flatten.map(&:object_id)
124+
assert_equal 1, object_ids.uniq.count
125+
end
126+
105127
def test_view_paths
106128
get :hello_world
107129
assert_response :success

railties/test/application/per_request_digest_cache_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def index
6262
end
6363

6464
test "template digests are cleared before a request" do
65-
assert_called(ActionView::LookupContext::DetailsKey, :clear, times: 3) do
65+
assert_called(ActionView::LookupContext::DetailsKey, :clear, times: 2) do
6666
get "/customers"
6767
assert_equal 200, last_response.status
6868
end
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# frozen_string_literal: true
2+
3+
require "isolation/abstract_unit"
4+
require "rack/test"
5+
6+
module ApplicationTests
7+
class ViewReloadingTest < ActiveSupport::TestCase
8+
include ActiveSupport::Testing::Isolation
9+
include Rack::Test::Methods
10+
11+
def setup
12+
build_app
13+
14+
app_file "config/routes.rb", <<-RUBY
15+
Rails.application.routes.draw do
16+
get 'pages/:id', to: 'pages#show'
17+
end
18+
RUBY
19+
20+
app_file "app/controllers/pages_controller.rb", <<-RUBY
21+
class PagesController < ApplicationController
22+
layout false
23+
24+
def show
25+
end
26+
end
27+
RUBY
28+
end
29+
30+
def teardown
31+
teardown_app
32+
end
33+
34+
test "views are reloaded" do
35+
app_file "app/views/pages/show.html.erb", <<-RUBY
36+
Before!
37+
RUBY
38+
39+
ENV["RAILS_ENV"] = "development"
40+
require "#{app_path}/config/environment"
41+
42+
get "/pages/foo"
43+
get "/pages/foo"
44+
assert_equal 200, last_response.status, last_response.body
45+
assert_equal "Before!", last_response.body.strip
46+
47+
app_file "app/views/pages/show.html.erb", <<-RUBY
48+
After!
49+
RUBY
50+
51+
get "/pages/foo"
52+
assert_equal 200, last_response.status
53+
assert_equal "After!", last_response.body.strip
54+
end
55+
end
56+
end

0 commit comments

Comments
 (0)