スケジューラーリファレンスガイド
最近のアプリケーションでは、特定のタスクを定期的に実行する必要があることがよくあります。Quarkusには、2つのスケジューラエクステンションがあります。 quarkus-scheduler
エクステンションは、APIと軽量のインメモリスケジューラー実装を提供します。 quarkus-quartz
エクステンションは、 quarkus-scheduler
エクステンションのAPIを実装し、Quartzライブラリに基づくスケジューラ実装を含んでいます。 quarkus-quartz
が必要になるのは、永続タスクやクラスタリングなど、より高度なスケジューリングのユースケースだけです。
プロジェクトに quarkus-quartz 依存関係を追加すると、 quarkus-scheduler エクステンションからの軽量スケジューラーの実装は自動的に無効になります。
|
1. スケジュールされたメソッド
@io.quarkus.scheduler.Scheduled
でアノテーションされたメソッドは、自動的に呼び出しがスケジュールされます。スケジュールされたメソッドは、abstractやprivateであってはなりません。静的または非静的であっても大丈夫です。スケジュールされたメソッドには、 @jakarta.transaction.Transactional
や @org.eclipse.microprofile.metrics.annotation.Counted
のようなインターセプターバインディングをアノテーションすることができます。
スコープを持たないBeanクラスで、 @Scheduled でアノテーションされた非静的メソッドが少なくとも一つ宣言されている場合、 @Singleton が使用されます。
|
さらに、アノテーションされたメソッドは void
を返し、パラメーターを宣言しないか、 io.quarkus.scheduler.ScheduledExecution
型のパラメーターを 1 つだけ宣言する必要があります。
アノテーションは繰り返し可能なので、1つのメソッドを複数回スケジュールすることができます。 |
1.1. メタデータの継承
サブクラスは、スーパークラスで宣言された @Scheduled
メソッドのメタデータを継承しません。
たとえば、クラス org.amce.Foo
がクラス org.amce.Bar
によって拡張されているとします。
Foo
が @Scheduled
アノテーションが付けられた非静的メソッドを宣言する場合、 Bar
はスケジュールされたメソッドのメタデータを継承しません。
次の例では、 everySecond()
メソッドは Foo
のインスタンスに対してのみ呼び出されます。
class Foo {
@Scheduled(every = "1s")
void everySecond() {
// ..do something
}
}
@Singleton
class Bar extends Foo {
}
1.2. CDI イベント
一部の CDI イベントは、特定のイベントが発生したときに同期的または非同期的に起動されます。
タイプ | イベントの説明 |
---|---|
|
スケジュールされたジョブの実行が正常に完了しました。 |
|
スケジュールされたジョブの実行が例外を伴って完了しました。 |
|
スケジュールされたジョブの実行がスキップされました。 |
|
スケジューラーが一時停止されました。 |
|
スケジューラーが再開されました。 |
|
スケジュールされたジョブが一時停止されました。 |
|
スケジュールされたジョブが再開されました。 |
1.3. トリガー
トリガーは、 @Scheduled#cron()
属性または @Scheduled#every()
属性によって定義されます。両方が指定されている場合、cron 式が優先されます。何も指定されていない場合、ビルドは IllegalStateException
で失敗します。
1.3.1. CRON
CRONトリガーは、cronライクな式で定義されています。例えば "0 15 10 * * ?"
は毎日午前10時15分に起動します。
@Scheduled(cron = "0 15 10 * * ?")
void fireAt1015AmEveryDay() { }
CRON 式で使用される構文は quarkus.scheduler.cron-type
プロパティーによって制御されます。 値は cron4j
, quartz
, unix
および spring
のいづれかです。 quartz
がデフォルトで使用されます。
cron
属性は、デフォルト値とネストされた値プロパティー式を含むProperty Expressions をサポートします。
("{property.path}" スタイルの式は引き続きサポートされますが、プロパティー式の機能がすべて提供されるわけではありません。)
@Scheduled(cron = "${myMethod.cron.expr}")
void myMethod() { }
特定のスケジュールメソッドを無効にしたい場合は、そのcron式を "off"
または "disabled"
に設定します。
myMethod.cron.expr=disabled
プロパティ式では、そのプロパティが構成されていない場合に使用されるデフォルト値を定義することができます。
0 0 15 ? * MON *
のCRON設定プロパティーの例@Scheduled(cron = "${myMethod.cron.expr:0 0 15 ? * MON *}")
void myMethod() { }
プロパティ myMethod.cron.expr
が未定義または null
の場合は、デフォルト値( 0 0 15 ? * MON *
)が使用されます。
1.3.1.1. タイムゾーン
cron式は、デフォルトのタイムゾーンのコンテキストで評価されます。しかし、cron式を特定のタイムゾーンに関連付けることも可能です。
@Scheduled(cron = "0 15 10 * * ?", timeZone = "Europe/Prague") (1)
void myMethod() { }
1 | タイムゾーンIDは、 java.time.ZoneId#of(String) を使って解析されます。 |
timeZone
属性はデフォルト値とネストされた値のプロパティー式を含む
Property Expressions をサポートします。
@Scheduled(cron = "0 15 10 * * ?", timeZone = "${myMethod.timeZone}")
void myMethod() { }
1.3.2. インターバル
インターバルトリガーは、次の呼び出しまでの期間を定義します。
期間の表現は ISO-8601 期間形式 PnDTnHnMn.nS
に基づいており、 @Scheduled#every()
の値は java.time.Duration#parse (CharSequence)
で解析されます。
ただし、式が数字で始まり、 d
で終わる場合、 P
接頭辞が自動的に追加されます。式が数字でのみ始まる場合、 PT
接頭辞が自動的に追加されます。
たとえば、 PT15M
の代わりに 15m
を使用すると、15 分として解析されます。
@Scheduled(every = "15m")
void every15Mins() { }
1 秒未満の値は、基盤となるスケジューラー実装ではサポートされていない可能性があります。その場合、ビルドおよびアプリケーションの起動時に警告メッセージが記録されます。 |
every
属性は、デフォルト値とネストされた値プロパティー式を含むProperty Expressions をサポートします。
("{property.path}" スタイルの式は引き続きサポートされますが、プロパティー式の機能がすべて提供されるわけではありません。)
@Scheduled(every = "${myMethod.every.expr}")
void myMethod() { }
Intervals は、その値を "off"
または "disabled"
に設定することで無効にすることができます。そのため、例えば、デフォルト値 "off"
のプロパティ式は、そのConfig Propertyが設定されていない場合、トリガーを無効にするために使用することができます。
@Scheduled(every = "${myMethod.every.expr:off}")
void myMethod() { }
1.4. アイデンティティ
デフォルトでは、スケジュールされた各メソッドに対して一意の ID が生成されます。このIDはログメッセージやデバッグ中に使用されます。時には、明示的な id を指定することが便利な場合もあります。
@Scheduled(identity = "myScheduledMethod")
void myMethod() { }
identity
属性は、デフォルト値とネストされた値プロパティー式を含むProperty Expressions をサポートします。
("{property.path}" スタイルの式は引き続きサポートされますが、プロパティー式の機能がすべて提供されるわけではありません。)
@Scheduled(identity = "${myMethod.identity.expr}")
void myMethod() { }
1.5. トリガーの遅延開始
@Scheduled
では、トリガーが発報を開始時刻を遅らせるための2つの方法を提供しています。
@Scheduled#delay()
と @Scheduled#delayUnit()
は初期遅延を一緒に形成します。
@Scheduled(every = "2s", delay = 2, delayUnit = TimeUnit.HOUR) (1)
void everyTwoSeconds() { }
1 | アプリケーション開始から2時間後に初めてトリガーが発射されます。 |
最終的な値は常に1秒単位で丸められます。 |
@Scheduled#delayed()
は、上記のプロパティーの代替テキストです。
期間の表現は ISO-8601 の期間形式 PnDTnHnMn.nS
に基づいており、値は java.time.Duration#parse (CharSequence)
で解析されます。
ただし、式が数字で始まり、 d
で終わる場合、 P
の接頭辞が自動的に追加されます。式が数字だけで始まる場合、 PT
接頭辞が自動的に追加されます。
たとえば、 PT15S
の代わりに 15s
を使用すると、15 秒として解析されます。
@Scheduled(every = "2s", delayed = "2h")
void everyTwoSeconds() { }
@Scheduled#delay() がゼロよりも大きい値に設定されている場合、 @Scheduled#delayed() の値は無視されます。
|
@Scheduled#delay()
と比較した場合の主な利点は、値が設定可能であることです。
delay
属性は、デフォルト値やネストされたプロパティ式を含む Property Expressions をサポートしています。(なお、 "{property.path}"
スタイルの表現はサポートされていますが、プロパティ式の完全な機能は提供されていません)。
@Scheduled(every = "2s", delayed = "${myMethod.delay.expr}") (1)
void everyTwoSeconds() { }
1 | 遅延の設定には、configプロパティー myMethod.delay.expr を使用します。 |
1.6. 遅延実行
@Scheduled#executionMaxDelay()
を設定すると、スケジュールされたメソッドの各実行を遅延できます。
この値は、トリガーのアクティブ化とスケジュールされたメソッドの実行との間の最大遅延を表します。
実際の遅延は、0 から指定された最大遅延までの間でランダムに生成された数値です。
値は DurationConverter#parseDuration (String)
で解析されます。
これはプロパティー式である可能性があり、その場合、スケジューラーは代わりに設定された値 (@Scheduled (executionMaxDelay = "${myJob.maxDelay}")
) を使用しようとします。
さらに、プロパティー式ではデフォルト値
(@Scheduled (実行最大遅延 = "${myJob.maxDelay}:500ms}")
) を指定できます。
@Scheduled(every = "2s", executionMaxDelay = "500ms") (1)
void everyTwoSeconds() { }
1 | 遅延の値は 0 - 500 ミリ秒です。その結果、 everyTwoSeconds() 実行間隔は、およそ 1.5 - 2.5 秒です。 |
1.7. 同時実行
デフォルトでは、スケジュールされたメソッドは同時に実行することができます。それにもかかわらず、 @Scheduled#concurrentExecution()
を通じて同時実行を処理するための戦略を指定することが可能です。
import static io.quarkus.scheduler.Scheduled.ConcurrentExecution.SKIP;
@Scheduled(every = "1s", concurrentExecution = SKIP) (1)
void nonConcurrent() {
// we can be sure that this method is never executed concurrently
}
1 | 同時実行はスキップされます。 |
スケジュールされたメソッドの実行がスキップされると、 io.quarkus.scheduler.SkippedExecution のタイプのCDIイベントが発生します。
|
同じアプリケーションインスタンス内での実行のみが考慮されることに注意してください。この機能は、クラスター間での動作を意図したものではありません。 |
1.8. 条件付き実行
@Scheduled#skipExecutionIf()
を使用して、スケジュールされたメソッドの実行をスキップするロジックを定義できます。
指定されたクラスは io.quarkus.scheduler.Scheduled.SkipPredicate
を実装する必要があり、 test()
メソッドの結果が true
の場合、実行はスキップされます。
クラスは CDI Bean を表すか、引数なしのパブリックコンストラクターを宣言する必要があります。
CDI の場合、Bean タイプのセット内に指定されたクラスを持つ Bean が 1 つだけ存在する必要があります。そうでない場合、ビルドは失敗します。
さらに、ジョブの実行中は Bean のスコープがアクティブである必要があります。
スコープが @Dependent
の場合、Bean インスタンスは特定のスケジュールメソッドだけに属し、アプリケーションがシャットダウンされると破棄されます。
class Jobs {
@Scheduled(every = "1s", skipExecutionIf = MyPredicate.class) (1)
void everySecond() {
// do something every second...
}
}
@Singleton (2)
class MyPredicate implements SkipPredicate {
@Inject
MyService service;
boolean test(ScheduledExecution execution) {
return !service.isStarted(); (3)
}
}
1 | MyPredicate.class の Bean インスタンスは、実行をスキップすべきかどうかを評価するために使用されます。Beanタイプのセットに指定されたクラスを持つBeanが正確に1つ存在する必要があり、そうでない場合はビルドが失敗します。 |
2 | Beanのスコープは、実行中を通じてアクティブです。 |
3 | Jobs.everySecond() は、 MyService.isStarted() が true を返すまでスキップされます。 |
なお、これは以下のコードと同等のものです:
class Jobs {
@Inject
MyService service;
@Scheduled(every = "1s")
void everySecond() {
if (service.isStarted()) {
// do something every second...
}
}
}
主なアイデアは、スケジュールされたビジネスメソッドの外で実行をスキップするロジックを維持することで、再利用やリファクタリングが容易にできるようにすることです。
スケジュールされたメソッドの実行がスキップされると、 io.quarkus.scheduler.SkippedExecution のタイプのCDIイベントが発生します。
|
アプリケーションの起動/シャットダウン中にスケジュールされた実行をスキップするには、 io.quarkus.scheduler.Scheduled.ApplicationNotRunning のスキップ条件を使用できます。
|
1.9. ノンブロッキング方式
デフォルトでは、スケジュールされたメソッドは、ブロックするタスクのメインエクゼキュータ上で実行されます。
そのため、Vert.xイベントループ上で実行するように設計された技術(Hibernate Reactiveなど)は、メソッド本体内で使用することができません。
このため、 java.util.concurrent.CompletionStage<Void>
または io.smallrye.mutiny.Uni<Void>
を返す、あるいは @io.smallrye.common.annotation.NonBlocking
でアノテーションされたスケジュール型メソッドは、代わりに Vert.x のイベントループで実行されます。
class Jobs {
@Scheduled(every = "1s")
Uni<Void> everySecond() { (1)
// ...do something async
}
}
1 | リターンタイプ Uni<Void> は、Vert.xのイベントループでメソッドを実行するようにスケジューラに指示します。 |
1.10. 複数のスケジューラー実装を使用する方法
場合によっては、スケジュールされたメソッドを実行するために使用されるスケジューラー実装を選択すると便利な場合があります。
ただし、デフォルトでは、すべてのスケジュールされたメソッドに対して 1 つの Scheduler
実装のみが使用されます。
たとえば、 quarkus-quartz
エクステンションはクラスタリングをサポートするスケジューラ実装を提供しますが、その代わりにシンプルなインメモリー実装は使用できなくなります。
そのため、クラスタリングが有効になっている場合、単一のノードでローカルに実行されるスケジュールされたメソッドを定義できません。
ただし、 quarkus.scheduler.use-composite-scheduler
設定プロパティーを true
に設定すると、代わりに複合 Scheduler
が使用されます。
つまり、複数のスケジューラー実装が並行して実行されます。
さらに、 @Scheduled#executeWith()
を使用して、スケジュールされたメソッドの実行に使用される特定の実装を選択することもできます。
class Jobs {
@Scheduled(cron = "0 15 10 * * ?") (1)
void fireAt10AmEveryDay() { }
@Scheduled(every = "1s", executeWith = Scheduled.SIMPLE) (2)
void everySecond() { }
}
1 | quarkus-quartz エクステンションが存在する場合、このメソッドは Quartz 固有のスケジューラーで実行されます。 |
2 | quarkus.scheduler.use-composite-scheduler=true が設定されている場合、このメソッドは quarkus-scheduler エクステンションによって提供される単純なインメモリー実装を使用して実行されます。 |
2. スケジューラー
Quarkusは、 io.quarkus.scheduler.Scheduler
型のビルトインBeanを提供しており、これを注入してスケジューラーを一時停止/再開するために使用することができます。
import io.quarkus.scheduler.Scheduler;
class MyService {
@Inject
Scheduler scheduler;
void ping() {
scheduler.pause(); (1)
scheduler.pause("myIdentity"); (2)
if (scheduler.isRunning()) {
throw new IllegalStateException("This should never happen!");
}
scheduler.resume("myIdentity"); (3)
scheduler.resume(); (4)
scheduler.getScheduledJobs(); (5)
Trigger jobTrigger = scheduler.getScheduledJob("myIdentity"); (6)
if (jobTrigger != null && jobTrigger.isOverdue()){ (7)
// the job is late to the party.
}
}
}
1 | すべてのトリガーを一時停止します。 |
2 | スケジュールされた特定のメソッドをそのIDで一時停止する |
3 | スケジュールされた特定のメソッドを、そのIDで再開する |
4 | スケジューラーを再開します。 |
5 | スケジューラーを再開します。 |
6 | スケジュールされた特定のメソッドを、そのIDで再開する。 |
7 | isOverdue()の猶予期間は、quarkus.scheduler.overdue-grace-periodを使用して設定することができます |
CDIイベントは、スケジューラーやスケジュールされたジョブが一時停止/再開されたときに、同期および非同期で発生します。ペイロードはそれぞれ io.quarkus.scheduler.SchedulerPaused , io.quarkus.scheduler.SchedulerResumed , io.quarkus.scheduler.ScheduledJobPaused , io.quarkus.scheduler.ScheduledJobResumed です。
|
3. 長時間タスクのスケジューリング
長時間実行されるタスクを実行すると、次のような警告メッセージが表示されることがあります:
WARN [io.ver.cor.imp.BlockedThreadChecker] (vertx-blocked-thread-checker) Thread Thread[vert.x-worker-thread-1,5,main] has been blocked for 81879 ms, time limit is 60000 ms: io.vertx.core.VertxException: Thread blocked
これは、デフォルトのワーカースレッドプールがVert.xから来たもので、スレッドがあまりにも長くブロックされるのを防ぐためです。
Vert.x ワーカースレッドをブロックできる時間も 設定可能 です。 |
したがって、長いタスクを実行する適切な方法は、スケジュールメソッドからカスタム実行サービスにオフロードすることです。以下は、頻繁に実行されるとは思っていない長時間タスクのセットアップの例です:
@ApplicationScoped
public class LongRunner implements Runnable {
private ExecutorService executorService;
@PostConstruct
void init() {
executorService = Executors.newThreadPerTaskExecutor(Executors.defaultThreadFactory()); (1)
}
@PreDestroy
void destroy() {
executorService.shutdown(); (2)
}
@Scheduled(cron = "{my.schedule}")
public void update() {
executorService.execute(this); (3)
}
@Override
public void run() { (4)
// perform the actual task here
}
}
1 | フィットするエクゼキューターを作成します。この場合、スケジュールされたタスクごとに新しいスレッドが作成され、タスクが終了すると停止します。 |
2 | @PreDestroy コールバックは、executorサービスをシャットダウンするために使用されます。 |
3 | スケジューリングされたメソッドは、ジョブをカスタムエグゼキュータに委譲するだけです。これにより、Vert.xスレッドがブロックされるのを防ぎます。 |
4 | このBeanは Runnable を実装しています。これは、エグゼキュータ・サービスにパラメータとして直接渡せるフォーマットです。 |
4. プログラムスケジューリング
注入された io.quarkus.scheduler.Scheduler
は、プログラム的にジョブをスケジュールするために使用することもできます。
import io.quarkus.scheduler.Scheduler;
@ApplicationScoped
class MyJobs {
@Inject
Scheduler scheduler;
void addMyJob() { (1)
scheduler.newJob("myJob")
.setCron("0/5 * * * * ?")
.setTask(executionContext -> { (2)
// do something important every 5 seconds
})
.schedule(); (3)
}
void removeMyJob() {
scheduler.unscheduleJob("myJob"); (4)
}
}
1 | これは、 @Scheduled(identity = "myJob", cron = "0/5 * * * * ?") でアノテーションされたメソッドに代わるプログラム的な代替です。 |
2 | ビジネスロジックはコールバックで定義されます。 |
3 | JobDefinition#schedule() メソッドが呼び出されると、ジョブがスケジュールされます。 |
4 | プログラムによって追加されたジョブも削除することができます。 |
デフォルトでは、 @Scheduled ビジネスメソッドが見つからない限り、スケジューラは開始されません。「純粋な」プログラムによるスケジューリングでは、 quarkus.scheduler.start-mode=forced を使ってスケジューラを強制的に起動させる必要があるかもしれません。
|
Quartz エクステンション が存在し、DB ストアタイプが使用されている場合、タスクインスタンスをジョブ定義に渡すことはできず、代わりにタスククラスを使用する必要があります。Quartz API を使用して、プログラムでジョブをスケジュールすることもできます。 |
場合によっては、詳細にわたるアプローチが必要になることがあります。そのため、Quarkus は CDI Bean として注入できる java.util.concurrent.ScheduledExecutorService
と java.util.concurrent.ExecutorService
も公開しています。
ただし、これらのエグゼキューターは他の Quarkus エクステンションによって使用されるため、注意して使用する必要があります。さらに、ユーザーがこれらのエグゼキューターを手動でシャットダウンすることは許可されていません。
class JobScheduler {
@Inject
ScheduledExecutorService executor;
void everySecondWithDelay() {
Runnable myRunnable = createMyRunnable();
executor.scheduleAtFixedRate(myRunnable, 3, 1, TimeUnit.SECONDS);
}
}
5. スケジュールされたメソッドとテスト
テストを実行するときは、スケジューラーを無効にすることが望ましい場合がよくあります。
スケジューラーは、ランタイム設定プロパティー quarkus.scheduler.enabled
を通じて無効にできます。
false
に設定すると、アプリケーションにスケジュールされたメソッドが含まれていても、スケジューラーは開始されません。
特定のスケジューラー (Test Profiles) を無効にすることもできます。
6. メトリクス
quarkus.scheduler.metrics.enabled
が true
に設定されていて、metrics エクステンションが存在する場合、いくつかの基本的なメトリクスがすぐに公開されます。
Micrometer エクステンション が存在する場合、すべての @Scheduled
メソッドに @io.micrometer.core.annotation.Timed
インターセプター・バインディングが自動的に追加され (すでに存在する場合を除く)、 scheduled.methods
という名前の io.micrometer.core.instrument.Timer
と scheduled.methods.running
という名前の io.micrometer.core.instrument.LongTaskTimer
が登録されます。宣言するクラスの修飾子名と @Scheduled
メソッド名がタグとして使用されます。
SmallRye Metrics エクステンションが存在する場合、 @org.eclipse.microprofile.metrics.annotation.Timed
インターセプターバインディングがすべての @Scheduled
メソッドに自動的に追加され(すでに存在している場合を除く)、 org.eclipse.microprofile.metrics.Timer
が各 @Scheduled
メソッドに対して作成されます。名前は、宣言したクラスの完全修飾名と @Scheduled
メソッドの名前で構成されます。タイマーには、タグ scheduled=true
が付いています。
7. OpenTelemetry トレース
quarkus.scheduler.tracing.enabled
を true
に設定し、OpenTelemetry エクステンション が存在する場合、 @Scheduled
アノテーションで定義されているか、プログラムでスケジュールされているかに関係なく、ジョブの アイデンティティ をもとにした名前でスパンが自動的に作成されます。
8. 仮想スレッド上で @Scheduled メソッドを実行
@Scheduled
でアノテーションされたメソッドは、 @RunOnVirtualThread
でアノテーションすることもできます。
この場合、メソッドは仮想スレッド上で呼び出されます。
メソッドは void
を返す必要があり、Java ランタイムは仮想スレッドのサポートを提供する必要があります。
詳細については、 仮想スレッドガイド をお読みください。
9. 設定リファレンス
ビルド時に固定された設定プロパティー。その他の設定プロパティーは、すべて実行時にオーバーライド可能です。
Configuration property |
型 |
デフォルト |
---|---|---|
The syntax used in CRON expressions. Environment variable: Show more |
|
|
Scheduled task metrics will be enabled if a metrics extension is present and this value is true. Environment variable: Show more |
boolean |
|
Controls whether tracing is enabled. If set to true and the OpenTelemetry extension is present, tracing will be enabled, creating automatic Spans for each scheduled task. Environment variable: Show more |
boolean |
|
By default, only one Scheduler implementations will be started depending on the value of Environment variable: Show more |
boolean |
|
If schedulers are enabled. Environment variable: Show more |
boolean |
|
Scheduled task will be flagged as overdue if next execution time is exceeded by this period. Environment variable: Show more |
|
|
Scheduler can be started in different modes. By default, the scheduler is not started unless a Environment variable: Show more |
|
|
期間フォーマットについて
期間の値を書くには、標準の 数字で始まる簡略化した書式を使うこともできます:
その他の場合は、簡略化されたフォーマットが解析のために
|