スケジューラ
定義した時間間隔で関数を実行します。
スケジューラを使う理由
アプリケーションには、期限切れレコードのクリーンアップ、毎日のメールダイジェスト送信、外部 API からのデータ同期、認証情報のローテーションなど、定期的に実行するバックグラウンドタスクが必要です。スケジューラがない場合、cron インフラを管理し、フォールトトレランスを確保し、デプロイ時の調整も自分で行う必要があります。
スケジューラを使えば、関数をデコレートしてデプロイするだけです:
// A decorated method that runs on a schedule
@scheduler('cleanupExpiredSessions', CronExpression.EVERY_DAY_AT_MIDNIGHT)
async cleanupExpiredSessions(): Promise<void> {
const sessions = this.squid.collection('sessions');
const expired = await sessions.query().where('expiresAt', '<', new Date()).dereference().snapshot();
for (const session of expired) {
await sessions.doc(session.id).delete();
}
}
cron サーバは不要。インフラも不要。時間通りに動く関数だけ。
概要
スケジューラは、UTC 時刻に基づいて定義された間隔で自動実行される backend 関数です。クライアントからのリクエストをトリガーにする必要がない、定期的なバックグラウンド処理に最適です。
スケジューラを使うべき場面
| ユースケース | 推奨 |
|---|---|
| スケジュールに従って定期的なバックグラウンドタスクを実行する | ✅ Scheduler |
| データベースの変更に反応する | Triggers を使用 |
| クライアントから関数を呼び出す | Executables を使用 |
| 外部サービス向けに HTTP エンドポイントを公開する | Webhooks を使用 |
仕組み
SquidServiceを継承したクラス内で、メソッドに@scheduler()を付与します- スケジュールとして cron 式または
CronExpressionenum 値を指定します - Squid がデプロイ時にスケジューラを検出して登録します
- 関数は UTC で、指定された間隔ごとに自動実行されます
- ログとエラーは Squid Console から確認できます
クイックスタート
前提条件
squid init-backendで初期化された Squid backend プロジェクト@squidcloud/backendパッケージがインストールされていること
Step 1: スケジューラを作成する
SquidService を継承する service クラスを作成し、スケジューラ関数を追加します:
import { CronExpression, SquidService, scheduler } from '@squidcloud/backend';
export class ExampleService extends SquidService {
@scheduler('logHeartbeat', CronExpression.EVERY_MINUTE)
async logHeartbeat(): Promise<void> {
console.log('Scheduler is running:', new Date().toISOString());
}
}
Step 2: service を export する
service の index ファイルから export されていることを確認します:
export * from './example-service';
Step 3: バックエンドを起動またはデプロイする
ローカル開発では、Squid CLI を使ってバックエンドをローカル実行します。
squid start
クラウドにデプロイするには、deploying your backend を参照してください。
Step 4: 確認する
Squid Console のログを確認し、想定した間隔でスケジューラが動作していることを確認します。
コアコンセプト
Cron expressions(cron 式)
@scheduler デコレータは、関数をいつ実行するかを定義する cron 式(文字列)を受け取れます。時刻はすべて協定世界時 (UTC) です。式を定義する際は、希望するローカル時刻を UTC に変換してください。
cron 式は次の形式に従います:
* * * * * *
| | | | | |
| | | | | day of week
| | | | months
| | | day of month
| | hours
| minutes
seconds (optional)
例:
| Expression | スケジュール |
|---|---|
0 0 * * * | 毎日 0:00(UTC) |
0 */6 * * * | 6 時間ごと |
30 9 * * 1-5 | 平日 9:30(UTC) |
0 0 1 * * | 毎月 1 日 0:00(UTC) |
CronExpression enum
CronExpression enum は、cron 文字列を手書きしなくてもよいように、定義済みの間隔を提供します:
import { CronExpression, scheduler } from '@squidcloud/backend';
@scheduler('everyMinute', CronExpression.EVERY_MINUTE)
async everyMinute(): Promise<void> { /* ... */ }
@scheduler('everyHour', CronExpression.EVERY_HOUR)
async everyHour(): Promise<void> { /* ... */ }
@scheduler('daily', CronExpression.EVERY_DAY_AT_MIDNIGHT)
async daily(): Promise<void> { /* ... */ }
exclusive パラメータ
@scheduler デコレータは 3 つのパラメータを受け取ります: スケジューラ名、cron 式、そして任意の exclusive boolean です。
exclusive が true(デフォルト)の場合、同時に実行されるスケジューラインスタンスは 1 つだけです。前回の実行がまだ動いている間に次の実行時刻になった場合、その実行はスキップされます。
// Default: exclusive is true, so overlapping runs are skipped
@scheduler('sendEmailReminders', CronExpression.EVERY_MINUTE, true)
async sendEmailReminders(): Promise<void> {
// If this takes longer than 1 minute, the next invocation is skipped
}
exclusive が false の場合、前回の実行が完了していなくても、スケジュール通りに新しいインスタンスが起動します。複数インスタンスが並行実行される可能性があります。
// Non-exclusive: allows concurrent runs
@scheduler('processQueue', CronExpression.EVERY_MINUTE, false)
async processQueue(): Promise<void> {
// Multiple instances may run in parallel
}
スケジューラの管理
スケジューラは、プログラムから無効化・再有効化・一覧取得ができます。
無効化と有効化:
無効化されたスケジューラは、再有効化されるまで実行されません。無効状態は再デプロイ後も保持されますが、undeploy の後に deploy した場合はすべてのスケジューラが再有効化されます。
// Disable a scheduler
await this.squid.schedulers.disable('logHeartbeat');
// Re-enable it later
await this.squid.schedulers.enable('logHeartbeat');
すべてのスケジューラを一覧表示:
登録されているスケジューラを、現在の状態(enabled / disabled)とともに返します。
const allSchedulers = await this.squid.schedulers.list();
console.log(allSchedulers);
エラーハンドリング
スケジューラが例外を投げた場合
スケジューラ関数がエラーを投げると、そのエラーはログに記録され、次回のスケジュール時刻でもスケジューラは継続して実行されます。単発の失敗でスケジューラが無効化されることはありません。
ログ出力とデバッグ
スケジューラ関数内では console.log と console.error を使用できます。出力は Squid Console のログで確認できます。
@scheduler('syncExternalData', CronExpression.EVERY_HOUR)
async syncExternalData(): Promise<void> {
console.log('Starting external data sync');
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`API returned status ${response.status}`);
}
const data = (await response.json()) as any[];
console.log(`Synced ${data.length} records`);
} catch (error) {
console.error('Sync failed:', error);
}
}
よくあるエラー
| エラー | 原因 | 解決策 |
|---|---|---|
| Scheduler が動作しない | service/index.ts で service が export されていない | service クラスが export されていることを確認 |
| Scheduler が動作しない | 変更後に backend がデプロイされていない | squid deploy を実行 |
| 実行の重複がスキップされる | exclusive が true で、前回の実行がまだアクティブ | 間隔を長くするか、exclusive を false にする |
| 実行タイミングが想定と違う | cron 式が UTC ではなくローカル時刻基準 | すべての時刻を UTC に変換 |
ベストプラクティス
-
冪等性 (idempotency) を前提に設計する。 スケジューラは、リトライや再デプロイにより想定以上に実行される場合があります。複数回実行されても同じ結果になるようにロジックを設計してください。
-
実行時間を短く保つ。 長時間実行されるスケジューラは、次回の呼び出しと重なったり、過剰にリソースを消費したりします。大きな処理は小さなバッチに分割してください。
-
exclusiveを適切に使う。 DB クリーンアップのように重複実行すべきでないタスクでは、exclusiveをtrue(デフォルト)のままにしてください。キュー内の独立したアイテムを処理するなど、並行実行が安全な場合にのみfalseにします。 -
スケジューラ内でエラーを処理する。
try/catchでロジックをラップしてエラーをログに出力します。これにより一時的な失敗が未処理例外になりにくくなります。 -
スケジューラを監視する。 Squid Console を使ってスケジューラが時間通りに動いていることを確認します。
this.squid.schedulers.list()で状態をプログラムから確認することもできます。 -
共有リソースを保護する。 スケジューラが共有データを変更する場合、依存サービス側で rate and quota limiting を使い、下流システムに負荷をかけすぎないようにしてください。
コード例
データベースのクリーンアップ
期限切れレコードを毎日削除します:
import { CronExpression, SquidService, scheduler } from '@squidcloud/backend';
interface Session {
id: string;
userId: string;
expiresAt: Date;
}
export class CleanupService extends SquidService {
@scheduler('cleanupExpiredSessions', CronExpression.EVERY_DAY_AT_MIDNIGHT)
async cleanupExpiredSessions(): Promise<void> {
const sessions = this.squid.collection<Session>('sessions');
const expired = await sessions
.query()
.where('expiresAt', '<', new Date())
.dereference()
.snapshot();
for (const session of expired) {
await sessions.doc(session.id).delete();
}
console.log(`Cleaned up ${expired.length} expired sessions`);
}
}
メールダイジェスト送信
毎週月曜 9:00(UTC)に週次サマリーを送信します:
import { SquidService, scheduler } from '@squidcloud/backend';
interface UserActivity {
userId: string;
email: string;
actionsThisWeek: number;
}
export class NotificationService extends SquidService {
@scheduler('sendWeeklyDigest', '0 9 * * 1') // Monday at 9:00 AM UTC
async sendWeeklyDigest(): Promise<void> {
const users = this.squid.collection<UserActivity>('userActivity');
const activeUsers = await users
.query()
.where('actionsThisWeek', '>', 0)
.dereference()
.snapshot();
for (const user of activeUsers) {
await this.sendDigestEmail(user.email, user.actionsThisWeek);
}
console.log(`Sent digest to ${activeUsers.length} users`);
}
private async sendDigestEmail(email: string, actionCount: number): Promise<void> {
const apiKey = this.secrets['EMAIL_API_KEY'];
await fetch('https://api.email.example.com/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: email,
subject: 'Your Weekly Activity Summary',
body: `You completed ${actionCount} actions this week.`,
}),
});
}
}
外部データの同期
外部 API からのデータを 1 時間ごとに取得します:
import { CronExpression, SquidService, scheduler } from '@squidcloud/backend';
interface Product {
id: string;
name: string;
price: number;
}
export class SyncService extends SquidService {
@scheduler('syncProducts', CronExpression.EVERY_HOUR)
async syncProducts(): Promise<void> {
try {
const apiKey = this.secrets['CATALOG_API_KEY'];
const response = await fetch('https://api.catalog.example.com/products', {
headers: { 'Authorization': `Bearer ${apiKey}` },
});
if (!response.ok) {
console.error(`Catalog API error: ${response.status}`);
return;
}
const products = (await response.json()) as Product[];
const collection = this.squid.collection<Product>('products');
for (const product of products) {
await collection.doc(product.id).insert(product);
}
console.log(`Synced ${products.length} products`);
} catch (error) {
console.error('Product sync failed:', error);
}
}
}
関連項目
- Triggers - データベース変更に反応する
- Executables - クライアント向けに backend 関数を公開する
- Webhooks - 外部サービス向けに HTTP エンドポイントを公開する
- Rate and quota limiting - backend 関数を保護する
- Database - データのアクセスと管理