ActiveRecordとdry-operationでバッチジョブをお手軽に管理してみる(2)

前回 ActiveRecordとdry-operationでバッチジョブを管理してみる(1) (2025-08-23) | あーありがち の続き。

今回は dry-operation を利用して実際のジョブの内容をどのように記述していくか、何がどのように記録されてどのように「再開」を支援するか。

ジョブの中身の定義と失敗をdry-operationで扱う

最近お気に入りの dry-operation でジョブの中身の定義を行うことにした。dry-operation については以下で紹介しているが、

要は

  • Operation の call メソッド内の記述のルールを守れば、自分で例外の定義をしなくても強制的に中断、脱出することができる(内部で独自に例外を使って脱出している)
  • これを利用しつつ、ブロック内が成功裡に終わったら現在の sequence を記録するための独自のブロックを用意、これを proceed! メソッドで実現する1

を最初に考え、特に後者については具体的にどのようにオブジェクトを組み立てて実現するかはあとで決めることとした2

イメージとしては以下のような感じで、

class SomeOperation < Dry::Operation
  def call(*args, **kwargs)
    proceed! do
      step ..
    end

    proceed! do
      step ..
    end
end

この proceed! の実体は

  • ジョブの進行の定義内に収まっていたらブロックの中身を実行する
  • ブロックの中身の実行が成功したら BatchJobExecution を通じて BatchJobEvent を create する

ことになる。proceed! で block を受け取って、それを実際に call し、結果に応じて処理を分ける。ざくっとしたイメージを描くと以下のようになる。3

ActiveRecord を含む全体的な書き方のコードのイメージはこんな感じ。

class SomeJob < BatchJob
  sequence = [:abc, :def]
end

class SomeOperation < BatchJobOperation
  def call(*args, **kwargs)
    proceed! do
      step ..
    end

    proceed! do
      step ..
    end

    proceed! do # 上で completed になっているはずなので、絶対失敗する
      step ..
  end
end

役割は改めて挙げると、

  1. Job 定義用のクラスに sequence をセット
  2. dry-operation が step 記法でマジカルにエラー処理してくれるので、記録したい event の単位で proceed! にブロックを与えてその中に step を記述する
    • すると SomeJob で定義済みの sequence を見ながら順次処理を進めてくれる
    • 失敗したら成功のイベントとしての記録を中止して Failure を返す(もちろん step の中の書き方による)

実際にはもう少し考えることがあって、

  • 最初の実行のきっかけをどう与えてどう結果(おそらく JobExecution)を受け取るか
  • 排他制御をするか、するならどのようにそれを実現するか
  • Operation と ActiveRecord の組み合わせに上記の排他制御をどう組み合わせるのか

も考えなければいけないが、これは(コードの書き味やテスタビリティなど考えるべきこともあるが)後回しでよいと思う。

途中の処理をskipできるようにして「再開」を実現する

これが自分的にはすごく欲しい機能。

ここまでで、Thread やイベント駆動の何かを一切使っておらず、バックエンドで処理するようにしていないので、実際にバッチジョブが終わったかどうかはジョブを起動するメソッドから戻り値を受け取る瞬間に分かる。

これを最大限利用してシンプルに考えると、

  • 仮に排他制御がうまく機能している場合は実行中に他のプロセスなどが同じ処理に同時に介入できない
  • 実行できるプロセスは前回の実行結果を、最新の Event の情報をもとに判別できる
  • もし completed が記録されていなければ失敗しており、どこまで成功したかが Event に残っている
  • この「前回の記録」をもとに再度実行すれば、「途中まで成功」している proceed! ブロックでは「実際の処理」を skip できる
    • proceed! ブロックの skip もまた「成功」である

を満たせば管理側の機能としては「再開」が可能になるはず。記録されるイベントとしては以下のようなイメージになる。

execution_idnamemessage(補足)
1invoked  
1abc ← abcまで成功して、defで失敗
2invoked  
2abcskipped← 成功しているabcを自動的にskipし、それを記録
2def  
2completed  

この機能は特にバッチジョブの開発初期の段階で重宝すると思う。 これがないと

  1. 明示的に途中から実行する機能を用意
  2. 実行結果からどこまで成功しているか判別
  3. 1, 2 をもとにどこから実行するかを人間が決めて手動で1の機能を利用

する必要がある。

1 はまだいい。そんなに難しくない(手間は掛かる)。問題は 2 で、これをシンプルなログだけで解決するのはなかなか大変。長い時間掛かるバッチジョブの実行の様子、成否の境目など、素朴にログに出力はできる。しかしそのログは他の情報と紛れ、アプリ側から利用するのはほぼ無理である(ログは出力するものであってそれを入力に利用できる設計になっているケースはそうない)。だからアプリ側でどこまで進んだかを残し、かつプログラムコード内でいちいち繊細な記述をせずに済むようにと考えていた。

「成功したイベントが記録されている」のが達成できているなら、あとは成功済みのイベントに該当するかどうかの判定。これも難しくない。

  1. 前回の実行の最終イベントを取得(実行時に取得して保持しておけばよい)
  2. 「定義済みのsequence配列」に対して「最後に記録されたイベント」が何番目に相当するか
  3. index を +1 したら次はなんのイベントか
  4. 3 が 1 を超えるまでは無条件に成功として記録し、proceed! の中身を無視する

これでよい。

続きは ActiveRecordとdry-operationでバッチジョブを管理してみる(3) (2025-08-27) | あーありがち へ。

  1. proceed! ブロック内でなんらかの失敗があった場合、その直前の proceed! までの成功が job_events として記録されている状態 

  2. この記法も近いものは思いついていたが、実際に書いて動かしながら何回かバージョンアップしながら練ってこの形になっている。 

  3. 実際にはエラー処理は当初考えていなかった。ただ、付け加えるのはまったく難しくないと考えていた。 

More