Skip to content

fix(email): hand storage reservations to UserMeter - #1136

Merged
kody-bot merged 40 commits into
mainfrom
cursor/mailbox-do-810a
Aug 1, 2026
Merged

kody-bot merged 40 commits into
mainfrom
cursor/mailbox-do-810a

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Aug 1, 2026 •

Copy link
Copy Markdown
Owner

Summary

Routes inbound/outbound email storage-byte reservations through the existing UserMeter accounting interface. Inbound uses Email Routing waitUntil (or awaits when no context); outbound awaits the caught accounting task. UserMeter internals, limits, retry/refund behavior, and Mailbox reads are unchanged.

Conductor report

  • STATUS: done
  • What shipped: durable email storage-reservation handoff to env.USER_METER via shared reserveEmailStorageBytes
  • Risk self-assessment: medium — accounting handoff only; no enforcement limits or delivery semantics changed
  • Merged/deployed: yes — PR #1136, merge, CI, deploy
  • Mailbox read cutover: unchanged/off; earliest gate remains 2026-08-02T15:50:08Z
  • Sibling scope spill: email call sites/helper only; no UserMeter internals/shared files
Open in Web Open in Cursor 

Summary by CodeRabbit

  • Bug Fixes

    • Improved storage quota accounting for inbound and outbound email.
    • Synchronized storage usage accurately with displayed meter values.
    • Prevented over-quota requests from changing storage totals.
    • Improved retry handling to avoid duplicate reservations or messages.
    • Ensured storage accounting completes reliably during email processing.
  • Tests

    • Added coverage for quota enforcement, retry behavior, storage synchronization, successful reservations, and email delivery.

cursoragent and others added 30 commits August 1, 2026 07:25
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Restore deleteEmailMessageById to D1 batch then immediate R2 cleanup with
no Mailbox env/waitUntil/mirror. Restore insertEmailMessageWithAttachments
signature without mirror forwarding. Drop PR-only delete mirror tests and
update data-storage.md: live explicit/retention deletes are repaired by
parity purge/rebuild; direct delete wiring remains pending.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
cursoragent and others added 7 commits August 1, 2026 15:03
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
…doff

Add a focused workers test that sends a successful outbound email, waits for
the best-effort D1→UserMeter storage shadow, and asserts readStorageBytes
matches authoritative users.d1_storage_bytes.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Exercise handleInboundEmail storage-byte reservation with real UserMeter:
capture ExecutionContext waitUntil, drain shadow sync, and assert
readStorageBytes matches authoritative users.d1_storage_bytes. Also cover
over-quota rejection without meter drift and pre-commit retry without
double-reserving D1 storage.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@coderabbitai

coderabbitai Bot commented Aug 1, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Inbound and outbound email flows now use reserveEmailStorageBytes for storage entitlement validation and accounting. Inbound processing supports deferred waitUntil work. Tests verify quota handling, retry-safe reservations, authoritative D1 accounting, and USER_METER synchronization.

Changes

Email storage metering

Layer / File(s) Summary
Shared storage reservation helper
packages/worker/src/email/storage-reservation.ts
reserveEmailStorageBytes validates storage entitlement and either schedules accounting with waitUntil or awaits it directly.
Inbound reservation and retry behavior
packages/worker/src/email/inbound.ts, packages/worker/src/email/inbound-storage-meter.workers.test.ts
Inbound processing passes worker context to the reservation helper. Tests cover successful accounting, quota rejection, blob-storage failure, retry-safe reservations, and single message persistence.
Outbound reservation and meter synchronization
packages/worker/src/email/outbound.ts, packages/worker/src/email/outbound-storage-shadow.workers.test.ts
Outbound processing uses the reservation helper. Tests verify authoritative D1 storage usage and matching USER_METER values.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • kentcdodds/kody#1115: Both changes integrate environment-aware storage accounting into inbound and outbound email handling.
  • kentcdodds/kody#1118: This change applies related D1-authoritative storage reservation and UserMeter synchronization to email flows.
  • kentcdodds/kody#1133: Both changes update outbound email metering in packages/worker/src/email/outbound.ts.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: routing email storage reservations through UserMeter.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/mailbox-do-810a

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@kody-bot
kody-bot marked this pull request as ready for review August 1, 2026 18:32
@github-actions

github-actions Bot commented Aug 1, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-1136.kody-a99.workers.dev

Worker: kody-pr-1136
D1: kody-pr-1136-db
KV: kody-pr-1136-oauth-kv

Mocks:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
packages/worker/src/email/inbound.ts (1)

479-491: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hoist the single waitUntil adapter instead of building it twice.

Lines 554-556 create the same ctx-bound adapter as lines 488-490. Move that const waitUntil declaration above this block and reuse it here. This keeps one definition of the deferral contract in handleInboundEmail.

♻️ Proposed refactor
+			const waitUntil = ctx
+				? (promise: Promise<unknown>) => ctx.waitUntil(promise)
+				: undefined
 			let delivery = activeWindow ?? candidateDelivery
 					await assertWithinStorageBytesEntitlement({
 						db: env.APP_DB,
 						env,
 						userId,
 						email: account.email,
 						requested: estimateInboundEmailStorageBytes({
 							message,
 							recipient,
 						}),
-						waitUntil: ctx
-							? (promise: Promise<unknown>) => ctx.waitUntil(promise)
-							: undefined,
+						waitUntil,
 					})

