Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion actionview/lib/action_view/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ class Base
cattr_accessor :automatically_disable_submit_tag, default: true

# Annotate rendered view with file names
cattr_accessor :annotate_rendered_view_with_filenames, default: false
class_attribute :annotate_rendered_view_with_filenames, default: false

class_attribute :_routes
class_attribute :logger
Expand Down
2 changes: 1 addition & 1 deletion actionview/lib/action_view/template/handlers/erb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class ERB # :nodoc:
# Strip trailing newlines from rendered output
class_attribute :strip_trailing_newlines, default: false

ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*")
ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*").freeze

LocationParsingError = Class.new(StandardError) # :nodoc:

Expand Down
30 changes: 25 additions & 5 deletions actionview/lib/action_view/unbound_template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,29 @@ def initialize(source, identifier, details:, virtual_path:)
end

def bind_locals(locals)
@strict_locals_template || @templates[locals] || build_bound_template(locals)
if @strict_locals_template
@strict_locals_template
elsif frozen?
locals = normalize_locals(locals)
ractor_local_templates.compute_if_absent(locals) { build_template(locals) }
else
@templates[locals] || build_bound_template(locals)
end
end

def built_templates # :nodoc:
@strict_locals_template ? [@strict_locals_template] : @templates.values
if @strict_locals_template
[@strict_locals_template]
elsif @templates
@templates.values
else
templates = ractor_local_store[self]
templates ? templates.values : []
end
end

def freeze # :nodoc:
unless bind_locals([]).strict_locals?
raise ArgumentError, "Cannot freeze #{@virtual_path.inspect}: templates must declare strict locals (e.g. `<%# locals: () %>`) to be frozen."
end
bind_locals([])
@source.freeze
@identifier.freeze
@virtual_path.freeze
Expand All @@ -41,6 +53,14 @@ def freeze # :nodoc:
end

private
def ractor_local_templates
ractor_local_store.compute_if_absent(self) { Concurrent::Map.new }
end

def ractor_local_store
ActiveSupport::Ractors.store_if_absent(:action_view_bound_templates) { Concurrent::Map.new }
end

def build_bound_template(locals)
@write_lock.synchronize do
return @strict_locals_template if @strict_locals_template
Expand Down
71 changes: 60 additions & 11 deletions actionview/test/template/file_system_resolver_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,24 +79,31 @@ def test_freeze_after_eager_load_makes_resolver_shareable
assert_predicate templates[0], :frozen?
end

def test_freeze_raises_for_non_strict_partial
def test_freeze_keeps_non_strict_templates_renderable
with_file "test/_card.html.erb", "<%= post %>"
resolver = ActionView::FileSystemResolver.new(tmpdir)
resolver.eager_load_templates
resolver.eager_load_templates(compile_view)
resolver.freeze

error = assert_raises(ArgumentError) { resolver.freeze }
assert_match "test/_card", error.message
assert_match "strict locals", error.message
assert_ractor_shareable resolver

template = find_all(resolver, "card", "test", true, [:post])[0]
assert_not_predicate template, :frozen?
assert_equal "hello", template.render(compile_view, { post: "hello" })
end

def test_freeze_raises_for_non_strict_template
with_file "test/hello_world.html.erb", "no locals here"
def test_frozen_non_strict_templates_are_cached_per_locals
with_file "test/_card.html.erb", "<%= post %>"
resolver = ActionView::FileSystemResolver.new(tmpdir)
resolver.eager_load_templates
resolver.eager_load_templates(compile_view)
resolver.freeze

error = assert_raises(ArgumentError) { resolver.freeze }
assert_match "test/hello_world", error.message
assert_match "strict locals", error.message
a = find_all(resolver, "card", "test", true, [:post])[0]
b = find_all(resolver, "card", "test", true, [:post])[0]
c = find_all(resolver, "card", "test", true, [:post, :other])[0]

assert_same a, b
assert_not_same a, c
end

def test_frozen_resolver_returns_empty_for_missing_template
Expand All @@ -108,3 +115,45 @@ def test_frozen_resolver_returns_empty_for_missing_template
assert_empty find_all(resolver, "nonexistent")
end
end

class FileSystemResolverRactorTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
include ActiveSupport::Testing::RactorsAssertions

# Compiling methods into FakeView from another Ractor might stop working with
# https://bugs.ruby-lang.org/issues/22226 but it is how non-strict templates could be
# compiled, with the view class container built in the main Ractor and other Ractors
# compiling methods into it.
class FakeView
def initialize(output_buffer)
@output_buffer = output_buffer
end
end

test "non-strict templates compile inside a non-main Ractor" do
Dir.mktmpdir do |dir|
Dir.mkdir(File.join(dir, "test"))
File.write(File.join(dir, "test", "_card.html.erb"), "<%= post %>")

Mime.eager_load!
ActionView::Template::Handlers::ERB.escape_ignore_list.freeze

resolver = ActionView::FileSystemResolver.new(dir)
resolver.eager_load_templates
resolver.freeze

rendered = on_ractor(resolver) do |resolver|
details = { locale: [:en].freeze, formats: [:html].freeze, variants: [].freeze, handlers: [:erb].freeze }.freeze
template = resolver.find_all("card", "test", true, details, nil, [:post])[0]
# compile! also instruments, which Notifications does not support
# from non-main Ractors yet; compile without instrumentation.
template.send(:compile, FakeView)

buffer = ActionView::OutputBuffer.new
FakeView.new(buffer).send(template.method_name, { post: "hello" }, buffer).to_s
end

assert_equal "hello", rendered
end
end
end
1 change: 1 addition & 0 deletions railties/lib/rails/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@ def ractorize! # :nodoc:
view = ActionView::LookupContext.view_context_class.new(ActionView::LookupContext.new([]), {}, nil)
ActionView::PathRegistry.all_file_system_resolvers.each do |resolver|
resolver.eager_load_templates(view)
resolver.freeze
end
end

Expand Down
8 changes: 6 additions & 2 deletions railties/test/application/ractors_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,12 @@ def teardown

ractorize!

templates = ActionView::PathRegistry.all_file_system_resolvers.flat_map(&:built_templates)
assert_not_empty templates
resolvers = ActionView::PathRegistry.all_file_system_resolvers
assert_not_empty resolvers
resolvers.each do |resolver|
assert_predicate resolver, :frozen?
assert_ractor_shareable resolver
end
end

test "error reporting works after the application is ractorized" do
Expand Down
Loading