Skip to content

Commit 1fd89a4

Browse files
Wrap entire compile step with mutex (#2132)
* Wrap entire compile step with mutex Currently we only wrap the redefinition of templates with a mutex. This has largely been fine, but recent changes have highlighted that being overly specific with the mutex still leads to race conditions that are complex to debug or test for. This change wraps the entire compile step with a mutex to help avoid introducing any race conditions during the compile step. I tested this change against the last race condition bug and validated that the demo app I setup locally still runs as expected. * Fix linter, add changelog entry * Make changelog a sentence * fix allocations count --------- Co-authored-by: Joel Hawksley <[email protected]> Co-authored-by: Joel Hawksley <[email protected]>
1 parent 587a9ed commit 1fd89a4

File tree

3 files changed

+35
-29
lines changed

3 files changed

+35
-29
lines changed

docs/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ nav_order: 5
1010

1111
## main
1212

13+
* Wrap entire compile step in a mutex to make it more resilient to race conditions.
14+
15+
*Blake Williams*
16+
1317
* Add [Niva]([niva.co](https://www.niva.co/)) to companies who use `ViewComponent`.
1418

1519
*Daniel Vu Dao*

lib/view_component/compiler.rb

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class Compiler
1212

1313
def initialize(component)
1414
@component = component
15-
@redefinition_lock = Mutex.new
15+
@lock = Mutex.new
1616
@rendered_templates = Set.new
1717
end
1818

@@ -24,30 +24,36 @@ def compile(raise_errors: false, force: false)
2424
return if compiled? && !force
2525
return if @component == ViewComponent::Base
2626

27-
gather_templates
27+
@lock.synchronize do
28+
# this check is duplicated so that concurrent compile calls can still
29+
# early exit
30+
return if compiled? && !force
2831

29-
if self.class.development_mode && @templates.any?(&:requires_compiled_superclass?)
30-
@component.superclass.compile(raise_errors: raise_errors)
31-
end
32+
gather_templates
3233

33-
if template_errors.present?
34-
raise TemplateError.new(template_errors) if raise_errors
34+
if self.class.development_mode && @templates.any?(&:requires_compiled_superclass?)
35+
@component.superclass.compile(raise_errors: raise_errors)
36+
end
3537

36-
# this return is load bearing, and prevents the component from being considered "compiled?"
37-
return false
38-
end
38+
if template_errors.present?
39+
raise TemplateError.new(template_errors) if raise_errors
3940

40-
if raise_errors
41-
@component.validate_initialization_parameters!
42-
@component.validate_collection_parameter!
43-
end
41+
# this return is load bearing, and prevents the component from being considered "compiled?"
42+
return false
43+
end
4444

45-
define_render_template_for
45+
if raise_errors
46+
@component.validate_initialization_parameters!
47+
@component.validate_collection_parameter!
48+
end
49+
50+
define_render_template_for
4651

47-
@component.register_default_slots
48-
@component.build_i18n_backend
52+
@component.register_default_slots
53+
@component.build_i18n_backend
4954

50-
CompileCache.register(@component)
55+
CompileCache.register(@component)
56+
end
5157
end
5258

5359
def renders_template_for?(variant, format)
@@ -60,9 +66,7 @@ def renders_template_for?(variant, format)
6066

6167
def define_render_template_for
6268
@templates.each do |template|
63-
@redefinition_lock.synchronize do
64-
template.compile_to_component
65-
end
69+
template.compile_to_component
6670
end
6771

6872
method_body =
@@ -93,14 +97,12 @@ def define_render_template_for
9397
out << "else\n #{templates.find { _1.variant.nil? && _1.default_format? }.safe_method_name}\nend"
9498
end
9599

96-
@redefinition_lock.synchronize do
97-
@component.silence_redefinition_of_method(:render_template_for)
98-
@component.class_eval <<-RUBY, __FILE__, __LINE__ + 1
99-
def render_template_for(variant = nil, format = nil)
100-
#{method_body}
101-
end
102-
RUBY
100+
@component.silence_redefinition_of_method(:render_template_for)
101+
@component.class_eval <<-RUBY, __FILE__, __LINE__ + 1
102+
def render_template_for(variant = nil, format = nil)
103+
#{method_body}
103104
end
105+
RUBY
104106
end
105107

106108
def template_errors

test/sandbox/test/rendering_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def test_render_inline_allocations
1515
ViewComponent::CompileCache.cache.delete(MyComponent)
1616
MyComponent.ensure_compiled
1717

18-
assert_allocations("3.4.0" => 107, "3.3.5" => 116, "3.3.0" => 129, "3.2.5" => 115, "3.1.6" => 115, "3.0.7" => 125) do
18+
assert_allocations("3.4.0" => 110, "3.3.5" => 116, "3.3.0" => 129, "3.2.5" => 115, "3.1.6" => 115, "3.0.7" => 125) do
1919
render_inline(MyComponent.new)
2020
end
2121

0 commit comments

Comments
 (0)