スケジューラー
定義された時間間隔で関数を実行します。
スケジューラーを使用する理由
アプリケーションには、定期的なバックグラウンドタスクが必要です。期限切れレコードのクリーンアップ、日次メールダイジェストの送信、外部 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 時刻に基づいて定義された間隔で自動的に実行されるバックエンド関数です。クライアントからのリクエストでトリガーする必要がない、定期的なバックグラウンド処理に最適です。
スケジューラーを使うべきとき
| ユースケース | 推奨 |
|---|---|
| スケジュールに従って定期的なバックグラウンドタスクを実行する | ✅ Scheduler |
| データベースの変更に反応する | Triggers を使用 |
| クライアントから関数を呼び出す | Executables を使用 |
| 外部サービス向けに HTTP エンドポイントを公開する | Webhooks を使用 |
仕組み
SquidServiceを拡張したクラス内で、メソッドに@scheduler()を付与します- スケジュールとして cron 式、または
CronExpressionenum 値を指定します - Squid はデプロイ時にスケジューラーを検出し、登録します
- 関数は UTC での各スケジュール間隔ごとに自動実行されます
- ログとエラーは Squid Console から確認できます
クイックスタート
前提条件
squid initで初期化された Squid backend プロジェクト@squidcloud/backendパッケージがNPMからインストールされていること
手順 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());
}
}
手順 2: service をエクスポートする
service の index ファイルからエクスポートされていることを確認します。
export * from './example-service';
手順 3: backend を起動またはデプロイする
ローカル開発では、Squid CLI を使って backend をローカルで実行します。
squid start
クラウドへデプロイする場合は、backend のデプロイ を参照してください。
手順 4: 確認する
Squid Console のログを確認し、スケジューラーが期待した間隔で実行されていることを確認します。
コアコンセプト
Cron expressions(cron 式)
@scheduler デコレータは、関数の実行タイミングを定義する cron 式文字列を受け取ります。時刻はすべて協定世界時 (UTC) です。式を定義する際は、希望するローカル時刻を UTC に変換してください。
cron 式は次の形式に従います。
* * * * * *
| | | | | |
| | | | | day of week
| | | | months
| | | day of month
| | hours
| minutes
seconds (optional)
例:
| 式 | スケジュール |
|---|---|
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 not running | service/index.ts で service がエクスポートされていない | service クラスがエクスポートされていることを確認する |
| Scheduler not running | 変更後に backend がデプロイされていない | squid deploy を実行する |
| Overlapping runs skipped | exclusive が true で、前回の実行がまだアクティブ | 間隔を長くするか exclusive を false にする |
| Incorrect timing | cron 式が UTC ではなくローカル時刻を使っている | すべての時刻を UTC に変換する |
ベストプラクティス
-
冪等性(idempotency)を意識して設計する。 スケジューラーは、リトライや再デプロイの影響で想定以上に実行されることがあります。複数回実行されても同じ結果になるようにロジックを設計してください。
-
実行時間を短く保つ。 長時間実行されるスケジューラーは次回の呼び出しと重なったり、過度なリソースを消費したりします。大きなタスクは小さなバッチに分割してください。
-
exclusiveを賢く使う。 データベースクリーンアップのように重複実行させたくないタスクでは、exclusiveはtrue(デフォルト)のままにします。キューの独立したアイテムを処理するなど、同時実行しても安全な場合にのみfalseを設定してください。 -
スケジューラー内でエラーを処理する。
try/catchでロジックを包み、エラーをログに残してください。一時的な失敗が未処理例外になるのを防げます。 -
スケジューラーを監視する。 Squid Console を使って、スケジューラーが時間通りに動作していることを確認してください。
this.squid.schedulers.list()を使うと、スケジューラーの状態をプログラムから確認できます。 -
共有リソースを保護する。 スケジューラーが共有データを変更する場合、下流システムを過負荷にしないよう、依存サービスに対して rate and quota limiting を使用してください。
コード例
Database cleanup(データベースクリーンアップ)
日次スケジュールで期限切れレコードを削除します。
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`);
}
}
Sending email digests(メールダイジェストの送信)
毎週月曜日の UTC 9:00 に週次サマリーメールを送信します。
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.`,
}),
});
}
}
Syncing external data(外部データの同期)
外部 API から毎時間データを取得します。
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 - クライアント向けにバックエンド関数を公開する
- Webhooks - 外部サービス向けに HTTP エンドポイントを公開する
- Rate and quota limiting - バックエンド関数を保護する
- Database - データへのアクセスと管理