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
- タスクの進捗状況を管理するために、実行回数のイテレーション数の目安を渡す
タスクの実行
タスクを作成すると、管理画面で一覧が表示されます。
タスクを選択して、RUNを押すと、タスクがActiveJobで非同期処理として実行が開始されます。
Pauseボタンと、Cancelボタンが表示されている通り、処理の一時停止やキャンセルも可能です。タスクの進行状況もリアルタイムで表示されるので、どの程度進んでいるかが一目でわかります。
countメソッドの結果が、進捗に書かれているout of 1000 times
の値になっていますね。非常にわかりやすいです。
タスク作成時に、以下のように --csv
オプションをつけると、CSVによるパラメータの渡し方が可能になっています。
$ bin/rails generate maintenance_tasks:task import_example_task --csv
管理画面も、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
を用いた引数のバリデーションも可能です。
管理画面はこのようになりました。
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では、社会課題の解決に一緒に取り組むエンジニアを募集しております。 ご応募お待ちしております。