Skip to content

Only register the compile cache executor hook without template caching - #2728

Merged
joelhawksley merged 6 commits into
ViewComponent:mainfrom
mhenrixon:register-compile-cache-hook-only-without-template-caching
Oct 19, 2026
Merged

joelhawksley merged 6 commits into
ViewComponent:mainfrom
mhenrixon:register-compile-cache-hook-only-without-template-caching

Conversation

@mhenrixon

@mhenrixon mhenrixon commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

What are you trying to accomplish?

Rails main has an experimental Rails.application.ractorize! that serves requests from non-main Ractors on Ruby 4.0. Once view_component is loaded under it, every request fails, including requests that don't render a component:

Ractor::IsolationError: can not access class variables from non-main Ractors (@@caching from ActionView::Resolver)
  lib/view_component/engine.rb:135:in 'block (2 levels) in <class:Engine>'   # 4.15.0

The executor hook in engine.rb runs before every request and reads ActionView::Base.cache_template_loading, which is ActionView::Resolver's @@caching class variable. In production that flag is true, so the hook does nothing, but it still reads the flag.

What approach did you choose and why?

The template watcher hook (from #2730) is now registered only if template caching is off when Action View loads. Inside ActiveSupport.on_load(:action_view), Rails' own action_view.caching hook has already set the flag, so this sees the app's real setting. The flag is checked once, at boot. Rails' Action View railtie makes the same decision for its own template cache expiry: it registers that hook only when templates aren't cached.

Nothing changes in development and test: the hook is registered and watches templates as before.

I considered recomputing the flag from config.action_view.cache_template_loading and reloading_enabled? in after_initialize, the way Rails' railtie does. I didn't, because it copies Rails' defaulting logic and misses apps that set ActionView::Base.cache_template_loading themselves.

Anything you want to highlight for special attention from reviewers?

  • New isolated tests. test/isolated/template_caching_test.rb uses ActiveSupport::Testing::Isolation, the way railties tests do, to boot a fresh app in a forked process for each test:

    • In development, the executor checks the template watcher on every request.
    • In production, the executor neither reads ActionView::Resolver.caching? nor builds the template watcher.
    • In production, changing a component's template between requests doesn't change the response.

    The two production tests fail if the boot-time check is removed.

    These tests can't share a process with the sandbox, which is already booted in test, so they run in a new rake isolated_test task. That task is excluded from rake test and runs as part of all_tests.

  • Moved test. test_template_changes_are_not_reflected_on_new_request_when_cache_template_loading_is_true turned template caching on after the sandbox had booted, but caching is now decided at boot. It moved to the isolated tests as test_template_changes_are_not_reflected_on_new_request_when_templates_are_cached, which boots a production app with a real component on disk, changes the template between requests, and checks the response doesn't change. The with_template_caching helper had no remaining callers, so it's removed.

  • Scope. This only stops the per-request failure. Rendering a component in a Ractor still fails. The next error is @__vc_compiler (base.rb, a Compiler holding a Mutex), and after that comes CompileCache.cache (compile_cache.rb, a mattr_accessor, so a class variable holding a Set). I'm happy to look at those separately.

  • Rails side. ActionView::Resolver.caching is a cattr_accessor. On Ruby master (4.1), non-main Ractors can read class variables that hold shareable values (see Make I18n locale switching work in non-main Ractors rails/rails#58974), and on 4.0 Rails may move it off a class variable. This change doesn't depend on either.

Tests:

  • bundle exec rake test: 653 runs, 0 failures
  • bundle exec rake isolated_test: 3 runs, 0 failures
  • bundle exec rake engine_test: 5 runs, 0 failures

I also checked it in a Rails main app under ractorize!, with requests running in a non-main Ractor. Before: GET /posts failed with the error above. After: GET /posts -> 200 and GET /stack/plain-helpers -> 200.

Conformance case

test/isolated/template_caching_test.rb: test_executor_does_nothing_when_templates_are_cached

bundle exec rake isolated_test

It boots a production app in a forked process, wraps a request in the executor, and asserts that ActionView::Resolver.caching? is never read and no template watcher is built. It needs no Ractor, so it runs on every Ruby in the matrix. It fails again if anything on the per-request executor path starts reading that class variable while templates are cached.

The executor hook that clears the compile cache before each request did
nothing when Action View caches templates, as in production, but it
still read `ActionView::Base.cache_template_loading` on every request.
That reads `ActionView::Resolver`'s `@@caching` class variable, which
non-main Ractors can't read on Ruby 4.0, so under Rails' experimental
`Rails.application.ractorize!` every request failed once view_component
was loaded.

Decide once, when Action View loads, whether to register the hook. Rails'
Action View railtie likewise registers its view reloader only when
template caching is off. The hook still checks the flag on each run, so
toggling it at runtime, with caching off at boot, works as before.
@mhenrixon mhenrixon moved this to Waiting on maintainers in Ractor-safe Rails Oct 18, 2026
joelhawksley and others added 5 commits October 19, 2026 12:53
Resolve conflicts with ViewComponent#2730's template watcher. Move hook registration
into CompileCache.watch_templates so the cached-templates case is tested
in-process instead of in a subprocess, and drop the runtime caching
check, which only existed to support a test that toggled caching after
boot.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f2e356ff-16cd-4517-8e63-a8600747d3fa
Since template caching is now decided at boot, with_template_caching also
skips the template watcher hook the sandbox registered.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f2e356ff-16cd-4517-8e63-a8600747d3fa
…olation

Boot a fresh development or production app per test in a forked process,
the way railties tests do, instead of an Open3 subprocess or an extracted
method. These tests run in their own rake task since the sandbox is
already booted in the test process.

Inline the executor hook back into the engine, and restore ViewComponent#2730's
integration tests that a previous commit dropped by mistake.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f2e356ff-16cd-4517-8e63-a8600747d3fa
Booting a production app tests template caching as decided at boot, so
the sandbox no longer needs to fake it by skipping the watcher hook.
Remove the now-unused with_template_caching helper.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f2e356ff-16cd-4517-8e63-a8600747d3fa
@joelhawksley
joelhawksley merged commit 5a58870 into ViewComponent:main Oct 19, 2026
27 of 28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants