Techouse Developers Blog

テックハウス開発者ブログ|マルチプロダクト型スタートアップ|エンジニアによる技術情報を発信|SaaS、求人プラットフォーム、DX推進

maintenance_tasksとData Migration - Kaigi on Rails Day2 Data Migration on Rails を受けて

ogp

Kaigi on Rails 2024

こんにちは、クラウドハウス労務のoctavioです。2024年10月に新卒として入社しました。

10/25(金)・26(土)の2日間に渡って、有明セントラルタワーホール&カンファレンスで Kaigi on Rails 2024 が開催されました。

みなさん、参加されましたか?

私は今回が初めての技術カンファレンス参加でした。
初めての参加ということで、聴講するだけでも緊張していましたが、「初学者から上級者までが楽しめるWeb系の技術カンファレンス」というコンセプトの通り、経験の浅い私でも多くの学びを得られる場で、とても有意義な時間を過ごせました。

本記事では、個人的に印象に残った @ohbarye様 のDay2での発表 「Data Migration on Rails」 についてのまとめと、 それに関連して、紹介があったmaintenance_tasks gem を試してみた内容を共有していきます。

セッションの内容 Data Migration on Rails

本セッションは、未だRails Wayが確立されていないと思われる Data Migration に関する発表です。
さまざまなアプローチと、それぞれのメリット・デメリット、プロジェクトに応じたアプローチ選定のポイントを紹介している非常に勉強になる発表でした。

セッションと重複した説明にはなりますが、ここで言うData Migration とは、狭義Data Migrationであり、データの移行・変更を伴う操作のことを指します。 データベースのスキーマ変更などのDDL的な操作(schema_migration, ActiveRecord::Migrationで担当する領域)とは異なり、こちらは実際のデータそのものの操作に焦点を当てています。

Rails公式が提供している一貫した手法がないため、各現場の事情や、その時々の状況に応じて、さまざまなアプローチが取られているのがData Migrationです。セッションではその中の代表的ないくつかのアプローチを紹介していました。

詳細については、Speaker Deck にスライドがアップロードされていますので、そちらをご覧ください。

ちなみに、私も自分たちのプロダクトではどのようにしてきたかを振り返ってみましたが、以下のようなアプローチを取っていました。

  • 基本的にはrake task を推奨
  • 一度きりの操作であればrails runner で実行
  • バリデーションやコールバックを経由する必要がない変更などは、権限者にSQL文を渡して直接実行してもらうこともあったが、最近は非推奨化。
    • 最もPhysicalで最もPrimitiveな方法 (スライド引用)

どうでしょうか?セッション内で紹介されていたアンケート結果と比較してみると、かなり一般的なものだったのでは無いかと思いました。

maintenance_tasks gem

セッションの後半では、数多くの専用gemが無常に生み出されては消えていく現状についての話がありました。

Data Migrationに関しては、明確な「Rails Way」がないため、多くの開発者が独自に解決策を模索してきました。

そんな中でも、特別枠 として、Shopifyが開発している maintenance_tasks というgemが紹介されました。

セッションの中で紹介されていたmaintenance_tasks の特徴は以下です。

  • rails generate で生成されたテンプレートをもとに処理を実装
  • Rails engineで提供されるダッシュボード
  • Active Jobを経由した非同期での実行、履歴管理
  • Jobの一時停止、再開が容易
  • CSVやGUIから、実行時のパラメータを渡せる

Railsガイド でも、Rails7.2以降、このgemが推薦されていることもセッションで言及されていましたね。

ということで、 これが新たなData Migration の事実上の標準となる未来に希望を抱いた自分も、実際にこのgemを試してみました。本記事では、その使い方を簡単に紹介いたします。

実際に試してみた

以下は主に、maintenance tasks を参考にしています。

インストール

$ bundle add maintenance_tasks
$ bin/rails generate maintenance_tasks:install

2行目のコマンドを実行すると、以下のようなマイグレーションが作成・実行され、maintenance_task_runsというテーブルが作成されます。

# frozen_string_literal: true

