view_component: GitHub Actions tests sometimes fail with the error "undefined local variable or method `call'" in a view component

Hello,

I’m using view_component in Avo quite a lot (about 95 components). Sometimes the tests fail when being run in GitHub Actions. The error is ActionView::Template::Error: undefined local variable or method 'call' for #<Avo::PanelComponent:0x000055865888ba10>. This error is never raised in my local environment. Most times I hit “Re-run jobs” and the tests pass.

It seems it only happens for PanelComponent.

Steps to reproduce

I can’t reproduce it locally. It only happened on GA and sporadically.

Expected behavior

The tests should pass without raising this error.

Actual behavior

Raises this error sporadically. It’s not raised every time.

System configuration

Rails version: Tested with 6.0 and 6.1

Ruby version: Tested with 2.6 and 2.7

Gem version: view_component (2.28.0)

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 1
  • Comments: 44 (26 by maintainers)

Commits related to this issue

Most upvoted comments

In some CI runs on my PR, before I’ve finalised the code, I’ve managed to obtain the undefined method call error.

After adding the Mutex, I’ve solved that error but I started getting a deadlock error. After replacing the mutex with a monitor, the error was fixed.

I’ve also added a new test, which concurrently renders a component 100 times starting from a cleared cache. Before adding my code, that test was failing, now it is passing.

I have a sample app where I can reproduce this on a production env here

The issue comes from view_component_storybook, and our configuration where we set app/components as the storybook_path. We set it like this: Rails.application.config.view_component_storybook.stories_path = Rails.root.join("app/components") which generates a pathname object: #<Pathname:/#{path_to_app}/cometeer/app/components>.

However, Rails adds app/components to eager and auto load as a string and not a Pathname object. (I’m assuming this functionality is coming from Rails, anyway).

So the order of operations is:

  • Rails adds app/components to eager and autoload.
  • Zeitwerk has no problems with this
  • view_component_storybook adds Pathname app/components to autoload
  • Pathname app/components is not the same as string app/components, and so in the setup_autoloaders method of ActiveSupport, app/components is not recognized as eager_loaded, and is specifically removed from eager_loading
  • The app/components that rails added to eager load is no longer set to eager load
  • No app/components get loaded past this point ¯_(ツ)_/¯

For us, the solution is simple: add a to_s to Rails.root.join("app/components"). I’ll open an issue on view_component_storybooks too, though.

@seand7565 , does this fix the issue on production too? We are not getting any error on test, only in production.

I think there are two distinct issues here - one in production and one in specs. The issue in specs was fixed in a recent version release. My post above addresses the bug in production.

Are your stories in app/components? If so, my fix above should work!

@seand7565 , Yes, actually… We do not get any issue on specs, but sometimes on production. So i’m going to try this fix… We are hosted on Heroku, and spend sometime researching but cant reproduce on development or local.

That did the trick! Thank you indeed.

(I think ripping it all out was completely genius 🚀)

This should be fixed in v2.74.1. Please reopen if not ❤️

This (ugly) workaround fixed the issue in production for us:

module ViewComponentRetryRenderIn
  def render_in(...)
    super
  rescue NameError => e
    if e.to_s.start_with? %{undefined local variable or method `call}
      Sentry.capture_exception(e, extra: { handled: true, retrying: true })
      sleep 0.01
      render_template_for(@variant).to_s + _output_postamble
    end
  end

  ViewComponent::Base.prepend self
end

Seems plausible at this point that two (or more) threads are trying to compile at the same time, and the sleep helps passing control to the original thread so that it’s able to finish.

The sequence could be something like:

  • thread1: compiled? is false, starts compilation
  • thread2: compiled? is false, starts compilation (while thread1 is still compiling)
  • thread1: finishes compilation, compiled? is now true and starts rendering components
  • thread2: during compilation undefines call in order to redefine it
  • thread1: while rendering bumps into NameError: undefined method call
  • thread2: then finishes compilation and marks (again) compiled? as true

@adrianthedev you know if the failing specs you’re getting are using puma with multiple threads?