タスクスケジューリングガイド
Guren はアプリケーション内でスケジュールタスクを定義するための Fluent API を提供します。複数のcronエントリを管理する代わりに、コード内でタスクスケジュール全体を定義できます。
タスクスケジューリングガイド
Guren はアプリケーション内でスケジュールタスクを定義するための Fluent API を提供します。複数のcronエントリを管理する代わりに、コード内でタスクスケジュール全体を定義できます。
推奨パターン: @guren/core から scheduling API をインポートし、スケジュールを一箇所で登録します。各機能のコードでは、実行されるジョブやコマンドの実装に集中します。
コアコンセプト
- Scheduler – スケジュールされたタスクを管理し、適切なタイミングで実行する。
- Schedule – Fluent API でタスクを定義するビルダー。
- ScheduledTask – スケジュールと設定を持つ個別のタスク。
- Cron Expression – タスクの実行タイミングを定義する標準的なcron構文。
基本的な使い方
クイックスタート
import { Scheduler } from '@guren/core'
const scheduler = new Scheduler()
scheduler.schedule((schedule) => {
// 毎日午前3時にコールバックを実行
schedule.call(async () => {
await cleanupOldSessions()
}).daily().at('03:00').name('cleanup-sessions')
})
scheduler.start()
スケジューラーの実行
アプリケーションのブートストラップでスケジューラーを起動します。
// app.ts
import { Scheduler } from '@guren/core'
const scheduler = new Scheduler({
timezone: 'Asia/Tokyo',
checkInterval: 60000, // 60秒ごとにチェック
logger: console.log,
})
// スケジュールを定義
scheduler.schedule((schedule) => {
schedule.call(() => console.log('Hello!')).everyMinute()
})
// アプリ起動時に開始
scheduler.start()
// シャットダウン時に停止
process.on('SIGTERM', () => {
scheduler.stop()
})
スケジュールの定義
コールバック
scheduler.schedule((schedule) => {
schedule.call(async () => {
// タスクロジック
await sendDailyReports()
}).daily().at('09:00')
})
ジョブ
キュージョブをスケジュールでディスパッチします。
import { SendWeeklyDigestJob } from '@/app/Jobs/SendWeeklyDigestJob'
scheduler.schedule((schedule) => {
schedule.job(SendWeeklyDigestJob, { userId: 'all' })
.weekly()
.sundays()
.at('09:00')
})
シェルコマンド
scheduler.schedule((schedule) => {
schedule.command('bunx guren db:backup')
.daily()
.at('02:00')
.name('database-backup')
})
実行頻度オプション
分単位
schedule.call(task).everyMinute() // 毎分
schedule.call(task).everyTwoMinutes() // 2分ごと
schedule.call(task).everyThreeMinutes() // 3分ごと
schedule.call(task).everyFourMinutes() // 4分ごと
schedule.call(task).everyFiveMinutes() // 5分ごと
schedule.call(task).everyTenMinutes() // 10分ごと
schedule.call(task).everyFifteenMinutes()// 15分ごと
schedule.call(task).everyThirtyMinutes() // 30分ごと
時間単位
schedule.call(task).hourly() // 毎時00分
schedule.call(task).hourlyAt(15) // 毎時15分
schedule.call(task).everyTwoHours() // 2時間ごと
schedule.call(task).everyThreeHours() // 3時間ごと
schedule.call(task).everyFourHours() // 4時間ごと
schedule.call(task).everySixHours() // 6時間ごと
日単位
schedule.call(task).daily() // 毎日0時
schedule.call(task).dailyAt('13:00') // 毎日13時
schedule.call(task).at('13:00') // dailyAtのエイリアス
schedule.call(task).twiceDaily(1, 13) // 1時と13時
週単位
schedule.call(task).weekly() // 毎週日曜0時
schedule.call(task).weeklyOn(1, '08:00') // 毎週月曜8時
// 曜日のショートカット
schedule.call(task).daily().sundays()
schedule.call(task).daily().mondays()
schedule.call(task).daily().tuesdays()
schedule.call(task).daily().wednesdays()
schedule.call(task).daily().thursdays()
schedule.call(task).daily().fridays()
schedule.call(task).daily().saturdays()
// 平日と週末
schedule.call(task).daily().weekdays() // 月曜〜金曜
schedule.call(task).daily().weekends() // 土曜〜日曜
月単位と年単位
schedule.call(task).monthly() // 毎月1日0時
schedule.call(task).monthlyOn(15, '09:00') // 毎月15日9時
schedule.call(task).lastDayOfMonth('18:00') // 月末18時
schedule.call(task).quarterly() // 1月、4月、7月、10月1日
schedule.call(task).yearly() // 1月1日0時
schedule.call(task).yearlyOn(6, 15, '12:00') // 6月15日12時
カスタムCron
// 標準cronフォーマット: 分 時 日 月 曜日
schedule.call(task).cron('0 */2 * * *') // 2時間ごと
schedule.call(task).cron('30 9 * * 1-5') // 平日9:30
schedule.call(task).cron('0 0 1,15 * *') // 1日と15日の0時
タスク設定
タスク名
schedule.call(sendReports)
.daily()
.name('send-daily-reports') // タスクの一意識別子
タイムゾーン
schedule.call(task)
.daily()
.at('09:00')
.tz('Asia/Tokyo') // 東京時間の9時に実行
// またはsetTimezoneを使用
schedule.call(task)
.daily()
.setTimezone('America/New_York')
重複実行の防止
前のインスタンスがまだ実行中の場合、タスクの実行をスキップします。
schedule.call(longRunningTask)
.everyMinute()
.preventOverlapping() // 前回の実行が終わっていなければスキップ
// 有効期限付き(10分後に自動アンロック)
schedule.call(task)
.everyMinute()
.preventOverlapping(600000) // ミリ秒で10分
単一サーバーでの実行
マルチサーバー環境で、1台のサーバーでのみタスクを実行します。
schedule.call(task)
.daily()
.runOnOneServer() // 分散ロック(Redis)が必要
条件付き実行
// 条件がtrueの場合のみ実行
schedule.call(task)
.daily()
.when(() => process.env.NODE_ENV === 'production')
// 条件がtrueの場合はスキップ
schedule.call(task)
.daily()
.skip(() => isMaintenanceMode())
ライフサイクルフック
schedule.call(sendEmails)
.daily()
.at('09:00')
.before(() => console.log('メール送信開始...'))
.after(() => console.log('メール送信完了'))
.onSuccess(() => metrics.increment('emails.sent'))
.onFailure((error) => {
alerting.notify('メール送信失敗', error)
})
スケジューラーAPI
const scheduler = new Scheduler()
// タスクを定義
scheduler.schedule((schedule) => { ... })
// ビルド済みタスクを追加
scheduler.addTask(scheduledTask)
// 開始/停止
scheduler.start()
scheduler.stop()
// ステータス確認
scheduler.getIsRunning()
// タスクを取得
scheduler.getTasks() // 全タスク
scheduler.getDueTasks() // 現在実行予定のタスク
scheduler.getTask('task-name') // 名前でタスクを取得
scheduler.count() // タスク数
// タスク管理
scheduler.removeTask('task-name') // 名前で削除
scheduler.clear() // 全タスク削除
// 手動実行
await scheduler.runDueTasks() // 実行予定の全タスクを今すぐ実行
CLI統合
コマンドラインからスケジューラーを実行します。
# スケジューラーを開始
bunx guren schedule:work
# カスタムタイムゾーンで開始
bunx guren schedule:work --timezone=Asia/Tokyo
# スケジュールされたタスクを一覧
bunx guren schedule:list
# 特定のタスクを即座に実行
bunx guren schedule:run cleanup-sessions
テスト
import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'
import { Scheduler, Schedule } from '@guren/core'
describe('Scheduling', () => {
let scheduler: Scheduler
beforeEach(() => {
scheduler = new Scheduler()
})
afterEach(() => {
scheduler.stop()
})
test('日次タスクをスケジュールする', () => {
scheduler.schedule((schedule) => {
schedule.call(() => {}).daily().at('09:00').name('test-task')
})
expect(scheduler.count()).toBe(1)
expect(scheduler.getTask('test-task')).toBeDefined()
})
test('実行予定のタスクを識別する', () => {
const mockTask = mock(() => {})
scheduler.schedule((schedule) => {
schedule.call(mockTask).everyMinute()
})
const dueTasks = scheduler.getDueTasks(new Date())
expect(dueTasks.length).toBeGreaterThan(0)
})
test('実行予定のタスクを実行する', async () => {
let executed = false
scheduler.schedule((schedule) => {
schedule.call(() => { executed = true }).everyMinute()
})
await scheduler.runDueTasks()
expect(executed).toBe(true)
})
test('when条件を尊重する', async () => {
let executed = false
scheduler.schedule((schedule) => {
schedule.call(() => { executed = true })
.everyMinute()
.when(() => false) // 実行しない
})
await scheduler.runDueTasks()
expect(executed).toBe(false)
})
})
ベストプラクティス
タスクに名前を付ける: デバッグと管理を容易にするため、常に
.name()を使用。適切な頻度を使用: 必要以上に頻繁にタスクをスケジュールしない。
タイムゾーンを明示的に設定: 時間に敏感なタスクには曖昧さを避けるためタイムゾーンを設定。
長いタスクには重複防止: インターバルより長くかかる可能性のあるタスクには
.preventOverlapping()を使用。失敗を適切に処理:
.onFailure()でエラーをログし、アラートを送信。スケジュールをテスト: タスクスケジューリングロジックを検証するテストを書く。
タスク実行を監視: タスクの実行をログし、成功/失敗のメトリクスを追跡。
重い処理にはジョブを使用: 重いタスクを直接スケジューラーで実行せず、ジョブをディスパッチ。