メインコンテンツまでスキップ

スケジューラ

定義した時間間隔で関数を実行します。

スケジューラを使う理由

アプリケーションには、期限切れレコードのクリーンアップ、毎日のメールダイジェスト送信、外部 API からのデータ同期、認証情報のローテーションなど、定期的に実行するバックグラウンドタスクが必要です。スケジューラがない場合、cron インフラを管理し、フォールトトレランスを確保し、デプロイ時の調整も自分で行う必要があります。

スケジューラを使えば、関数をデコレートしてデプロイするだけです:

Backend code
// 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 を使用

仕組み

  1. SquidService を継承したクラス内で、メソッドに @scheduler() を付与します
  2. スケジュールとして cron 式または CronExpression enum 値を指定します
  3. Squid がデプロイ時にスケジューラを検出して登録します
  4. 関数は UTC で、指定された間隔ごとに自動実行されます
  5. ログとエラーは Squid Console から確認できます

クイックスタート

前提条件

  • squid init-backend で初期化された Squid backend プロジェクト
  • @squidcloud/backend パッケージがインストールされていること

Step 1: スケジューラを作成する

SquidService を継承する service クラスを作成し、スケジューラ関数を追加します:

Backend code
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 されていることを確認します:

Backend code
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 文字列を手書きしなくてもよいように、定義済みの間隔を提供します:

Backend code
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 です。

exclusivetrue(デフォルト)の場合、同時に実行されるスケジューラインスタンスは 1 つだけです。前回の実行がまだ動いている間に次の実行時刻になった場合、その実行はスキップされます。

Backend code
// 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
}

exclusivefalse の場合、前回の実行が完了していなくても、スケジュール通りに新しいインスタンスが起動します。複数インスタンスが並行実行される可能性があります。

Backend code
// Non-exclusive: allows concurrent runs
@scheduler('processQueue', CronExpression.EVERY_MINUTE, false)
async processQueue(): Promise<void> {
// Multiple instances may run in parallel
}

スケジューラの管理

スケジューラは、プログラムから無効化・再有効化・一覧取得ができます。

無効化と有効化:

無効化されたスケジューラは、再有効化されるまで実行されません。無効状態は再デプロイ後も保持されますが、undeploy の後に deploy した場合はすべてのスケジューラが再有効化されます。

Backend code
// Disable a scheduler
await this.squid.schedulers.disable('logHeartbeat');

// Re-enable it later
await this.squid.schedulers.enable('logHeartbeat');

すべてのスケジューラを一覧表示:

登録されているスケジューラを、現在の状態(enabled / disabled)とともに返します。

Backend code
const allSchedulers = await this.squid.schedulers.list();
console.log(allSchedulers);

エラーハンドリング

スケジューラが例外を投げた場合

スケジューラ関数がエラーを投げると、そのエラーはログに記録され、次回のスケジュール時刻でもスケジューラは継続して実行されます。単発の失敗でスケジューラが無効化されることはありません。

ログ出力とデバッグ

スケジューラ関数内では console.logconsole.error を使用できます。出力は Squid Console のログで確認できます。

Backend code
@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 を実行
実行の重複がスキップされるexclusivetrue で、前回の実行がまだアクティブ間隔を長くするか、exclusivefalse にする
実行タイミングが想定と違うcron 式が UTC ではなくローカル時刻基準すべての時刻を UTC に変換

ベストプラクティス

  1. 冪等性 (idempotency) を前提に設計する。 スケジューラは、リトライや再デプロイにより想定以上に実行される場合があります。複数回実行されても同じ結果になるようにロジックを設計してください。

  2. 実行時間を短く保つ。 長時間実行されるスケジューラは、次回の呼び出しと重なったり、過剰にリソースを消費したりします。大きな処理は小さなバッチに分割してください。

  3. exclusive を適切に使う。 DB クリーンアップのように重複実行すべきでないタスクでは、exclusivetrue(デフォルト)のままにしてください。キュー内の独立したアイテムを処理するなど、並行実行が安全な場合にのみ false にします。

  4. スケジューラ内でエラーを処理する。 try/catch でロジックをラップしてエラーをログに出力します。これにより一時的な失敗が未処理例外になりにくくなります。

  5. スケジューラを監視する。 Squid Console を使ってスケジューラが時間通りに動いていることを確認します。this.squid.schedulers.list() で状態をプログラムから確認することもできます。

  6. 共有リソースを保護する。 スケジューラが共有データを変更する場合、依存サービス側で rate and quota limiting を使い、下流システムに負荷をかけすぎないようにしてください。

コード例

データベースのクリーンアップ

期限切れレコードを毎日削除します:

Backend code
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)に週次サマリーを送信します:

Backend code
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 時間ごとに取得します:

Backend code
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 - データのアクセスと管理