バッチを、なくす — 項目別タイマー起動イベントで実現するバッチレス設計
「全件を舐めて対象を探す」夜間バッチをやめ、各項目が自分の発火を"予約"する。軽量ティッカーが到来分だけを起こす、スキャンしない設計。
「毎晩2:00、全注文・全定期をスキャンして、今日処理すべきものを探す」——このバッチは、データが増えるほど遅くなり、時間内に終わらなくなります。Vegas はこの発想を逆転させ、各項目に"いつ起きるか"を持たせ、その時が来たものだけをイベントとして起こすことで、大きな夜間バッチをなくしています。
The Problem「スキャン型バッチ」の限界
次回定期受注の生成、予約キャンセル、ポイント失効、出荷予定日の出荷指示——通販には「ある時刻に何かする」処理が山ほどあります。素朴に作ると、それぞれが全件スキャンの定期バッチになります。
- スケールしない:件数に比例して処理時間が伸び、夜間ウィンドウが破綻する。
- 負荷が集中する:定時に一気に走り、DBもインフラもスパイクする。
- 粒度が粗い:「日次」でしか動けず、リアルタイム性がない。
The Idea「探す」のをやめ、「予約」する
Vegas では、処理を起こしたい項目が発生した時点で、それを「予約イベント(トリガ)」としてレジストリに登録します。各エントリは「対象・呼ぶべきAPI・発火条件(時刻)・未発火フラグ」を持ちます。そしてごく軽量なティッカーが短間隔で回り、到来した(発火すべき)エントリだけをイベントバスへ流し、処理後に"発火済み"を記録します。
全件をスキャンして対象を「探す」のではなく、各項目が自分の発火を「予約」しておく。ティッカーは到来分だけを起こす=処理量はデータ総量ではなく"いま到来した件数"に比例する。
Mechanism仕組み:レジストリ+ティッカー+イベントバス
レジストリは「未発火」かつ「発火時刻が到来した」行だけを索引で引くので、テーブルが何億行あっても、1回で扱うのは"いま到来した数十件"だけです。ティッカー自体は数秒で終わる薄い処理で、EventBridge Scheduler のような仕組みで短間隔に起動します。
// ティッカー(例: 1分ごと)。全件スキャンではなく「到来した未発火」だけを引く const due = await db.query(` SELECT id, api_name, target FROM trigger_registry WHERE fired_at IS NULL AND fire_at <= NOW() -- 発火時刻が到来 ORDER BY fire_at ASC LIMIT 500`); -- 到来分だけ・上限つき for (const t of due) { await eventBus.publish(t.api_name, t.target); // イベント化(冪等) await db.query(`UPDATE trigger_registry SET fired_at = NOW() WHERE id = ?`, [t.id]); }
Why it wins効いてくる点
- データが増えても遅くならない:処理量は"到来件数"に比例。総量スキャンが消える。
- 負荷が平準化:深夜一括ではなく、時刻に沿って分散。サーバーレスと好相性(必要な瞬間だけ)。
- ほぼリアルタイム:「日次」ではなく「予約時刻ちょうど」に近づく。
- 疎結合:発火は既存のイベント経路(冪等・自動リトライ・履歴)に乗る。新しい"予約"の追加はレジストリに1行足すだけ。
設定を時刻で解決する時刻バージョニングと根は同じ——「時刻を一級の概念として扱う」という思想を、処理の起動にも広げたものです。
Trade-offs設計上の勘所
- 一度だけ発火(冪等):ティッカーの多重起動やリトライで二重発火しないよう、更新の原子性と冪等キーで守る(二重処理の防止と同根)。
- キャッチアップ:ティッカーが止まっていた間に到来した予約を、復旧後にまとめて回収できること(fire_at ≤ NOW() が自然にこれを満たす)。
- 時刻の一貫性:発火基準のタイムゾーン/クロックを揃える。
- 可視性:「いつ・何が発火予定か」を一覧・監査できること。予約の取消・再スケジュールの手当ても要る。
「全件を探すバッチ」を「項目ごとの予約イベント+軽量ティッカー」に置き換えると、処理量はデータ総量から切り離され、負荷は平準化し、実行はほぼリアルタイムになる。夜間バッチという運用の常識を、1つ消せる。