mike-neckのブログ

Java or Groovy or Swift or Golang

Spring の Scheduling Task で柔軟にタスクの実行開始時間を指定する

f:id:mike_neck:20180802004547p:plain

現在作っているもので、タスクの開始時間を柔軟に行えないか調べていたら、普通に Spring のドキュメントに書いてあり、実験の結果をメモした。

docs.spring.io

なお、ここに書いてることは多くのケースでは使わないし普通のスケジューリングで十分だと思っている。また、そうでなくても Spring Retry でまかなえる可能性が高いので、普通には覚える必要はないかと思われる。もし使う必要があるのなら設計を疑ったほうがいい(鏡)。


Spring (3.0 から) では TaskScheduler というインターフェースが提供されていて、これに具体的な日時と Runnable を指定してタスクを実行させられる。この TaskScheduler のメソッドに次のようなメソッドがある。

ScheduledFuture schedule(Runnable task, Trigger trigger)

これは Trigger というオブジェクトによって、実行開始時間を計算しながらタスクのスケジューリングを行うメソッドとなっている。

Trigger は次のようなインターフェースで、これとタスク(Runnable)を TaskScheduler にわたすとタスクのスケジューリングが登録できる。

interface Trigger {
    Date nextExecutionTime(TriggerContext trigggerContext);
}

Trigger には CronTriggerPeriodicTrigger があり、 @Scheduled アノテーションでのクーロン指定、およびピリオド指定にそれぞれ対応している。ここではカスタムのスケジューリングを行いたいので、 Trigger タスクを実装してみる


例えば次のようなスケジューリングを行いたいとする

  1. 前回のタスクが 5 分よりもかかった場合は、次のタスクは 2 分後に開始する
  2. 前回のタスクが 2 分よりもかかった場合は、次のタスクは 3 分後に開始する
  3. 前回のタスクが 1 分よりもかかった場合は、次のタスクは 6 分後に開始する
  4. 前回のタスクが 1 分以下だった場合は、次のタスクは 8 分後に開始する
  5. 初回起動の場合は、次のタスクは 1 分後に開始する

これを満たす Trigger を記述すると次のようになる

object MyTrigger: Trigger {
  override fun nextExecutionTime(triggerContext: TriggerContext): Date =
      triggerContext.lastActualExecutionTime().with { triggerContext.lastCompletionTime() }
          ?.map { start, end -> PreviousTask(end.toInstant(),duration(start, end)) }
          ?.determineConditionally<PreviousTask, Instant>()
          ?.on { FIVE_MIN < it.timeElapsed }?.then { it.finishedAt + TWO_MIN }
          ?.on { TWO_MIN < it.timeElapsed }?.then { it.finishedAt + THREE_MIN }
          ?.on { ONE_MIN < it.timeElapsed }?.then { it.finishedAt + SIX_MIN }
          ?.others { it.finishedAt + EIGHT_MIN }
          ?.date ?: (Instant.now() + ONE_MIN).date

  private val FIVE_MIN: Duration = Duration.ofSeconds(5 * DurableTask.lengthUnit)
  private val TWO_MIN: Duration = Duration.ofSeconds(2 * DurableTask.lengthUnit)
  private val ONE_MIN: Duration = Duration.ofSeconds(1 * DurableTask.lengthUnit)
  // 以下省略
}

簡単に言えば、次にタスクを起動する時刻を表す Date オブジェクトを返せば良い

これを用いて タスクをスケジュールするには次のようなコードを実行する

@Component
class Task(private val scheduler: TaskScheduler) {
  override fun run(args: Array<String>) {
    scheduler.schedule(Runnable {
      heavyTask()
    }, MyTrigger)
  }
}

実際お実験コードはこちら

gist.github.com