zoominfo

9 Tips & Tricks to Enable Zeitwerk Autoloader in your Rails App

by Mandeep Khinda | March 18, 2021

Co-authored by Sanket Patel

Introduction

Do you have a Rails 5 (or older) app that uses Sidekiq and want to upgrade to the latest and greatest version of both? If you do, then stick around for some helpful tips and tricks we discovered while going through this very scenario that will hopefully help you save time during your own upgrade.

At Rewind we use Sidekiq 5 extensively for our backup and copy apps asynchronous jobs.

We use a Rails Engine mounted in a Rails app that serves up Sidekiq (following the principal of infrastructure as code). The engine contains all of the workers and other business logic. It looks something like this:

workers and other business logic

During our upgrade from Rails 5 to 6 we found that our gems and Rails engine had code that was incompatible with the new default autoloader called Zeitwerk and the application servers would not start. We also learned that Sidekiq 6 requires the zeitwerk autoloader and will not work with classic mode.

Upgrading the major version of a core 3rd party library creates some anxiety (and understandably so- we’re no stranger to data loss horror stories). Adding in a fundamental change to how the app loads and reads Classes too? Ouch.

The team decided to implement a workaround by reverting the autoloader to classic and leaving Sidekiq at v5.

Nevertheless, it was almost a year after the release of Sidekiq 6 and an upgrade was long overdue.

By now Sidekiq 6.1 was released and it included a number of security/bug fixes and enhancements that would improve our service – we needed to figure out how this was going to play out in our platform.

In classic Rewind fashion, we carved out some time and constructed a plan to limit the blast radius of the upgrade.

Here are 9 tips and tricks you should know before getting started on your own upgrade:

1) zeitwerk:check is your friend

The zeitwerk gem is part of Rails 6 and it includes a handy checking tool that will scan your project folder and attempt to eager load just as your application would do on startup. We found 2 ways of using it:

In a Rails app:

bundle exec rake zeitwerk:check

In a Rails engine:

bundle exec rake app:zeitwerk:check

2) Use Inflectors to deal with character case quirks

Zeitwerk likes camelcase and if you have classes with consecutive uppercase characters and don’t want to rename them you can make use of the Zeitwerk::Inflector in a Rails initializer:

# config/initializers/zeitwerk.rb
Rails.autoloaders.each do |autoloader|
 autoloader.inflector.inflect(
   'gdpr_handler' => 'GDPRHandler'
 )
end

Without this, zeitwerk expects your class to be named “GdprHandler”

3) Don’t stack inflector initializers

In our Rails Engine we instantiated inflectors as shown below but found they were overridden when the Rails app that used the engine also instantiated its own inflectors.

# config/initializers/zeitwerk.rb
Rails.autoloaders.each do |autoloader|
 autoloader.inflector = Zeitwerk::Inflector.new
 autoloader.inflector.inflect(
   'graph_ql_query_service' => 'GraphQLQueryService',
   'graph_ql' => 'GraphQL'
 )
end

If you have an engine that uses this technique, then you can work around this situation by configuring the initializer in your Rails app as shown in tip #2 so that it adds to the inflectors and doesn’t replace them.

4) Concerns is a reserved word

We had Rails Concerns in a “Concerns” module namespace below the “app” directory in Rails app. Zeitwerk did not like this. So we had 2 choices: Remove the namespace or rename the module namespace.

We went with renaming the namespace of the concern file from “Concerns::Foo” to “Support::Foo”

Let’s take a bit more look at what this means to get a general understanding:

So take an example:

# file at /app/foo/concerns/something.rb
module Foo::Concerns
   module Something

The above is fine as we will do include Foo::Concerns::Something
But the below is not:

# file at /app/concerns/something.rb
module Concerns
   module Something

So zeitwerk will complain about this: include ::Concerns::Something

So basically you cannot have ::Concerns::Something for the same reason you cannot have Models::User or Controllers:UserController

In case you don’t want to change “Concerns” namespace to something else like “Support”, you can do the below:

# file at /app/concerns/something.rb module Something

# then include as below include Something

This way you can still have your “Concerns” in the “concerns” folder and avoid changing the structure of your application.

In general, you cannot have a module starting with “Concerns”.

5) Feel free to exclude monkey patches