Then remove the later duplicate declaration at lines 554-556.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/email/inbound.ts` around lines 479 - 491, In
handleInboundEmail, hoist the ctx-bound waitUntil adapter above the
assertWithinStorageBytesEntitlement call and pass that shared variable into the
request. Remove the duplicate waitUntil declaration later in the function so the
deferral contract has one definition.
packages/worker/src/email/inbound-storage-meter.workers.test.ts (2)

108-296: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Split this test into separate test cases per scenario.

One test now covers five independent scenarios: happy-path shadowing, over-quota rejection, blob-failure reservation retention, retry without double reservation, and single-message persistence. The scenarios share no required state, because each one seeds its own user. A failure in the first scenario hides the remaining four, and the 30-second timeout applies to the whole chain.

Extract the over-quota scenario (lines 172-213) and the retry scenario (lines 215-293) into their own tests. Move ensureEmailTestSchema and ensureUsageRollupsTestSchema into a shared setup helper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/email/inbound-storage-meter.workers.test.ts` around lines
108 - 296, Split the combined inbound storage test into independent test cases
for happy-path shadowing, over-quota rejection, and retry behavior, preserving
each scenario’s existing assertions and isolated seeded user state. Extract the
over-quota block and retry block from the current test into separate tests, and
add a shared setup helper that performs ensureEmailTestSchema and
ensureUsageRollupsTestSchema before each case.

45-60: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Import the production estimator instead of re-implementing it.

inbound.ts already defines estimateInboundEmailStorageBytes, but it is not exported. Make it available to inbound-storage-meter.workers.test.ts and import it here so the test exercises the same byte formula used by production.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/email/inbound-storage-meter.workers.test.ts` around lines
45 - 60, Export estimateInboundEmailStorageBytes from inbound.ts and replace the
duplicate local implementation in inbound-storage-meter.workers.test.ts with an
import of that production function, ensuring the test uses the same storage-byte
calculation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/worker/src/email/inbound.ts`:
- Around line 479-491: Update the inbound email entitlement flow around
assertWithinStorageBytesEntitlement to ensure UserMeter shadow work completes
without an ExecutionContext: pass ctx.waitUntil when ctx exists, and otherwise
await the scheduled shadow promise before the request exits. Preserve the
existing entitlement validation and use the existing
scheduleUserMeterStorageBytesShadow path rather than bypassing it.

---

Nitpick comments:
In `@packages/worker/src/email/inbound-storage-meter.workers.test.ts`:
- Around line 108-296: Split the combined inbound storage test into independent
test cases for happy-path shadowing, over-quota rejection, and retry behavior,
preserving each scenario’s existing assertions and isolated seeded user state.
Extract the over-quota block and retry block from the current test into separate
tests, and add a shared setup helper that performs ensureEmailTestSchema and
ensureUsageRollupsTestSchema before each case.
- Around line 45-60: Export estimateInboundEmailStorageBytes from inbound.ts and
replace the duplicate local implementation in
inbound-storage-meter.workers.test.ts with an import of that production
function, ensuring the test uses the same storage-byte calculation.

In `@packages/worker/src/email/inbound.ts`:
- Around line 479-491: In handleInboundEmail, hoist the ctx-bound waitUntil
adapter above the assertWithinStorageBytesEntitlement call and pass that shared
variable into the request. Remove the duplicate waitUntil declaration later in
the function so the deferral contract has one definition.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f333e065-60c0-4366-ac94-7863eb3fb3e9

📥 Commits

Reviewing files that changed from the base of the PR and between a9c01a6 and 0b35244.

📒 Files selected for processing (4)
  • packages/worker/src/email/inbound-storage-meter.workers.test.ts
  • packages/worker/src/email/inbound.ts
  • packages/worker/src/email/outbound-storage-shadow.workers.test.ts
  • packages/worker/src/email/outbound.ts

Comment thread packages/worker/src/email/inbound.ts Outdated
cursoragent and others added 2 commits August 1, 2026 18:38
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/worker/src/email/outbound.ts`:
- Line 572: Update the outbound email flow around reserveEmailStorageBytes to
compensate every reservation when consumeDailyEntitlement, message persistence,
or attachment storage fails before data is committed. Add an idempotent
refund-or-commit workflow for each pre-persistence failure path, and
specifically refund the attachment reservation when attachment storage fails,
while preserving successful committed reservations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 439736d0-5a96-45d1-81e3-99974bf37986

📥 Commits

Reviewing files that changed from the base of the PR and between 928412d and 75e4f89.

📒 Files selected for processing (3)
  • packages/worker/src/email/inbound.ts
  • packages/worker/src/email/outbound.ts
  • packages/worker/src/email/storage-reservation.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/worker/src/email/inbound.ts

references: input.references ?? [],
})
await assertWithinStorageBytesEntitlement({
await reserveEmailStorageBytes({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Compensate storage reservations when the reserved bytes are not persisted.

reserveEmailStorageBytes durably increments users.d1_storage_bytes before consumeDailyEntitlement at Line 592 and before message persistence at Line 618. If the daily-send check or a later write fails, the request retains storage bytes for data that does not exist. If attachment storage fails at Line 660, the reservation also retains attachment bytes that were not stored.

Add an idempotent refund or commit workflow for every pre-persistence failure path. Refund the attachment portion when attachment storage fails.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/email/outbound.ts` at line 572, Update the outbound email
flow around reserveEmailStorageBytes to compensate every reservation when
consumeDailyEntitlement, message persistence, or attachment storage fails before
data is committed. Add an idempotent refund-or-commit workflow for each
pre-persistence failure path, and specifically refund the attachment reservation
when attachment storage fails, while preserving successful committed
reservations.

@kody-bot
kody-bot merged commit d03e332 into main Aug 1, 2026
10 checks passed
@kody-bot
kody-bot deleted the cursor/mailbox-do-810a branch August 1, 2026 18:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants