Rails 6 Upgrade Tips
I was recently tasked with upgrading our codebase at GoNoodle from Rails 5.2.3
to 6.0.1
, and I wanted to take some time to document my experience and share some tips that others might find helpful. My main objective here is to articulate what I learned, and to draw attention to any hang ups I ran into that were not directly addressed in the Rails migration guide. It is worth noting that because this was my first time doing a major update, some of what I mention will be version update agnostic and will just be general Rails upgrade tips, but the rest will be specific to this version upgrade.
Setting Up Multi Database Support
The multi database support in Rails 6 is something we were really excited about, and while the docs for setting this up were by no means bad, there were some things that were left a little unclear. Here are some of my findings.
Handling automatic connection switching
To take full advantage of the Rails multi database support, we set up most of our models to read from our replica, and we activated automatic connections switching. When we first started implementing this we realized we were a little unclear about how this all worked, and in an effort to be sure we were implementing everything correctly we took some time to do some testing and answer some questions (listed below with answers) relating to the portion of the docs outlined below.
“If the application is receiving a POST, PUT, DELETE, or PATCH request the application will automatically write to the primary. For the specified time after the write the application will read from the primary. For a GET or HEAD request the application will read from the replica unless there was a recent write. Rails guarantees “read your own write” and will send your GET or HEAD request to the primary if it’s within the delay window”. - Rails Docs
Questions Asked
- How does Rails keep track of when the last write happened?
- Rails keeps a
last_write
attribute in the session that is an integer of the seconds since epoch. So, if your delay window is set to be 2 seconds, for example, anyGET
request that comes through will do a check against thatlast_write
attribute. If thelast_write
was less than 2 seconds ago, Rails will ensure that all reads in thatGET
request come from the primary database instead of the replica. If thelast_write
was more than 2 seconds ago, the replica will be used for reads.
- Rails keeps a
- Is the “read your own write” guarantee table specific, or is it database wide. In other words, if a write takes place in table A, and then a read from table B happens within the delay window, does it use the replica for that read?
- No, It is not table specific. Any table being read from in a
GET
request that takes place within the delay window after aPOST
request will be read from the primary database.
- No, It is not table specific. Any table being read from in a
- If a write takes place during a
GET
request (this is not restful but there are times where it can make sense), does that write automatically happen in the primary database.- No. Any write that takes place in a
GET
request will be attempted in the replica. Ensuring that this write take place in the primary database requires updating the code (details in “Code Changes Made” section below).
- No. Any write that takes place in a
- If a
GET
request happens to do a write to the database, will any subsequent read in that request automatically read from the primary because of the “read your own write” guarantee?- No. All reads in a
GET
request will come from the replica regardless of what has taken place is the request, and this is because thelast_write
session attribute, which is what is used to determine if the primary db should be read from, cannot be set in aGET
request even if that request does a write.
- No. All reads in a
Once we were confident in the answers to these questions we updated our code by doing the following.
Code Changes Made
- Look at every
GET
request (found these by runningbundle exec rake routs | grep GET
) in the app in an effort to find anyGET
requests where writes were taking place. Any writes that we found were wrapped in the theActiveRecord::Base.connected_to(role: :writing)
block (see docs for details). - Create a new mysql (applies to postgres as well) user with read only access and update your database.yml file to ensure your replica uses the credentials for this user. This will ensure an error will be thrown if a write is attempted in a
GET
request.
How to rollback a migration that is not in your primary database
Doing rails db:rollback
will rollback your most recent migration (these migration files are in db/migrate
in your primary database, but if you want to rollback a migration in another database (these migration files are in db/[name_of_your_db]_migrate
) you need to do rails db:migrate:down:[name_of_your_db] VERSION=[migration_version_number]
.
manifest.js
After successfully updating the Rails version I got the following error when trying to run the app locally. Expected to find a manifest file in app/assets/config/manifest.js
. To handle this I had to do some digging and found the following post by Richard Schneeman, which directed me to do the following.
- create
app/assets/config/manifest.js
file and paste in the following.//= link_tree ../images //= link_directory ../javascripts .js //= link_directory ../stylesheets .css
- Move
Rails.application.config.assets.precompile +=
fromconfig/initializers/assets.rb
to newapp/assets/config/manifest.js
file.
Updating Gems
The first thing I did to get started was to update the Rails version in the gemfile and run bundle update rails
, and this resulted in the following error. Note that while my subsequent explanation will apply to the actionpack
scenario listed below, the core way to address this error applies to all gems.
Bundler could not find compatible versions for gem "actionpack":
In Gemfile:
active_model_serializers (~> 0.10) was resolved to 0.10.9, which depends on
actionpack (>= 4.1, < 6)
active_admin_datetimepicker (~> 0.6) was resolved to 0.6.3, which depends on
activeadmin (~> 1.1) was resolved to 1.4.3, which depends on
formtastic (~> 3.1) was resolved to 3.1.5, which depends on
actionpack (>= 3.2.13)
active_admin_datetimepicker (~> 0.6) was resolved to 0.6.3, which depends on
activeadmin (~> 1.1) was resolved to 1.4.3, which depends on
inherited_resources (>= 1.9.0) was resolved to 1.10.0, which depends on
has_scope (~> 0.6) was resolved to 0.7.2, which depends on
actionpack (>= 4.1)
active_admin_datetimepicker (~> 0.6) was resolved to 0.6.3, which depends on
activeadmin (~> 1.1) was resolved to 1.4.3, which depends on
inherited_resources (>= 1.9.0) was resolved to 1.10.0, which depends on
actionpack (>= 5.0, < 6.0)
lograge (~> 0.11) was resolved to 0.11.0, which depends on
actionpack (>= 4)
rails (= 6.0.1) was resolved to 6.0.1, which depends on
actionpack (= 6.0.1)
rails-controller-testing was resolved to 1.0.4, which depends on
actionpack (>= 5.0.1.x)
active_admin_datetimepicker (~> 0.6) was resolved to 0.6.3, which depends on
activeadmin (~> 1.1) was resolved to 1.4.3, which depends on
ransack (>= 1.8.7) was resolved to 2.1.1, which depends on
actionpack (>= 5.0)
responders (~> 2.4) was resolved to 2.4.1, which depends on
actionpack (>= 4.2.0, < 6.0)
rspec-rails was resolved to 3.7.2, which depends on
actionpack (>= 3.0)
The issue here is that actionpack
, because it is a gem that ships with Rails, is trying to upgrade to version 6.0.1
, but one or more of my gems has a sub-dependency that requires an actionpack
version < 6. The confusing part about this error is that not all of the gems listed here are at fault, so it’s important to know where to look when addressing this error. active_model_serializers
(the first gem mentioned here), for example, is one of the offending culprits because it requires actionpack (>= 4.1, < 6)
. lograge
, however, is not one of the offending culprits because it requires actionpack (>= 4)
. After figuring out how to navigate these error messages is was just a matter of updating the gem version of the offending culprits to versions that support actionpack 6.0.1
.
All in all the upgrade experience, aside from a major hangup we had with our mysql_online_migrations
gem (see details here if interested), was fairly straightforward and painless, and I definitely learned a lot. Lastly, I want to give a shout out Travis Roberts for all of this help during this process. His contribution was invaluable.