loyalty.dev

Upgrading Hanami - Part 1: Migrations

The first little step on our journey to upgrade our Hanami 1.3 services to Hanami 2.0. This article opens a series of posts related to upgrading Hanami projects. This time we'll cover the Migrations upgrade.

TLDR

You may visit the curated hanami upgrade thread on the forum where We're collecting all resources related to this upgrade.

For example, I've published the video guiding through the persistence layer upgrade. This series is the expanded version, split into more accessible and informative sections.

Enjoy!


Recently we started a new challenge in our company - upgrading all our Hanami Services from Hanami 1.3 to Hanami 2.0.

This is BIG!

If you think about it, it's like upgrading rails from version 1. The challenge is, that Hanami 2.0 is a complete framework rewrite, similar to rails in the early stages.

This post starts a short series, where I'll share my thoughts around upgrading one of our services, split into steps, so if you'll need to apply it to your projects, it should be simpler.

WHY?

Let's start with our why. First of all, why have we waited for so long?

Hanami 1 not active.

Hanami team has great assumptions that led to creating an amazing first version of the framework a few years back. However, it was far from perfection.

With version 2, the team put the framework to the next level, optimizing performance and developers' happiness by a whole lot!

Finally, due to a lack of updates in version 1, we stuck with older libraries and the old Ruby Version. This update will allow us to use again the most modern solutions available for the Ruby community!

Community Support

The Hanami community is still not too big, but growing fast, and more people are joining every week, willing to share their ideas and help each other. Let's be honest - while we're all very helpful and try to share what we know with others, it's useful to also be able to ask questions when we're stuck.

With the upgrade, we'll be able to ask questions to a WHOLE LOT of ruby developers and extremely skilled hackers all hyped about Hanami.

That's a priceless benefit.

Hanami 2 exciting stuff.

Then finally, we have all the cool features that come with the new version, performance optimizations, architecture improvements... all that the geeks need more than air.

Let me just list a few.

Hanami 2.X will use ROM 5+ under the hood

From ROM version 3 (used by Hanami 1.X) to ROM 5 is a big shift, and the newest version of the whole ROM gem family is something I can't wait to work with!

We're going to upgrade our stuff to ROM 5 even though version six is behind the corner, but when it comes, it'll be very easy to upgrade further, because the API is backward-compatible.

Old libraries prevent us from upgrading to Ruby 3.X

A similar thing happens for Ruby. The shift is quite significant, and after the update, we'll be able to make use of all sorts of cool ruby 3 features, like ractors, improved pattern matching, or a built-in debugger, with tons of performance improvements.

Having that said, let's take a look at how we tackle this update.

Slices, Dry-System, Providers.

Hanami 2 is amazing in terms of providing tools for maintaining scalable applications, and some of our apps do scale. We have small and big services in the company, and sometimes it's getting frustrating when we need to wait several seconds to run a single component test suit.

After the upgrade, file loading will be improved by a lot!

  • Slices allow us to load only files needed by this single slice.
  • Providers allow us to load a different set of files on application setup and after application boot.
  • Dry-System brings the improved file-loading mechanism via zeitwerk, and built-in dependency injection, which will simplify our tests like crazy!

Are you excited as I am? Then let's go!

Strategy

The draft strategy was simple.

  1. Update gems - as much as possible
  2. Switch Hanami::Model -> ROM 5 configuration
  3. Upgrade Ruby to 3.X
  4. Update Hanami 1.3 -> 2.0
  5. Cleanup

It looks simple, doesn't it? So what could possibly go wrong?

image

Nothing, really. And the fact I've ended up with almost +3k lines of code affected was just a coincidence:D.

image

It's worth mentioning, that it was 1. hanami-model -> ROM 5 upgrade part 2. A reporting service, which definitely isn't the biggest, nor most complicated of our projects.

Requirements

So let's have a look at the requirements we have to come up with the actual upgrade strategy.

  1. No downtime
  2. No production issues.
  3. No security leaks

There are just three, but extremely important and make our upgrade a bit more challenging.

  1. No security leaks means - posting questions on discussion forums requires me to write some imaginary examples of code snippets for blockers I faced.
  2. No production issues, means - extremely well-tested code base, which we didn't have in some of the services.
  3. No downtime means - figure out how to deploy it for all our tenants without blocking the team.

To make it all work, we needed to split the update into several steps, to minimize the range of changes in the single release.

The first big step was to just update the persistence layer and replace the hanami-model with ROM 5.

