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

  1. 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, any GET request that comes through will do a check against that last_write attribute. If the last_write was less than 2 seconds ago, Rails will ensure that all reads in that GET request come from the primary database instead of the replica. If the last_write was more than 2 seconds ago, the replica will be used for reads.
  2. 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 a POST request will be read from the primary database.
  3. 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).
  4. 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 aGET request will come from the replica regardless of what has taken place is the request, and this is because the last_write session attribute, which is what is used to determine if the primary db should be read from, cannot be set in a GET request even if that request does a write.

Once we were confident in the answers to these questions we updated our code by doing the following.

Code Changes Made

  1. Look at every GET request (found these by running bundle exec rake routs | grep GET ) in the app in an effort to find any GET requests where writes were taking place. Any writes that we found were wrapped in the the ActiveRecord::Base.connected_to(role: :writing) block (see docs for details).
  2. 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.

  1. create app/assets/config/manifest.js file and paste in the following.
    //= link_tree ../images
    //= link_directory ../javascripts .js
    //= link_directory ../stylesheets .css
    
  2. Move Rails.application.config.assets.precompile += from config/initializers/assets.rb to new app/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.