Rails の本番作業を便利にする maintenance_tasks gem の紹介
はじめに
初めまして、バックエンドエンジニアの otsubo です 🙇♂️
この記事では Rails の本番作業を便利にしてくれる maintenance_tasks gem を紹介します!
実際に Rails アプリケーションに導入して本番作業が快適になったため、利用して分かったことや注意点をまとめました。
maintenance_tasks は本番作業の実行環境を整えてくれる gem です。特に、下記のように DB や CSV のレコード単位で処理を行う単発の作業に向いています。
- DB の特定カラムの値を別のカラムをもとに更新する (backfill)
- CSV のデータを DB に投入する
こういった作業は rails runner や Kubernetes の Job で特定のスクリプトを実行するのがよくある運用かと思いますが、タスクの一時停止や再開を自前で実装するのは手間がかかります。
そこで maintenance_tasks は下記のような便利機能を提供してくれます。
- タスク定義とそのテストコードの雛形を作成するコマンド
- タスクをバックグラウンドジョブ (Active Job) として実行する仕組み
- タスクの実行・一時停止・再開や途中経過の確認ができる Web UI
maintenance_tasks の機能
installation に従い導入すると、bin/rails generate maintenance_tasks:task <タスク名>
というコマンドでタスクの雛形を生成できます。
雛形を少し編集した README の例 のようなクラスがタスクの定義になります。
# app/tasks/maintenance/update_posts_task.rb
module Maintenance
class UpdatePostsTask < MaintenanceTasks::Task
# 一度に取得するレコード数
collection_batch_size(1000)
# 処理対象の全体
def collection
Post.all
end
# レコード1件毎の処理
def process(post)
post.update!(content: "New content!")
end
end
end
このタスクを実行すると、下記の Active Job に相当するジョブが開始されるようなイメージです。
class UpdatePostJob < ApplicationJob
def perform
Post.find_each(batch_size: 1000) do |post|
post.update!(content: "New content!")
end
end
end
ただジョブを実行するだけではなく、どの位置まで処理したかを表す cursor
や累積実行時間 time_running
などの情報を maintenance_tasks_runs テーブル で管理してくれます。実行や途中経過の確認は、gem が生成する routes にアクセスして Web UI から行えます。
maintenance_tasks が生成してくれる Web UI
その他にも多くの機能があります。書ききれないほどに多くのことができるため、詳細は README を参照してください。
-
Processing Batch Collections
- レコード1件毎ではなく batch 毎に
process
メソッドを実行します。ActiveRecord::Batches#in_batches
をcollection
に指定できます。
- レコード1件毎ではなく batch 毎に
-
Creating a CSV Task
- CSV をアップロードして行毎に処理します。アップロードには Active Storage を利用します。
-
Tasks that don’t need a Collection
- 反復処理を行わないような単発スクリプトを実行します。
-
Tasks with Custom Enumerators
- DB や CSV 以外のリソースを対象にした反復処理を行います。例えば、外部 API をページネーションで呼び出しながら1件毎に処理できます。
-
Custom Task Parameters
- 実行パラメータを付与します。パラメータを入力する Web UI も生成してくれます。
- Active Model のバリデーションを記述でき、不正なパラメータの場合はタスクが実行されません。
自動生成されるパラメータ入力の UI とバリデーション
-
Subscribing to instrumentation events
- タスクの成功・失敗などに応じて処理を実行します。通知やログ出力に便利です。
導入するメリット
本番環境へのアクセスに関する統制面、そして利便性の両方にメリットがありました。
- タスクの実行・一時停止を Web UI から行えます。
- デプロイするだけで実行可能になるため、本番環境のマシンに接続して…などが不要です。
- 途中経過を確認しつつ、異常があれば一時停止できます。
- 過去の実行履歴も Web UI から確認できます。
- いつ・どのタスクを・どのパラメータで実行したか、またその結果が DB で永続化されます。
- バックグラウンドジョブ実行のプラクティスに沿った雛形を生成してくれます。
- 後述の注意点 を意識すれば、自然とタスクの中断に耐性がある実装になります(内部で利用する job-iteration の効果です)。
- テストコードの雛形も生成されるため、タスクをテストしやすいです。
実装時の注意点
README と重複する部分が多いですが、実装面で気を付けるべきポイントをいくつか挙げます。
gem が生成するエンドポイントの保護
maintenance_tasks を導入することで新しい routes や controller が追加されます。追加される controller はデフォルトで ActionController::Base
を継承しており、基本的に誰でもアクセスできる状態です。そのため、親 controller を変更してアクセス制御を行いましょう。
# config/initializers/maintenance_tasks.rb
# gem が追加する controller の親クラス (default: `ActionController::Base`)
# 例えば Admin 画面にアクセスできる人のみが task を実行できるようにする
MaintenanceTasks.parent_controller = 'CustomAdminController'
タスクは並列実行される(実行ボタン連打に注意)
後述する内部の仕組み の通り、タスク実行ボタンを押すと maintenance_tasks_runs テーブルにレコードが1件作成され、タスクの実行状態が DB 上で管理されます。タスク毎の状態管理は gem が自動で行ってくれますが、タスク間の排他制御までは行われません。そのため、実行ボタンを複数回押すと、その数だけタスクが並列に実行されます。
並列実行数を制御したい場合は Rails アプリケーション側で行う必要があります。弊社ではまだ試せていませんが、Active Job 側で特定 queue を捌く worker の concurrency を制御したり、タスク開始直後に他の maintenance_tasks_runs レコードを確認して実行可否を判断したりすることで制御できそうです。
タスクを実行する queue は、maintenance_tasks が実行する Job クラスを指定することで設定できます。
# config/initializers/maintenance_tasks.rb
MaintenanceTasks.job = "CustomTaskJob"
# app/jobs/custom_task_job.rb
class CustomTaskJob < MaintenanceTasks::TaskJob
queue_as :slow_queue
end
process メソッドはできるだけ高速かつ冪等に
メリット で記載したように、生成されるタスクの雛形はバックグラウンドジョブのプラクティスに沿った実装になっています。具体的には、タスク実行中にインフラ起因の中断(例えば、タスクを実行している Kubernetes の Pod がデプロイにより入れ替わるなど)が発生したとしても、Graceful Shutdown の条件を満たせば process
メソッド単体は実行し終えてからジョブが終了します。そして、インフラが復旧した後は中断位置の次のレコードから process
が再開されます。
ポイントは「Graceful Shutdown の条件を満たせば」という部分です。process
メソッドに時間がかかりすぎると、process
メソッドの途中でジョブが強制終了し、最悪の場合ジョブがうまくリキューされずにロストしてしまう可能性があります。具体的な時間はインフラと queue adapter の設定によって変わりますが、Sidekiq の場合はタイムアウトオプション(デフォルトは 25 秒)以内に process
メソッドを完了する必要があります。
また、Graceful Shutdown できなかった場合、リトライ時に process
メソッドが同じ引数で2回以上実行される可能性があります。そのため、process
メソッドはできるだけ冪等性を保つように実装しましょう。
詳しくは README の Considerations when writing Tasks セクションや、job-iteration のガイド Iteration: how it works を参照してください。
タスクインスタンスのライフサイクルはジョブと同じ
この記事では、読みやすさを重視して「タスクの実行」「ジョブの実行」を厳密に区別せず使っていますが、ジョブとタスクは異なる概念です。エンキューされて実行されるのはジョブであり、ジョブの中でタスクに定義した処理を実行していくという関係です。具体的にコードを見ていくと、 ジョブ (MaintenanceTasks::TaskJob
) の開始直前のコールバック before_perform
でタスク(例えば SampleTask
)をインスタンス化し、
ジョブの実行中はそのインスタンスを使い回します。
そのため、下記のようにタスクのインスタンス変数 @processed_count
をカウントアップするコードは一応動作します。一時停止や中断を挟まない場合は、最終的な @processed_count
は collection
の件数と一致します。
module Maintenances
class SampleTask < MaintenanceTasks::Task
collection_batch_size(20)
after_pause :log_processed_count
after_complete :log_processed_count
def log_processed_count
p "Processed count: #{@processed_count}"
end
def collection
Post.all
end
def process(_element)
@processed_count ||= 0
@processed_count += 1
end
end
end
ただし、一時停止や中断によってジョブが再作成される場合、タスクのインスタンスも再作成されるため @processed_count
はリセットされます。例えば、collection が100件のレコードの場合、
- 実行
- 一時停止
Processed count: 15
- 再開
- 完了
Processed count: 85
(最初の15件は含まれない)
のようになります。さらにインフラ起因の中断がある場合は、 @processed_count
は中断から再開した後の件数になるため、集計操作を行う場合は途中経過を DB 上に保存するなどの工夫が必要です。
内部の仕組み
maintenance_tasks が裏側でどのように動作しているのかを見るため、シンプルなタスクを実行してジョブが発行する SQL を観察してみました。実行環境と、実行したタスクは下記の通りです。
- Ruby 3.3.8
- Rails 7.2.2.1
- Sidekiq 8.0.4
module Maintenances
class SampleTask < MaintenanceTasks::Task
collection_batch_size(20)
def collection
Post.all # 全部で100件のレコードがあるテーブル
end
def process(_element)
sleep 0.1 # gem が発行する SQL を観察するため、ユーザ定義の処理は何も行わない
end
end
end
100件のうち、24件処理したタイミングで Pause ボタンを押下して一時停止させました。その時の Sidekiq ジョブのログが下記になります。簡単にするため TRANSACTION BEGIN/COMMIT
のログは省略し、適宜コメントや改行を入れています。
-- 実行開始時の処理
-- maintenance_tasks_runs.status を "enqueued" --> "running" に更新
-- maintenance_tasks_runs.tick_total に対象の全件数を保存
MaintenanceTasks::Run Load (2.3ms) SELECT `maintenance_tasks_runs`.* FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1 LIMIT 1
MaintenanceTasks::Run Update (0.2ms) UPDATE `maintenance_tasks_runs` SET `maintenance_tasks_runs`.`status` = 'running', `maintenance_tasks_runs`.`updated_at` = '2025-06-16 06:00:41.829229', `maintenance_tasks_runs`.`lock_version` = 1 WHERE `maintenance_tasks_runs`.`id` = 1 AND `maintenance_tasks_runs`.`lock_version` = 0
Post Count (0.5ms) SELECT COUNT(*) FROM `posts`
MaintenanceTasks::Run Update (0.2ms) UPDATE `maintenance_tasks_runs` SET `maintenance_tasks_runs`.`started_at` = '2025-06-16 06:00:41', `maintenance_tasks_runs`.`tick_total` = 100, `maintenance_tasks_runs`.`updated_at` = '2025-06-16 06:00:41.836991', `maintenance_tasks_runs`.`lock_version` = 2 WHERE `maintenance_tasks_runs`.`id` = 1 AND `maintenance_tasks_runs`.`lock_version` = 1
-- collection から取得(1つめの batch)
Post Load (0.2ms) SELECT `posts`.* FROM `posts` ORDER BY posts.id LIMIT 20
-- process 毎に maintenance_tasks_runs.status を確認
MaintenanceTasks::Run Pluck (0.3ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Pluck (0.4ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Pluck (1.4ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Pluck (1.2ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Pluck (0.6ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Pluck (0.5ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Pluck (1.4ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Pluck (0.9ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Pluck (1.0ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
-- tick (1秒) 毎にタスクの進捗を更新
MaintenanceTasks::Run Update All (3.6ms) UPDATE `maintenance_tasks_runs` SET `maintenance_tasks_runs`.`tick_count` = COALESCE(`maintenance_tasks_runs`.`tick_count`, 0) + 10, `maintenance_tasks_runs`.`time_running` = COALESCE(`maintenance_tasks_runs`.`time_running`, 0) + 1.08908025, `maintenance_tasks_runs`.`lock_version` = COALESCE(`maintenance_tasks_runs`.`lock_version`, 0) + 1, `maintenance_tasks_runs`.`updated_at` = '2025-06-16 06:00:42.931541' WHERE `maintenance_tasks_runs`.`id` = 1
-- process 毎に maintenance_tasks_runs.status を確認(続き)
MaintenanceTasks::Run Pluck (0.4ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Pluck (1.2ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Pluck (1.9ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Pluck (1.0ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Pluck (1.1ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Pluck (1.0ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Pluck (0.7ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Pluck (7.7ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Pluck (4.3ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
-- tick (1秒) 毎にタスクの進捗を更新
MaintenanceTasks::Run Update All (3.1ms) UPDATE `maintenance_tasks_runs` SET `maintenance_tasks_runs`.`tick_count` = COALESCE(`maintenance_tasks_runs`.`tick_count`, 0) + 9, `maintenance_tasks_runs`.`time_running` = COALESCE(`maintenance_tasks_runs`.`time_running`, 0) + 1.042947584, `maintenance_tasks_runs`.`lock_version` = COALESCE(`maintenance_tasks_runs`.`lock_version`, 0) + 1, `maintenance_tasks_runs`.`updated_at` = '2025-06-16 06:00:43.966657' WHERE `maintenance_tasks_runs`.`id` = 1
-- process 毎に maintenance_tasks_runs.status を確認(続き)
MaintenanceTasks::Run Pluck (0.4ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Pluck (1.6ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
-- collection から取得(2つめの batch)
Post Load (0.7ms) SELECT `posts`.* FROM `posts` WHERE (posts.id > '20') ORDER BY posts.id LIMIT 20
-- process 毎に maintenance_tasks_runs.status を確認
MaintenanceTasks::Run Pluck (1.2ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Pluck (1.4ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Pluck (0.6ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Pluck (0.3ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
-- 一時停止を検出して maintenance_tasks_runs.status を "pausing" --> "paused" に更新
-- 一時停止位置を maintenance_tasks_runs.cursor に保存
MaintenanceTasks::Run Update All (1.4ms) UPDATE `maintenance_tasks_runs` SET `maintenance_tasks_runs`.`tick_count` = COALESCE(`maintenance_tasks_runs`.`tick_count`, 0) + 5, `maintenance_tasks_runs`.`time_running` = COALESCE(`maintenance_tasks_runs`.`time_running`, 0) + 0.540926333, `maintenance_tasks_runs`.`lock_version` = COALESCE(`maintenance_tasks_runs`.`lock_version`, 0) + 1, `maintenance_tasks_runs`.`updated_at` = '2025-06-16 06:00:44.507424' WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Update (0.2ms) UPDATE `maintenance_tasks_runs` SET `maintenance_tasks_runs`.`status` = 'paused', `maintenance_tasks_runs`.`cursor` = '24', `maintenance_tasks_runs`.`updated_at` = '2025-06-16 06:00:44.510166', `maintenance_tasks_runs`.`lock_version` = 7 WHERE `maintenance_tasks_runs`.`id` = 1 AND `maintenance_tasks_runs`.`lock_version` = 6
タスク実行ボタンを押下すると maintenance_tasks_runs
レコードが controller 側で作成されます。レコード作成はタスク実行ボタンを押下した時に呼ばれるアクション MaintenanceTasks::RunsController#create
内で Runner
が行います。
ジョブが開始されると、maintenance_tasks_runs
テーブルから id=1
のレコードを SELECT
し、状態を running
にしつつ全件数などのメタ情報を保存しているのが分かります。「タスクの実行1回」がレコード1つに紐付き、一時停止や再開はレコードの status
を切り替えることで管理しているようです。
MaintenanceTasks::Run Load (2.3ms) SELECT `maintenance_tasks_runs`.* FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1 LIMIT 1
MaintenanceTasks::Run Update (0.2ms) UPDATE `maintenance_tasks_runs` SET `maintenance_tasks_runs`.`status` = 'running', `maintenance_tasks_runs`.`updated_at` = '2025-06-16 06:00:41.829229', `maintenance_tasks_runs`.`lock_version` = 1 WHERE `maintenance_tasks_runs`.`id` = 1 AND `maintenance_tasks_runs`.`lock_version` = 0
Post Count (0.5ms) SELECT COUNT(*) FROM `posts`
MaintenanceTasks::Run Update (0.2ms) UPDATE `maintenance_tasks_runs` SET `maintenance_tasks_runs`.`started_at` = '2025-06-16 06:00:41', `maintenance_tasks_runs`.`tick_total` = 100, `maintenance_tasks_runs`.`updated_at` = '2025-06-16 06:00:41.836991', `maintenance_tasks_runs`.`lock_version` = 2 WHERE `maintenance_tasks_runs`.`id` = 1 AND `maintenance_tasks_runs`.`lock_version` = 1
続いて collection_batch_size(20)
で指定した件数のレコードを collection
から取得しています。
Post Load (0.2ms) SELECT `posts`.* FROM `posts` ORDER BY posts.id LIMIT 20
その後 process
メソッドを1件ずつ処理している様子が分かります。process
を実行するたびに maintenance_tasks_runs
レコードを取得し、status
を確認してタスクの中断や一時停止を検出しています。
MaintenanceTasks::Run Pluck (0.3ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Pluck (0.4ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Pluck (1.4ms) SELECT `maintenance_tasks_runs`.`status`, `maintenance_tasks_runs`.`lock_version` FROM `maintenance_tasks_runs` WHERE `maintenance_tasks_runs`.`id` = 1
...略...
途中で挟まれる maintenance_tasks_runs
レコードの UPDATE
は、タスクの進捗状況の更新です。この定期更新は、MaintenanceTasks.ticker_delay (デフォルト1秒)の間隔で呼ばれています。
MaintenanceTasks::Run Update All (3.1ms) UPDATE `maintenance_tasks_runs` SET `maintenance_tasks_runs`.`tick_count` = COALESCE(`maintenance_tasks_runs`.`tick_count`, 0) + 9, `maintenance_tasks_runs`.`time_running` = COALESCE(`maintenance_tasks_runs`.`time_running`, 0) + 1.042947584, `maintenance_tasks_runs`.`lock_version` = COALESCE(`maintenance_tasks_runs`.`lock_version`, 0) + 1, `maintenance_tasks_runs`.`updated_at` = '2025-06-16 06:00:43.966657' WHERE `maintenance_tasks_runs`.`id` = 1
最後のログは、Pause ボタン押下に反応してタスクの状態を paused
にするログです。
Pause ボタンを押下すると MaintenanceTasks::RunsController#pause
でタスクの状態が pausing
になります。
そのため、process
メソッド毎に行っている状態確認で一時停止を検出できます。一時停止するときに、どの位置まで処理したかを cursor
に保存し、再開時にその位置から処理を始められるようにしています。
MaintenanceTasks::Run Update All (1.4ms) UPDATE `maintenance_tasks_runs` SET `maintenance_tasks_runs`.`tick_count` = COALESCE(`maintenance_tasks_runs`.`tick_count`, 0) + 5, `maintenance_tasks_runs`.`time_running` = COALESCE(`maintenance_tasks_runs`.`time_running`, 0) + 0.540926333, `maintenance_tasks_runs`.`lock_version` = COALESCE(`maintenance_tasks_runs`.`lock_version`, 0) + 1, `maintenance_tasks_runs`.`updated_at` = '2025-06-16 06:00:44.507424' WHERE `maintenance_tasks_runs`.`id` = 1
MaintenanceTasks::Run Update (0.2ms) UPDATE `maintenance_tasks_runs` SET `maintenance_tasks_runs`.`status` = 'paused', `maintenance_tasks_runs`.`cursor` = '24', `maintenance_tasks_runs`.`updated_at` = '2025-06-16 06:00:44.510166', `maintenance_tasks_runs`.`lock_version` = 7 WHERE `maintenance_tasks_runs`.`id` = 1 AND `maintenance_tasks_runs`.`lock_version` = 6
発行される SQL をまとめると、以下の表のようになります。
タイミング | SQL |
---|---|
タスクの開始・中断時 |
UPDATE maintenance_tasks_runs で status などを更新 |
batch 毎 | collectionの取得 |
process 毎 | SELECT status, lock_version FROM maintenance_tasks_runs |
tick 毎(1秒毎) |
UPDATE maintenance_tasks_runs で tick_count (累計処理件数)や time_running (累積実行時間)を更新 |
おわりに
maintenance_tasks の機能や仕組みを紹介しました。本番作業に重宝しているため、良さが伝われば幸いです!
余談ですが、この記事を書いている途中に Active Job Continuations という機能が Active Job に追加されました(2025/5/30/this-week-in-rails より)。maintenance_tasks が内部で利用している job-iteration にインスパイアされて作られた、ジョブに中断耐性を持たせる機能のようです。この記事でも触れたジョブ実行のプラクティスが Rails コアにも影響を与えているのを見て「おおっ」と思いました。
Discussion