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:
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:
- Zeitwerk documentation on GitHub:
- Understanding Zeitwerk:
- Some information on monkey patching or Overriding of classes/modules
Interested in solving problems like this one? Explore our Open Positions or join our Talent Network.