However, as you may guess from the PR changes above, a further split had been required. I was able to extract the migrations changes apart from the PR and before I'll tell you how I did it.

Step 1. Update gems

Before we did anything, we wanted to be sure that all our gems are using the newest possible versions.

In Ascenda we have the practice to periodically updating all the gems so this part was not a big deal.

The only thing that drove my eye was the dry-container issue so we were forced to lock it at version 0.8.0

dry-container - lock it at 0.8.0

Step 2. Update Migrations

Having the updated dependencies as much as possible, I've gone through the migrations switch.

Setting up database rake tasks for ROM

ROM comes with full database migration support - own migration generators, - rake tasks for managing database, - the syntax to implement migration classes.

The available rake tasks are:

  • rake db:create_migration[create_users] - create migration file under db/migrate
  • rake db:migrate - runs migrations
  • rake db:clean - removes all tables
  • rake db:reset - removes all tables and re-runs migrations

To make them all work, we need just to add the rake db:setup, to set up the db connection properly.

# rakelib/db.rake

namespace :db do
  task :setup do
    ROM::SQL::RakeSupport.env =
      ROM::Configuration.new(
        :sql,
        ENV.fetch('DATABASE_URL'),
        logger: Hanami.logger,
        extensions: %i(pg_array pg_json)
      )
  end
end

Additional Rake tasks (optional)

You may notice, that on the tasks list above, there are missing positions like: rake db:create or rake db:drop, so we added them manually for our convenience.

I guess just running createdb in the shell terminal is fine, but... isn't it better if the DB name is read from the environment?

image

I know what you're thinking, but we're geeks, please be forgiving.

If you want to implement the missing rake tasks, the easiest way is to copy them from the Hanami source code. But please, remember to support the Hanami team, if you do so!

In Ascenda, we've made a bit more sophisticated approach, by NOT copying the source code from Hanami, but supporting the team anyway: XD.

# rakelib/db.rake

namespace :db do
  # ...

  desc 'Create database and run migrations'
  task prepare: %i(create migrate)

  desc 'Drop database'
  task :drop do
    DbTaskHelper.set_environment_variables

    DbTaskHelper.call_db_command('dropdb')
  end
class DbTaskHelper
  class << self
  # ...
    def call_db_command(command)
      Open3.popen3(*command_with_credentials(command)) do |_stdin, _stdout, stderr, wait_thr|
        raise MigrationError, stderr.read unless wait_thr.value.success? # wait_thr.value is the exit status
      end
    rescue SystemCallError => e
      raise MigrationError, e.message
    end
  end
end

The rest should be pretty straightforward but feel free to reach out on the discourse if it's not!

Move migration files

Having the rake tasks setup, we've moved all our migrations to a new location db/migrations -> db/migrate, because this is what ROM threats by default and we wanted to minimize the number of custom tweaks in the future.

image

Hanami Migrations updates

After moving migrations, the only thing left was to switch from using the Hanami::Model constant to ROM::SQL, as a preparation to remove the gem in the upcoming commits.

image

After that we needed to change the primary_key for uuid types, to different syntax, but that was pretty much it.

# before
primary_key :id, 'uuid'

# after
column :id, 'uuid', primary_key: true, default: Sequel.function(:uuid_generate_v4)

Because ROM uses sequel for migration implementation, it was not hard to find all required updates in the sequel migration documentation.

Good things about migrations

It's worth mentioning, that number of changes required within the migrations files was pretty much minimal and this is because we've followed a few practices, that I'd suggest to make a rule of thumb whenever you work with migration files in any framework.

  • We did not overuse migrations (seeding data)
  • If seeding was required, we always did raw sql queries

Summary of upgrading Hanami Migrations

This upgrade is big, and it's a kind of work one does not want to do, but at some point, it has to be done.

It reminded me when I needed to split a big monolith into two separate projects a few years back.

I'm not sure why, but I love such tasks, but I don't want to face them too often :D.

I understand that Hanami needed to stabilize itself after the first major release, and it was not feasible for the team to keep releasing often and keep all the changes backward-compatible.

Now, however, with team being bigger, and more companies joining to support the project, with monthly official releases, and growing community, I bet it will only be easier and easier to work with the upgrades in the future!

Hurray!

image

We done it. The first part....

but... It's not the end.

In the next article, I'll tell more about the rest of the persistence upgrades, and challenges we've faced and solved!

Stay tuned by following me on Twitter!