From 16370feccbe7018c495c425d9bd72bd8899f42ec Mon Sep 17 00:00:00 2001 From: liplus-lin-lay Date: Fri, 26 Jun 2026 19:16:43 +0900 Subject: [PATCH] Shorten processed-event retention window from 7 to 3 days MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 処理済み (mark_processed 済み) webhook イベントの保持期間を 7 日から 3 日へ短縮する。 処理済み窓に構造依存は無く (再 delivery は INSERT OR REPLACE で捌き、list_pending 系は processed=0 のみ参照)、唯一の用途は get_event の事後 lookback。3 日でその floor は十分。 ストレージ主因の未処理 90 日側は変更しない (3/90 の非対称は意図的)。 - worker/wrangler.toml: PURGE_AFTER_DAYS 7 -> 3 - worker/src/store.ts: DEFAULT_PURGE_DAYS 7 -> 3, doc comment / asymmetry note 同期 - README.md / mcp-server/README.md: retention 表 7 -> 3 days - docs/0-requirements(.ja).md: F2.4 / F2.5 / N2.7 / N2.8 表記同期 - worker/test/workers/store.test.ts: stale な 7-day コメント同期 (アサート値 30/1d は不変、全 green) Closes #240 --- README.md | 4 ++-- docs/0-requirements.ja.md | 8 ++++---- docs/0-requirements.md | 8 ++++---- mcp-server/README.md | 4 ++-- worker/src/store.ts | 6 +++--- worker/test/workers/store.test.ts | 12 ++++++------ worker/wrangler.toml | 2 +- 7 files changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index b9d5214..8e59b81 100644 --- a/README.md +++ b/README.md @@ -133,11 +133,11 @@ for tenants that never call `mark_processed`: | Event class | Retention window | Env var | Default | |-------------|------------------|---------|---------| -| Processed (`mark_processed` called) | older than the window is deleted | `PURGE_AFTER_DAYS` | `7` days | +| Processed (`mark_processed` called) | older than the window is deleted | `PURGE_AFTER_DAYS` | `3` days | | Unprocessed (never marked) | older than the window is deleted | `UNPROCESSED_PURGE_AFTER_DAYS` | `90` days | - The longer window for unprocessed events is intentional: unprocessed means - user-unseen, so the safety margin before dropping is wide (the 7-day vs 90-day + user-unseen, so the safety margin before dropping is wide (the 3-day vs 90-day asymmetry is by design). - The sweep runs via a Durable Object Alarm on a daily cadence and reschedules itself, so it fires independently of consumption. Processed events are also diff --git a/docs/0-requirements.ja.md b/docs/0-requirements.ja.md index e7771ea..c0c3ba4 100644 --- a/docs/0-requirements.ja.md +++ b/docs/0-requirements.ja.md @@ -75,8 +75,8 @@ GitHub --POST--> Cloudflare Worker --> TenantRegistry DO | F2.1 | イベントを UUID 付きで WebhookStore DO の SQLite に保存する | | F2.2 | 各イベントは id, type, payload, received_at, processed フィールドを持つ | | F2.3 | trigger_status, last_triggered_at フィールドを保持する(将来の trigger 機能用) | -| F2.4 | `mark_processed` 実行時に、`processed=1` かつ `received_at` が保持期間(`PURGE_AFTER_DAYS` 日、既定 7)より古いイベントを自動削除する。処理済み死蔵行による DO ストレージ肥大を防ぐ(即時性のための補助経路。本質的な保証は F2.5 の Alarm sweep が担う) | -| F2.5 | DO Alarm(`ctx.storage.setAlarm` + `alarm()` ハンドラ)による時間駆動の retention sweep を提供する。sweep は (a) `processed=1` かつ `received_at` が `PURGE_AFTER_DAYS` 日(既定 7)より古い処理済みイベントと、(b) `processed=0` かつ `received_at` が `UNPROCESSED_PURGE_AFTER_DAYS` 日(既定 90)より古い未処理イベントを、まとめて削除する。alarm() は実行後に次回 sweep を再スケジュールし、消費ゼロ(`mark_processed` が一度も呼ばれない放置テナント)でも周期実行(日次)される。未処理保持期間を処理済みより長く取るのは未読データ消失の安全域確保のため(7 日 / 90 日の非対称は意図的)。既知の限界(スコープ外): 時間窓は「古さ」を縛るが「量」は縛らないため、高頻度 × 無消費テナントは 90 日窓があっても Cloudflare の 1GB/DO 壁に先に到達しうる。量ベース hard cap は別 issue 候補 | +| F2.4 | `mark_processed` 実行時に、`processed=1` かつ `received_at` が保持期間(`PURGE_AFTER_DAYS` 日、既定 3)より古いイベントを自動削除する。処理済み死蔵行による DO ストレージ肥大を防ぐ(即時性のための補助経路。本質的な保証は F2.5 の Alarm sweep が担う) | +| F2.5 | DO Alarm(`ctx.storage.setAlarm` + `alarm()` ハンドラ)による時間駆動の retention sweep を提供する。sweep は (a) `processed=1` かつ `received_at` が `PURGE_AFTER_DAYS` 日(既定 3)より古い処理済みイベントと、(b) `processed=0` かつ `received_at` が `UNPROCESSED_PURGE_AFTER_DAYS` 日(既定 90)より古い未処理イベントを、まとめて削除する。alarm() は実行後に次回 sweep を再スケジュールし、消費ゼロ(`mark_processed` が一度も呼ばれない放置テナント)でも周期実行(日次)される。未処理保持期間を処理済みより長く取るのは未読データ消失の安全域確保のため(3 日 / 90 日の非対称は意図的)。既知の限界(スコープ外): 時間窓は「古さ」を縛るが「量」は縛らないため、高頻度 × 無消費テナントは 90 日窓があっても Cloudflare の 1GB/DO 壁に先に到達しうる。量ベース hard cap は別 issue 候補 | **イベント構造:** @@ -204,8 +204,8 @@ Worker は GitHub の web OAuth flow をホストする独自実装を備える | N2.4 | カスタムドメイン | `github-webhook.smgjp.com` | Cloudflare Worker のカスタムドメインとして設定済み | | N2.5 | 認証方式 | Worker 自前認証 | Cloudflare Access は使用しない。Worker が webhook secret + Worker-hosted web OAuth で認証を処理する | | N2.6 | プレビューインスタンス | `preview` 環境 | 本番と同一構成の検証用インスタンス | -| N2.7 | 処理済みイベント保持期間 | `PURGE_AFTER_DAYS` 環境変数(wrangler.toml `[vars]`) | `7`(日)。`mark_processed` 時および DO Alarm sweep 時に保持期間超過の処理済みイベントを削除。`0` で処理済みを即削除 | -| N2.8 | 未処理イベント保持期間 | `UNPROCESSED_PURGE_AFTER_DAYS` 環境変数(wrangler.toml `[vars]`) | `90`(日)。DO Alarm sweep 時に保持期間超過の未処理イベントを削除。`0` で未処理を即削除。処理済み(7 日)より長いのは未読データ消失の安全域(非対称は意図的) | +| N2.7 | 処理済みイベント保持期間 | `PURGE_AFTER_DAYS` 環境変数(wrangler.toml `[vars]`) | `3`(日)。`mark_processed` 時および DO Alarm sweep 時に保持期間超過の処理済みイベントを削除。`0` で処理済みを即削除 | +| N2.8 | 未処理イベント保持期間 | `UNPROCESSED_PURGE_AFTER_DAYS` 環境変数(wrangler.toml `[vars]`) | `90`(日)。DO Alarm sweep 時に保持期間超過の未処理イベントを削除。`0` で未処理を即削除。処理済み(3 日)より長いのは未読データ消失の安全域(非対称は意図的) | ### N3. 制約 diff --git a/docs/0-requirements.md b/docs/0-requirements.md index eaa4572..b3c0ca3 100644 --- a/docs/0-requirements.md +++ b/docs/0-requirements.md @@ -75,8 +75,8 @@ GitHub ──POST──▶ Cloudflare Worker ──▶ TenantRegistry DO | F2.1 | イベントを UUID 付きで WebhookStore DO の SQLite に保存する | | F2.2 | 各イベントは id, type, payload, received_at, processed フィールドを持つ | | F2.3 | trigger_status, last_triggered_at フィールドを保持する(将来の trigger 機能用) | -| F2.4 | `mark_processed` 実行時に、`processed=1` かつ `received_at` が保持期間(`PURGE_AFTER_DAYS` 日、既定 7)より古いイベントを自動削除する。処理済み死蔵行による DO ストレージ肥大を防ぐ(即時性のための補助経路。本質的な保証は F2.5 の Alarm sweep が担う) | -| F2.5 | DO Alarm(`ctx.storage.setAlarm` + `alarm()` ハンドラ)による時間駆動の retention sweep を提供する。sweep は (a) `processed=1` かつ `received_at` が `PURGE_AFTER_DAYS` 日(既定 7)より古い処理済みイベントと、(b) `processed=0` かつ `received_at` が `UNPROCESSED_PURGE_AFTER_DAYS` 日(既定 90)より古い未処理イベントを、まとめて削除する。alarm() は実行後に次回 sweep を再スケジュールし、消費ゼロ(`mark_processed` が一度も呼ばれない放置テナント)でも周期実行(日次)される。未処理保持期間を処理済みより長く取るのは未読データ消失の安全域確保のため(7 日 / 90 日の非対称は意図的)。既知の限界(スコープ外): 時間窓は「古さ」を縛るが「量」は縛らないため、高頻度 × 無消費テナントは 90 日窓があっても Cloudflare の 1GB/DO 壁に先に到達しうる。量ベース hard cap は別 issue 候補 | +| F2.4 | `mark_processed` 実行時に、`processed=1` かつ `received_at` が保持期間(`PURGE_AFTER_DAYS` 日、既定 3)より古いイベントを自動削除する。処理済み死蔵行による DO ストレージ肥大を防ぐ(即時性のための補助経路。本質的な保証は F2.5 の Alarm sweep が担う) | +| F2.5 | DO Alarm(`ctx.storage.setAlarm` + `alarm()` ハンドラ)による時間駆動の retention sweep を提供する。sweep は (a) `processed=1` かつ `received_at` が `PURGE_AFTER_DAYS` 日(既定 3)より古い処理済みイベントと、(b) `processed=0` かつ `received_at` が `UNPROCESSED_PURGE_AFTER_DAYS` 日(既定 90)より古い未処理イベントを、まとめて削除する。alarm() は実行後に次回 sweep を再スケジュールし、消費ゼロ(`mark_processed` が一度も呼ばれない放置テナント)でも周期実行(日次)される。未処理保持期間を処理済みより長く取るのは未読データ消失の安全域確保のため(3 日 / 90 日の非対称は意図的)。既知の限界(スコープ外): 時間窓は「古さ」を縛るが「量」は縛らないため、高頻度 × 無消費テナントは 90 日窓があっても Cloudflare の 1GB/DO 壁に先に到達しうる。量ベース hard cap は別 issue 候補 | **イベント構造:** @@ -204,8 +204,8 @@ Worker は GitHub の web OAuth flow をホストする独自実装を備える | N2.4 | カスタムドメイン | `github-webhook.smgjp.com` | Cloudflare Worker のカスタムドメインとして設定済み | | N2.5 | 認証方式 | Worker 自前認証 | Cloudflare Access は使用しない。Worker が webhook secret + Worker-hosted web OAuth で認証を処理する | | N2.6 | プレビューインスタンス | `preview` 環境 | 本番と同一構成の検証用インスタンス | -| N2.7 | 処理済みイベント保持期間 | `PURGE_AFTER_DAYS` 環境変数(wrangler.toml `[vars]`) | `7`(日)。`mark_processed` 時および DO Alarm sweep 時に保持期間超過の処理済みイベントを削除。`0` で処理済みを即削除 | -| N2.8 | 未処理イベント保持期間 | `UNPROCESSED_PURGE_AFTER_DAYS` 環境変数(wrangler.toml `[vars]`) | `90`(日)。DO Alarm sweep 時に保持期間超過の未処理イベントを削除。`0` で未処理を即削除。処理済み(7 日)より長いのは未読データ消失の安全域(非対称は意図的) | +| N2.7 | 処理済みイベント保持期間 | `PURGE_AFTER_DAYS` 環境変数(wrangler.toml `[vars]`) | `3`(日)。`mark_processed` 時および DO Alarm sweep 時に保持期間超過の処理済みイベントを削除。`0` で処理済みを即削除 | +| N2.8 | 未処理イベント保持期間 | `UNPROCESSED_PURGE_AFTER_DAYS` 環境変数(wrangler.toml `[vars]`) | `90`(日)。DO Alarm sweep 時に保持期間超過の未処理イベントを削除。`0` で未処理を即削除。処理済み(3 日)より長いのは未読データ消失の安全域(非対称は意図的) | **GitHub Webhook 購読イベント:** diff --git a/mcp-server/README.md b/mcp-server/README.md index 46d7d56..d1d9c86 100644 --- a/mcp-server/README.md +++ b/mcp-server/README.md @@ -155,10 +155,10 @@ The Worker purges stored events automatically so Durable Object storage stays bo | Event class | Retention window | Worker env var | Default | |---|---|---|---| -| Processed (`mark_processed` called) | events older than the window are deleted | `PURGE_AFTER_DAYS` | `7` days | +| Processed (`mark_processed` called) | events older than the window are deleted | `PURGE_AFTER_DAYS` | `3` days | | Unprocessed (never marked) | events older than the window are deleted | `UNPROCESSED_PURGE_AFTER_DAYS` | `90` days | -- The longer window for unprocessed events is intentional — unprocessed means user-unseen, so the margin before dropping is wide (the 7-day vs 90-day asymmetry is by design). +- The longer window for unprocessed events is intentional — unprocessed means user-unseen, so the margin before dropping is wide (the 3-day vs 90-day asymmetry is by design). - Processed events are also purged immediately on `mark_processed` for promptness; the Alarm sweep is the guarantee that covers tenants that stop consuming. - Both windows are Worker-side configuration (`worker/wrangler.toml` `[vars]`); set a value to `0` to purge that class immediately on sweep. These env vars live on the Worker, not in this proxy. - Known limitation: the windows bound event *age*, not *volume*. A high-rate, never-consumed tenant can still hit Cloudflare's 1 GB-per-DO ceiling before the 90-day window applies. diff --git a/worker/src/store.ts b/worker/src/store.ts index e03a9b5..ea68987 100644 --- a/worker/src/store.ts +++ b/worker/src/store.ts @@ -18,13 +18,13 @@ interface StoreEnv { } /** Default retention window (days) for processed events when PURGE_AFTER_DAYS is unset. */ -const DEFAULT_PURGE_DAYS = 7; +const DEFAULT_PURGE_DAYS = 3; /** * Default retention window (days) for UNPROCESSED events when * UNPROCESSED_PURGE_AFTER_DAYS is unset. Deliberately much longer than the * processed window: unprocessed = user-unseen, so the safety margin before - * silently dropping is wide (the 7d-vs-90d asymmetry is intentional). + * silently dropping is wide (the 3d-vs-90d asymmetry is intentional). */ const DEFAULT_UNPROCESSED_PURGE_DAYS = 90; @@ -100,7 +100,7 @@ export class WebhookStore extends DurableObject { /** * Time-based retention sweep. Deletes: - * - processed events older than PURGE_AFTER_DAYS (default 7), and + * - processed events older than PURGE_AFTER_DAYS (default 3), and * - UNPROCESSED events older than UNPROCESSED_PURGE_AFTER_DAYS (default 90). * Returns the per-class purged counts. Bounds DO storage growth from the only * remaining unbounded path (unprocessed rows on abandoned tenants, #236). diff --git a/worker/test/workers/store.test.ts b/worker/test/workers/store.test.ts index 0b52f63..e7bc424 100644 --- a/worker/test/workers/store.test.ts +++ b/worker/test/workers/store.test.ts @@ -185,7 +185,7 @@ describe("WebhookStore: get_event (/event?id=)", () => { }); // received_at relative to the real wall-clock "now". The DO purges processed -// events older than PURGE_AFTER_DAYS (default 7); tests bind received_at relative +// events older than PURGE_AFTER_DAYS (default 3); tests bind received_at relative // to now so they stay stable regardless of the absolute date. function isoFromNow(deltaMs: number): string { return new Date(Date.now() + deltaMs).toISOString(); @@ -195,7 +195,7 @@ const DAY_MS = 86_400_000; describe("WebhookStore: mark-processed", () => { it("flips processed so a within-retention event drops out of pending but remains fetchable by id, returning purged=0", async () => { const stub = storeFor("mark-processed"); - // received_at is recent (1 day ago) so it survives the default 7-day purge window + // received_at is recent (1 day ago) so it survives the default 3-day purge window await ingest(stub, makeEvent({ id: "m1", type: "issues", received_at: isoFromNow(-1 * DAY_MS) })); const mp = await stub.fetch( @@ -223,7 +223,7 @@ describe("WebhookStore: mark-processed", () => { describe("WebhookStore: mark-processed auto-purge", () => { it("deletes processed events older than the retention window and reports the purged count", async () => { const stub = storeFor("purge-old-processed"); - // An old, already-processed event (30 days ago, beyond the 7-day window) + // An old, already-processed event (30 days ago, beyond the 3-day window) await ingest( stub, makeEvent({ id: "old", type: "issues", received_at: isoFromNow(-30 * DAY_MS), processed: true }), @@ -314,9 +314,9 @@ describe("WebhookStore: time-based sweep (DO Alarm body)", () => { expect(status.pending_count).toBe(1); }); - it("purges PROCESSED events older than PURGE_AFTER_DAYS (default 7) in the same sweep — no mark_processed call required", async () => { + it("purges PROCESSED events older than PURGE_AFTER_DAYS (default 3) in the same sweep — no mark_processed call required", async () => { const stub = storeFor("sweep-old-processed"); - // 30 days old, already processed → beyond the 7-day window → swept by the + // 30 days old, already processed → beyond the 3-day window → swept by the // time-based sweep without any mark_processed trigger await ingest(stub, makeEvent({ id: "doneold", type: "issues", received_at: isoFromNow(-30 * DAY_MS), processed: true })); // fresh processed event stays @@ -332,7 +332,7 @@ describe("WebhookStore: time-based sweep (DO Alarm body)", () => { expect(keptRes.status).toBe(200); }); - it("sweeps processed (>7d) and unprocessed (>90d) together while keeping in-window rows of both classes", async () => { + it("sweeps processed (>3d) and unprocessed (>90d) together while keeping in-window rows of both classes", async () => { const stub = storeFor("sweep-mixed"); await ingest(stub, makeEvent({ id: "p-old", received_at: isoFromNow(-30 * DAY_MS), processed: true })); // swept await ingest(stub, makeEvent({ id: "p-new", received_at: isoFromNow(-2 * DAY_MS), processed: true })); // kept diff --git a/worker/wrangler.toml b/worker/wrangler.toml index 200f4da..805bec2 100644 --- a/worker/wrangler.toml +++ b/worker/wrangler.toml @@ -7,7 +7,7 @@ compatibility_flags = ["nodejs_compat"] # Retention window (days) for processed webhook events. On mark_processed and on # the periodic DO Alarm sweep, the WebhookStore DO deletes processed events older # than this to bound DO storage. 0 = purge processed events immediately. -PURGE_AFTER_DAYS = "7" +PURGE_AFTER_DAYS = "3" # Retention window (days) for UNPROCESSED webhook events. The DO Alarm sweep # deletes unprocessed events older than this. Much longer than the processed # window because unprocessed = user-unseen (the asymmetry is intentional). This