Cron を使わない古いデータの削除
保持期間(既定は 180 日)を過ぎた問い合わせを、定期実行のジョブなしで削除するしくみを説明します。
問い合わせは保持期間(既定は 180 日)を過ぎると削除されます。定期実行のジョブは使わず、ふだんのリクエストの中で、多くても 1 時間に 1 回削除します。
何をするか
- 新しい問い合わせの後に削除します。 問い合わせを保存した後、Worker は
ctx.waitUntil(応答の後も処理を続けるしくみ)の中でsettings.last_cleanup_atを確認します。前回から 1 時間以上たっていれば、保持期間を過ぎた問い合わせを削除し、last_cleanup_atを更新します。 - 管理画面とログインのときも削除します。 問い合わせの一覧を開いたときと、Passkey でログインしようとしたときにも、同じ削除の処理が動きます。新しい問い合わせが来なくても、古いものは消えます。リクエストがまったく来ないサイトでは、次のリクエストまで何も削除されません。
- 削除は速く済みます。
created_atにインデックスがあるからです。 - 期限切れのチャレンジも消します。 Passkey でログインしようとするたびに、Better Auth の
verificationテーブルにチャレンジを書き込みます。使ったチャレンジはすぐ消えますが、途中でやめたものは残り続けます。同じ削除の処理で、期限切れの行も消します。チャレンジは数分で期限が切れ、ログインには回数制限があるので、インデックスがなくてもテーブルは小さいままです。 - 期限切れのセッションも消します。 期限切れの管理者のセッションは、ブラウザがそのトークンをもう一度送るまで、トークンとブラウザ名を
sessionテーブルに残します。送られないままのこともあります。同じ削除の処理で、期限を過ぎたセッションも消します。セッションを持つのは管理者だけなので、このテーブルも小さいままです。
この自動の削除とは別に、管理者は問い合わせを手動で削除したり、CSV で書き出したりもできます。書き出しは 1 回のクエリで 200 行ずつ読んで順に送り出すので、テーブルが大きくても同じように動きます。
コードでは
問い合わせ API は、応答を返す直前に削除の処理を始めます。runInBackground() は処理を ctx.waitUntil に渡します。失敗してもログに書くだけで、応答は変えません。
runInBackground(c, 'prune', () => pruneIfDue(env.DB, now));
return c.json({ ok: true });pruneIfDue() は、まず削除を実行する権利を取ります。条件付きの upsert(行がなければ追加し、あれば更新)を使い、保存されている last_cleanup_at が 1 時間以上前のときだけ現在の時刻に書き換えます。書き換えたリクエストだけが削除に進むので、2 つのリクエストが同時に来ても、削除するのは片方だけです。
保持期間は settings.retention_days から読みます。範囲は 1〜3,650 日で、ないときや不正な値のときは 180 日です。
const orm = getDatabase(db);
const claim = await orm
.insert(settings)
.values({ key: 'last_cleanup_at', value: String(now) })
.onConflictDoUpdate({
target: settings.key,
set: { value: sql`excluded.value` },
setWhere: lte(
sql`cast(${settings.value} as integer)`,
now - PRUNE_INTERVAL_MS,
),
});
if (claim.meta.changes === 0) return { ran: false };
const retentionDays = await getRetentionDays(db);
const result = await orm
.delete(submissions)
.where(lt(submissions.createdAt, now - retentionDays * DAY_MS));
const verifications = await orm
.delete(verification)
.where(lt(verification.expiresAt, new Date(now)));
const sessions = await orm
.delete(session)
.where(lt(session.expiresAt, new Date(now)));なぜこう作るか
goodnight-web は Cron Triggers を使いません。設定して理解するものを、Worker 1 つとデータベース 1 つだけにするためです。
古いデータは決まった時刻に消す必要はなく、いずれ消えれば十分です。そのため、もともと来るリクエストの中で削除します。1 時間に 1 回までに間引くので、増える処理はわずかです。
削除は応答の後に動くので、問い合わせの送信を遅くしたり、失敗させたりすることはありません。
削除した行も、しばらくは D1 の Time Travel で復元できます。プライバシーについてを参照してください。