Task Queue用のジョブスケジューラを設計する
App Engineで日次バッチを動かす場合はCronかTask Queueを選択することになります。重複起動したくない、ジョブ失敗時はリトライしたい*1という要件があるならTask Queueを使います。Cronの場合は WEB-INF/cron.xml を編集するだけですが、Task Queueの場合はジョブスケジューラを自分で書かなければなりません。
追記: Cronの場合はスケジュール指定でsynchronizedキーワードを付けると重複起動が許容されます。@bluerabbit777jpさん、ありがとうございます。
ここでは、App Engineに限らず汎用的に使えるジョブスケジューラを書いてみます。日次ジョブスケジューラのアルゴリズムを考えてみましょう。例えば、午前3時にジョブを実行したい場合、
- 現在日時をTとする。Tは年月日時分秒ミリ秒から成る。
- Tの時、分、秒、ミリ秒を0に変更する。
- Tの時を3に変更する。
- Tが現在時刻より古い場合はTに1日を加える。
- 次のジョブ実行日時はTである。
となります。
条件が複数ある場合は、現在日時に最も近い値をジョブ実行日時とします。もし午前3時と午後3時の両方にジョブを実行したい場合は、先ほどの手順に3と15を適用して直近の日時を得ます。
日次ジョブスケジューラのアルゴリズムを実装すると以下のようになります。
/** * Scheduler for {@link DailyJob}. */ public class DailyJobScheduler implements Scheduler<DailyJob> { @Override public long getNextTime(DailyJob dailyJob, long baseTime) { if (dailyJob == null) { throw new NullPointerException("dailyJob is null"); } TimeZone timeZone; if (dailyJob.timezone().length() == 0) { timeZone = TimeZone.getDefault(); } else { timeZone = TimeZone.getTimeZone(dailyJob.timezone()); } long earliest = Long.MAX_VALUE; for (Time time : dailyJob.triggers()) { Calendar calendar = Calendar.getInstance(timeZone); calendar.setTimeInMillis(baseTime); calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); calendar.add(Calendar.HOUR_OF_DAY, time.hour()); calendar.add(Calendar.MINUTE, time.minute()); calendar.add(Calendar.SECOND, time.second()); if (calendar.getTimeInMillis() < baseTime) { calendar.add(Calendar.DAY_OF_MONTH, 1); } long candidate = calendar.getTimeInMillis(); if (candidate < earliest) { earliest = candidate; } } return earliest; } @Override public Class<DailyJob> getTargetAnnotation() { return DailyJob.class; } }
ジョブクラスには日次ジョブであることを示すアノテーションを付加します。
@DailyJob(triggers = @Time(hour = 3), timezone = "Asia/Tokyo") public class DeleteOldAlbumsController extends Controller { @Override public Navigation run() throws Exception { // バッチ処理... // 次のバッチをスケジュールする。 JobScheduler jobScheduler = new JobScheduler(); TaskHandle handle = jobScheduler.nextJob(getClass(), request.getRequestURI()); logger.info("Next job has been scheduled at " + DateUtil.toString(new Date(handle.getEtaMillis()), DateUtil.ISO_DATE_TIME_PATTERN)); return null; } }
ここではSlim3のコントローラクラスを例に出していますが何でも構いません。JobScheduler#nextJob() に渡すクラスにアノテーションが付いていればOKです。
JobScheduler#nextJob() にクラスを渡すと、アノテーションの内容に応じて実行日時が計算され、新しいタスクがスケジュールされます。実行日時の計算は SchedulerService クラスの役割です。
詳しくはgithubを参照してください。
- https://github.com/int128/lab.hidetake.org/tree/published/main/src/org/hidetake/lab/service/job
https://github.com/int128/lab.hidetake.org/tree/master/src/org/hidetake/lab/job(URLが変更になりました)
バッチの世界は地味ですが、毎日正しく動いているとうれしいですね。というか動かないと困りますね。日次ジョブが必要になったので作ってみましたが、今後、毎時ジョブが必要になったらスケジューラを追加したいと思います。
*1:Cronもリトライしてくれる旨をどこかで読んだ気がしますが、検証した限りではリトライしてくれないようです。