Quartz Scheduler のジョブからバックグラウンドタスクを実行する際、
Spring の ThreadPoolTaskExecutor
に処理を移乗するようにしました。 1
その際、以下のような挙動を実現したかったが、どう設定すれば思った通りの設定になるのかよくわからず、いろいろ調べた内容をまとめておきます。
実現したい挙動
アプリを停止する際に、以下のようにしたい。
- 新規のタスクは受付停止する
- すでに起動中のタスクは、そのまま完了するまで待つ
- 無限に待つのも良くないので、タイムアウトを設定する
ThreadPoolTaskExecutorの停止関連の設定
調べたところ、以下の設定が停止時の挙動に関連している事が判明。
strictEarlyShutdown
acceptTasksAfterContextClose
waitForTasksToCompleteOnShutdown
awaitTerminationMillis
JavaDocを読んでもよく挙動の違いがわかんないため、ソースからいろいろ調べてみた。
三種類の停止モード
実装を見たところ、どうやら三種類の「停止モード」(JavaDocではそのような呼び方はしてないため、便宜上の呼び方)がある模様。 この停止モードの違いにより、新規タスクを受け付けるタイミング、新規タスクの実行を抑制するタイミングなどが異なるようです。
default
earlyShutdown
lateShutdown
タイミング | default | earlyShutdown | lateShutdown |
---|---|---|---|
ContextClose | 制御なし | 新規タスク受付停止 | 制御なし |
LifecycleStop | 新規タスク受付停止, 新規タスク実行抑制, 実行中タスクの完了待ち | 新規タスク実行抑制, 実行中タスクの完了待ち | 制御なし |
BeanDestroy | 実行中タスクの強制停止, 完了待ち | 実行中タスクの強制停止, 完了待ち | 新規タスク受付停止, 新規タスク実行抑制, (実行中タスクの強制停止), 完了待ち |
基本的に lateShutdown
以外は、ライフサイクルの停止処理時には新規タスクの実行を抑制して、実行中タスクの完了待ちをする。 default
と earlyShutdown
の違いは、新規タスクの受付(submit
) をいつまで許容するかで、 earlyShutdown
のほうがタイミングが早い。
lateShutdown
の場合は、ライフサイクルとか関係なく、停止する直前までなるべくタスク受付して実行しようと頑張る感じ。「実行中タスクの強制停止」を括弧書きにしたのは、ここが設定によって挙動が変わるから。
また、default
earlyShutdown
の場合は LifecycleStop
で完了待ちするから、BeanDestroy
で強制停止なんてしないのでは?という感じもあるが、多分 LifecycleStop
の完了待ちにはタイムアウトがありそう。このタイムアウト時間を超えて停止できないと処理が先に進んで、最終的に BeanDestroy
で強制停止という形になるように見えた。
各設定値の効果
strictEarlyShutdown
earlyShutdown
モードにする。
ただし、acceptTasksAfterContextClose
または waitForTasksToCompleteOnShutdown
が設定されていると無視されて、強制的に lateShutdown
モードに切り替わる。
acceptTasksAfterContextClose
lateShutdown
モードにする。
前述の通り、strictEarlyShutdown
よりも優先される。
また、BeanDestroy
時に実行中タスクを強制停止する。
waitForTasksToCompleteOnShutdown
lateShutdown
モードにする。
前述の通り、strictEarlyShutdown
よりも優先される。
acceptTasksAfterContextClose
とは違い、BeanDestroy
時に実行中タスクを強制停止しない。
awaitTerminationMillis
BeanDestroy
時の、実行中タスク完了までの待機時間。
デフォルトでは待機しない。
強制停止したら待機もなにもないのでは?と思うかもしれませんが、処理が動いているスレッドを止めるのはそんなに簡単じゃないのです。
基本的に外部からはそのスレッドに割り込むしかできませんが、
スレッド側の処理が (Thread.sleep()
などの) 割り込みを受け付ける処理で待機しているか、
明示的に割り込みがないかを確認して自発的に止めない限り、処理は止まらない仕組みです。
なので、強制停止という名の割り込みをかけても止まる保証はないので、ちゃんと止まるまで待つみたいなことを考える必要があるわけです。
デフォルトは待機しないなので、例えば実行中のタスクの最後でDBにステータス更新するような処理が合った場合も、アプリが終了しちゃって更新されないままになります。無慈悲です。
実現したい挙動を実現するには
基本的にはデフォルトのままで良い(というか、良かれと思って waitForTasksToCompleteOnShutdown
とか設定すると思わぬ挙動になる) ということがわかりました。
また、追加で以下のようなカスタムが必要ということもわかりました。
ThreadPoolTaskExecutor
のphase
を、 QuartzのSchedulerFactoryBean
よりも小さい値にする- 停止時に Quartz Scheduler が停止してから
ThreadPoolTaskExecutor
を停止しないと、新規タスクを受付できなくなるため (ここで良かれと思ってacceptTasksAfterContextClose
を設定したりすると思わぬ挙動になる) - 実行されることよりも、変なログが出ないようにしたいという気持ち
- 停止時に Quartz Scheduler が停止してから
- LifecycleStop時のタイムアウトを調整する
参考資料
Footnotes
-
Quartz SchedulerのMisfireあたりの挙動がいろいろ難しく、ジョブが滞留した場合とか、アプリを上げ下げしたときとか、なかなか思ったとおりに制御しづらい感じになりました。 できないことはないけど、Quartzのナイーブな仕様に乗っかった実装をするのはリスクがあるため、自分でスレッドプールを使って制御したほうが楽そうという判断になりました。 ↩