Quartzでのジョブ重複起動の抑止方法

バッチ処理に欠かすことのできないジョブスケジューラ。特にJavaの場合は,JavaVMというプロセス自体が重厚なために,個々のバッチプログラムをそれぞれJavaVMプロセス起動で実行することは,バッチプログラムの本数が多くなればなるほど非現実的になる。そのために,一つのJavaVM上でのジョブスケジューリングが基本となり,つまり各バッチ処理をプロセスとしてではなく,スレッドとして実行する基盤が欲しくなってくる。 スレッドを使用したことのある開発者であれば,必要最低限なジョブスケジューリングの基盤を実装することはそう難しいことではないだろう。しかし,バッチ間の協調が求められたり,スケジューリングが複雑になったりする場合には,やはりそれなりに機能を持つ基盤をネットで探して利用したくなるだろう。

OpenSymphonyが提供している Quartzは,そんな状況にもってこいのジョブスケジューラ向けOSSライブラリである。 Quartzの場合,スケジューリングされるジョブは,Jobインタフェースを実装したオブジェクトである。Jobインタフェースを実装したクラスを作成し,そのクラスオブジェクトをSchedulerオブジェクトに登録する。正確には,JobDetailオブジェクトにJobクラスオブジェクトを持たせて,JobDetailオブジェクトをTriggerオブジェクト(ジョブの起動タイミングを決定する)と共にSchedulerオブジェクトに登録することで,ジョブがスケジューリングされる。ジョブの起動の度に,Jobクラスオブジェクトを元にそのインスタンスが生成される。そしてJobインスタンスのexecute()メソッドが呼び出されて,バッチ処理が実行されるという仕組みである。 もちろん「毎分起動」といった設定が可能なので,繰り返しジョブを実行することが可能である。しかし,既にジョブが実行されている際に同一のジョブの重複起動は行いたくない,といったことをしたくても,QuartzのAPIからは見つけることができない。 重複起動を阻止するためには,TriggerListenerというものをうまく使うことで実現可能になる。TriggerListenerインタフェースは,ジョブの起動スケジュールを司るTriggerオブジェクトの動作の結果生じる各種イベントに対応して,何らかの処理を行いたい場合に使用するイベントリスナーだ。 TriggerListenerオブジェクトの登録は,2種類の登録先が用意されている。

  • 各Triggerオブジェクト毎

  • グローバル グローバルとは,Schedulerに登録された全てのTriggerオブジェクトに関して,一括で扱いたい場合に使用する。つまり,1つのTriggerListenerオブジェクトで,全てのイベント通知を受けることができる。下のコードは,重複起動を阻止する処理が実装されたTriggerListenerクラスである。ちなみに,重複起動に関係のないメソッドは,省略している。

public class TriggerListenerImpl implements TriggerListener {   private Set currentRunningJobSet;   public TriggerListenerImpl() {     currentRunningJobSet = new HashSet ();   }   ・・・   public boolean vetoJobExecution(       Trigger trigger, JobExecutionContext context) {     String name = context.getJobDetail().getName();     if (currentRunningJobSet.contains(name)) {       return true;     } else {       currentRunningJobSet.add(name);       return false;     }   }   public void triggerComplete(Trigger trigger,       JobExecutionContext context, int instructionCode) {     String name = context.getJobDetail().getName();     currentRunningJobSet.remove(name);   } }

仕組み的には,起動したジョブの名前をコレクションに保持しておいて,Triggerオブジェクトによるジョブ起動の度にジョブ名がコレクション内に存在するかどうかチェック,その結果含まれていた場合は実行中と判断してジョブの実行を取りやめる,という内容である。実行中のジョブ名を保持しておくコレクションはコンストラクタに生成処理を記述する。ジョブの起動を行っていいかどうかを判断するためのvetoJobExecution()メソッドがジョブ起動の直前に呼び出されるので,その中でジョブ名の存在チェックを行い,存在していればtrueを返却してジョブの実行を抑止する。逆に存在していなければfalseを返却して,ジョブの起動を継続する。ジョブの実行完了時にはtriggerComplete()メソッドが呼び出されるので,コレクションからジョブ名を削除する。 このTriggerListenerImplオブジェクトは,下記のようにしてSchedulerに登録する。

Scheduler scheduler = …; scheduler.addGlobalTriggerListener(new TriggerListenerImpl());

もちろん各Triggerオブジェクト毎にTriggerListenerオブジェクトを登録する方が,TriggerListenerインタフェースの実装クラスの処理内容が簡略化される(Job:Trigger=1:1の場合)。相手が決まっているので,boolean値だけ上げ下げしてあげれば重複起動を抑止できるだろう。しかし,ジョブ定義をXMLファイルで記述し,JobSchedulingDataProcessorクラスを使ってSchedulerオブジェクトに一括登録する場合などは,上記のコードの方が簡潔な実装と言えるだろう。 TriggerListenerという仕組みを通じて,やはりライブラリでもIDEでもフレームワークでも何でも,コールバックの仕組みとその種類の多さ,コールバック時に行える処理の豊富さが,採用頻度を上げるための必須要素だなと改めて認識させてくれた。特に,用意されているコールバックメソッドのセンス,これが重要。QuartzのvetoJobExecution()メソッド,これは久々に僕をビリッと感じさせてくれたメソッドだった。

このエントリーをはてなブックマークに追加

関連記事

2023年のRemap

Remapにファームウェアビルド機能を追加しました

Google I/O 2023でのウェブ関連のトピック

2022年を振り返って

現在のRemapと今後のRemapについて