# This migration comes from maintenance_tasks (originally 20201211151756)
class CreateMaintenanceTasksRuns < ActiveRecord::Migration[6.0]
  def change
    create_table(:maintenance_tasks_runs) do |t|
      t.string(:task_name, null: false)
      t.datetime(:started_at)
      t.datetime(:ended_at)
      t.float(:time_running, default: 0.0, null: false)
      t.integer(:tick_count, default: 0, null: false)
      t.integer(:tick_total)
      t.string(:job_id)
      t.bigint(:cursor)
      t.string(:status, default: :enqueued, null: false)
      t.string(:error_class)
      t.string(:error_message)
      t.text(:backtrace)
      t.timestamps
      t.index(:task_name)
      t.index([:task_name, :created_at], order: { created_at: :desc })
    end
  end
end

作成したタスクが実行された時の、実行履歴を保存するテーブルです。

管理画面へのルーティングを追加する

プロジェクトによっては異なる場合がありますが、自分の場合はバックドアからのみアクセスできるように以下のような記述を追加しました。

# config/routes.rb
mount MaintenanceTasks::Engine, at: '/maintenance_tasks'

これによって、maintenance_tasksの管理画面に設定したパスからアクセスできるようになります。

管理画面

タスクの作成

Rails generatorを使って、タスクを作成できます。

$ bin/rails generate maintenance_tasks:task my_task

すると以下のような位置に、タスクとテストのテンプレートが作成されます。

create  app/tasks/maintenance/my_task.rb
create  spec/tasks/maintenance/my_task_spec.rb

rake taskが一般的にlib/tasks 以下に配置されるのに対して、maintenance_tasks ではapp/tasks 以下に配置されるという点は注意が必要です。

generatorで作成されたmy_task.rb は以下のような内容になっています。

# frozen_string_literal: true

module Maintenance
  class MyTask < MaintenanceTasks::Task
    def collection
      # Collection to be iterated over
      # Must be Active Record Relation or Array
    end

    def process(element)
      # The work to be done in a single iteration of the task.
      # This should be idempotent, as the same element may be processed more
      # than once if the task is interrupted and resumed.
    end

    def count
      # Optionally, define the number of rows that will be iterated over
      # This is used to track the task's progress
    end
  end
end

タスクのクラスはMaintenanceTasks::Task クラスを継承しています。
その中にcollection メソッド、process メソッド、count メソッドが作られ、中身を埋めるようなテンプレートになっています。

各メソッドの基本的な役割は以下の通りです。

  • collection
    • タスクの対象となるデータを返すメソッド
    • ActiveRecord Relation か Array を返す
  • process
    • collection で取得したデータを処理するメソッド
    • collectionのひとつの要素に対して処理を行う
    • 要素によって実行される回数が異なる場合があるため、「冪等性」があることが求められる。
  • count
    • Optional
    • タスクの進捗状況を管理するために、実行回数のイテレーション数の目安を渡す

タスクの実行

タスクを作成すると、管理画面で一覧が表示されます。

maintenance_tasks_gui_2.png

タスクを選択して、RUNを押すと、タスクがActiveJobで非同期処理として実行が開始されます。

maintenance_tasks_gui_3.png

Pauseボタンと、Cancelボタンが表示されている通り、処理の一時停止やキャンセルも可能です。タスクの進行状況もリアルタイムで表示されるので、どの程度進んでいるかが一目でわかります。 countメソッドの結果が、進捗に書かれているout of 1000 times の値になっていますね。非常にわかりやすいです。

タスク作成時に、以下のように --csv オプションをつけると、CSVによるパラメータの渡し方が可能になっています。

$ bin/rails generate maintenance_tasks:task import_example_task --csv

maintenance_tasks_gui_4.png

管理画面も、CSVファイルをアップロードするフォームが表示されるようになり、processメソッドにはCSVの各行が渡されるようになります。 countメソッドは、暗黙的にCSVの列数になるので、進捗状況も問題なく把握できます。

def count(task)
  task.csv_content.count("\n") - 1
end

CSVオプションをつけたタスクのテンプレートが以下です。

# frozen_string_literal: true

module Maintenance
  class ImportPostsTask < MaintenanceTasks::Task
    csv_collection

    def process(row)
      # The work to be done on a row of the CSV
    end
  end
end

csv_collection の1行の指定で、CSVによる処理が可能になるのは非常に便利ですね。

引数を渡す

rake taskやrails runnerで実行する時のように、実行時の引数を渡すことができるのかは、気になるポイントです。 特定の企業や特定ユーザーに対してのみ、データパッチを実行したい場面などが実際には多いはずです。

# frozen_string_literal: true

module Maintenance
  class UpdateAllEmployeeForCompany < MaintenanceTasks::Task
    attribute :company_id, :string
    validates :company_id, presence: true

    def collection
      # Collection to be iterated over
      # Must be Active Record Relation or Array
      Employee.where(company_id: company_id)
    end

    def process(element)
      # The work to be done in a single iteration of the task.
      # This should be idempotent, as the same element may be processed more
      # than once if the task is interrupted and resumed.
      element.update!(dummy_column: 'dummy_value')
    end

    def count
      # Optionally, define the number of rows that will be iterated over
      # This is used to track the task's progress
      collection.count
    end
  end
end

このようにattribute メソッドを使って、引数を受け取ることができます。validates を用いた引数のバリデーションも可能です。

管理画面はこのようになりました。

maintenance_tasks_gui_5.png

attribete メソッドで指定した型に合わせたフォームが出現し、引数を入力してRUNを押すと、引数を渡してタスクが実行されます。 validates で指定したバリデーションも、管理画面上でエラーが表示されるようになっています。

別のジョブを呼び出す

module Maintenance
  class NoCollectionTask < MaintenanceTasks::Task
    no_collection

    def process
      SomeAsyncJob.perform_later
    end
  end
end

no_collection を指定することでcollectionメソッドを省略でき、processメソッドの中ではプロダクトの任意の非同期のActiveJobを呼び出すことができます。 これによって、別のところで管理しているAciveJobを、maintenance_tasksの管理画面から実行 & 管理することが可能になります。

コールバックの利用

process メソッドでトリガーされるコールバックが用意されています。

  • after_start
  • after_pause
  • after_interrupt
  • after_cancel
  • after_complete
  • after_error
module Maintenance
  class UpdatePostsTask < MaintenanceTasks::Task
    after_complete :notify
    after_error :dangerous_notify

    def notify
      Rails.logger.info('Task completed!')
    end
    
    def dangerous_notify
      raise NotDeliveredError
    end

    # ...
  end
end

これによってタスクが完了した際に、任意の処理を実行することができます。通知や監視をうまくここに組み込むことができそうです。

コマンドラインからの実行

管理画面からのGUI実行だけでなく、コマンドライン実行も可能です。

$ bundle exec maintenance_tasks perforce Maintenance::MyTask

CSVを処理するときは、--csv オプションをつける必要があります。標準入力からの受け取りも可能です。

$ bundle exec maintenance_tasks perforce Maintenance::ImportPostsTask --csv path/to/csv

# 標準入力からの受け取り
$ curl 'some/remote/csv' | bundle exec maintenance_tasks perforce Maintenance::ImportPostsTask --csv 

まとめ

実際に試してみて、maintenance_tasks は、導入コストも低く非常に使いやすいと感じました。

セッションの通り、万能の薬は存在しないですが、maintenance_tasksは、Railsのコンテキストの中に、Data Migrationをかなりうまく組み込むことができている気がしました。 諸行無常に生まれては消えていくgemたちの流れの中で、maintenance_tasksがRailsの中でどのような位置を確立していくのか、今後の動向にも注目して参ります。

最後になりますが、本記事を執筆するきっかけとなった @ohbarye様 、Kaigi on Rails 2024 運営の皆様にこの場を借りて感謝を申し上げます。


Techouseでは、社会課題の解決に一緒に取り組むエンジニアを募集しております。 ご応募お待ちしております。

jp.techouse.com