We have monkey patches in our engine and these violated the zeitwerk:check command. For example we do some custom stuff to ActiveResource and need to inject some logic into the Base class. We have a file named lib/active_resource/base_ext.rb which defines the class ActiveResource::Base but the error we receive says that that this must define a class named ActiveResource::BaseExt

The solution we went with here was to simply configure zeitwerk to ignore this particular path in the lib folder:

loader = Zeitwerk::Loader.for_gem
loader.ignore("#{__dir__}/active_resource")

Note: You will need to manually require these files later.

6) Split apart single files with multiple classes into their own files

We had an exceptions file namespaced in our engine like this:

# lib/foo/exceptions.rb
class MissingConfigurationKeys < StandardError
 def initialize(msg = 'One or more required configuration keys are missing or invalid')
   super
 end
end

class MappingsOutOfDate < StandardError
 def initialize(msg = 'ID mappings are out of date')
   super
 end
end

class ImageNotCopiedError < StandardError
 def initialize(msg = 'Image not copied during product copy')
   super
 end
end

These classes were available throughout the app simply by their name:

raise MissingConfigurationKeys

The zeitwerk rules had us break these classes into their own files and reference them using the engine namespace:

# lib/foo/missing_configuration_keys.rb
raise Foo::MissingConfigurationKeys

7) If you do decide to enable zeitwerk in a Rails Engine, the order in which you configure zeitwerk in your main interface matters!

In the first attempt we followed the README which lists the steps as follows:

# lib/foo.rb (main file)
require "zeitwerk"
loader = Zeitwerk::Loader.for_gem
loader.setup # ready!

module Foo
 # ...
end

loader.eager_load # optionally

Everything seemed to work fine when using this sequence and mounting the engine in our Rails app using a local filesystem Gemfile path.

After releasing the Engine as a built gem however, things blew up during startup with the message “Foo not found”. We fought with this for a few hours and eventually the eureka! moment came after a little refactoring: We moved the zeitwerk loading after the definition of our engine and that did the trick:

# lib/foo.rb (main file)

require internal/external gems

module Foo
end

require 'zeitwerk'

loader = Zeitwerk::Loader.for_gem
loader.ignore("#{__dir__}/active_resource")
loader.setup

loader.eager_load

8) Collapse directories that exist only for organization

It is very common to have directories which only exist for the purpose of giving structure to your application.

For example, suppose you have an API gem named “location_api” which has files Country and City. Each resource has a separate class which is organized as lib/location_api/resources/country.rb and lib/location_api/resources/city.rb

Now, Zeitwerk will expect country.rb to be something like below:

module Resources
   Module Country

And now when you want to use this API, you will need to make a call something like

LocationApi::Resources::Country

This may be fine if you are building a new gem. But, In case you have an existing structure like this before moving to zeitwerk. All the existing calls to this API gem will be LocationApi::Country.

This could be a simple refactor where references are changed to LocationApi::Resources::Country. But what if this gem is used by a large number of applications spread out across your entire code base? Not as simple right?

Here is where Zeitwerk’s collapse functionality comes in:

loader.collapse("#{__dir__}/location_api/resources")

By simply adding the collapse directive, keep using the API as LocationApi::Country without any further changes to your application.

9) Use eager loading to weed out hidden problems

In your engine’s main file, you can instruct zeitwerk to eager load your files as shown in tip #7.

The opposite of eager loading is lazy loading which ignores your classes until the code path they are on is activated by some input (API call, view click etc). We found that enabling eager loading helped increase our confidence that we didn’t miss anything.

That’s it! 9 tips and tricks that we used to switch from the classic to zeitwerk autoloader in one of our products. The product is now humming along using Sidekiq 6 and Rails 6 in production. It also unlocked the ability for us to upgrade our Redis version as well!

If you were stuck or unsure about upgrading your autoloader we hope these 9 tips will help you attack the problem in your own rails app and maybe save a little time in the process.

Here are some references that we found useful:

Interested in solving problems like this one? Explore our Open Positions or join our Talent Network


Profile picture of Mandeep Khinda
Mandeep Khinda
Mandeep Khinda is a DevOps Specialist. After obtaining his Bachelor of Science in Computer Engineering from the University of Ottawa, Mandeep brought his skills in software development, solution architecture, and troubleshooting to Rewind. When Mandeep isn't solving interesting problems using cool technology, he spends his time with his family, playing hockey, or mountain biking the local trails in Ottawa, Ontario.