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

スケジューラー

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

スケジューラーを使用する理由

アプリケーションには、定期的なバックグラウンドタスクが必要です。期限切れレコードのクリーンアップ、日次メールダイジェストの送信、外部 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 時刻に基づいて定義された間隔で自動的に実行されるバックエンド関数です。クライアントからのリクエストでトリガーする必要がない、定期的なバックグラウンド処理に最適です。

スケジューラーを使うべきとき

ユースケース推奨
スケジュールに従って定期的なバックグラウンドタスクを実行する✅ Scheduler
データベースの変更に反応するTriggers を使用
クライアントから関数を呼び出すExecutables を使用
外部サービス向けに HTTP エンドポイントを公開するWebhooks を使用

仕組み

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

クイックスタート

前提条件

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

手順 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());
}
}

手順 2: service をエクスポートする

service の index ファイルからエクスポートされていることを確認します。

Backend code
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 文字列を手動で書かなくてもよいように、事前定義された間隔を提供します。

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 not runningservice/index.ts で service がエクスポートされていないservice クラスがエクスポートされていることを確認する
Scheduler not running変更後に backend がデプロイされていないsquid deploy を実行する
Overlapping runs skippedexclusivetrue で、前回の実行がまだアクティブ間隔を長くするか exclusivefalse にする
Incorrect timingcron 式が UTC ではなくローカル時刻を使っているすべての時刻を UTC に変換する

ベストプラクティス

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

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

  3. exclusive を賢く使う。 データベースクリーンアップのように重複実行させたくないタスクでは、exclusivetrue(デフォルト)のままにします。キューの独立したアイテムを処理するなど、同時実行しても安全な場合にのみ false を設定してください。

  4. スケジューラー内でエラーを処理する。 try/catch でロジックを包み、エラーをログに残してください。一時的な失敗が未処理例外になるのを防げます。

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

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

コード例

Database cleanup(データベースクリーンアップ)

日次スケジュールで期限切れレコードを削除します。

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`);
}
}

Sending email digests(メールダイジェストの送信)

毎週月曜日の UTC 9:00 に週次サマリーメールを送信します。

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.`,
}),
});
}
}

Syncing external data(外部データの同期)

外部 API から毎時間データを取得します。

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 - クライアント向けにバックエンド関数を公開する
  • Webhooks - 外部サービス向けに HTTP エンドポイントを公開する
  • Rate and quota limiting - バックエンド関数を保護する
  • Database - データへのアクセスと管理