Template: checkout expiry Worker processor
apps/cron-control/src/processors/checkout-expiry.ts ports Inventory’s
checkout-expiry processor. It runs only when inventory-checkout-expiry is in
both CRON_CONTROL_ENABLED_JOBS and CRON_CONTROL_WORKER_PROCESSORS, so the
schedule can move first and the processor later. Both vars are empty by default.
- Same claim. The Worker calls the private
expire_inventory_checkout_sessionsRPC directly with the service key inapikey. The RPC locks candidates withFOR UPDATE SKIP LOCKEDand only movesreservedrows, so a concurrent legacy run cannot double-expire a checkout. - Expiry bounds and backpressure. At most 10 batches of 1,000 rows per slot,
one shared
p_now, 30-second database deadlines and manual redirects. Check the durable stop fence before every batch and revalidation chunk. Active obligations and quarantined work share a 5,000-workspace bound. Reserve room for a worst-case 1,000 distinct workspaces before each expiry RPC; stop expiry when capacity is insufficient. Ledger overflow is refused atomically; no oldest-item eviction. - Whole-run deadline. Claim, HTTP headers/body, pending-ledger RPCs, stop checks and completion share an absolute 240-second deadline inside the 270-second lease. HTTP requests also retain their shorter timeout. A timed-out ledger call can still complete remotely; do not replay it immediately. No dependent operation starts after timeout. Late obligation settlement and retry charges are fenced by the original claim’s generation and slot. An unconfirmed completion leaves the claim to be charged as unknown by the next slot rather than reporting success.
- Owed work first. Outstanding revalidation runs before any expiry, with up to 120 seconds reserved for that phase. If owed work remains, start no new expiry. Fresh expiries persist obligations before revalidation. Incomplete work, capacity exhaustion and quarantine never count as a successful slot.
- Per-workspace receipts. Next.js owns storefront cache invalidation. The Worker
posts at most 100 workspace UUIDs to Inventory’s authenticated POST adapter
(8 KiB input bound). The adapter uses the throwing invalidation helper, isolates
each workspace failure and returns
results: [{ wsId, ok }]. Only an explicitok: truereceipt for a requested workspace settles its obligation. Missing, contradictory, failed or legacy aggregate-only receipts stay owed. - Poison isolation. A failed chunk does not prevent later chunks from running
within the remaining time. Each unconfirmed item gets one durable attempt charge
per sweep. After three attempts it moves to bounded
quarantinedWork; the item is retained, occupies capacity and is not retried automatically. Healthy chunks can still settle. Inspect quarantine through the authenticated ledger status; repair and confirm invalidation before deliberately settling it. Resume alone preserves quarantine. Five failed or unknown slots still stop the entire job.
CRON_CONTROL_WORKER_PROCESSORS in a separate change. Verify one slot
with detail.expired and storefront freshness for a test checkout, then remove the
Vercel cron. Roll back by removing the id from CRON_CONTROL_WORKER_PROCESSORS;
the job returns to calling the Next route.
Regression coverage: src/processors/checkout-expiry.test.ts counts RPC and
revalidation calls for idle, draining, capped, stopped, partially failed, deadline,
malformed and chunked cases; src/dispatch.test.ts proves the port runs only when
selected; the Inventory route test covers revalidation auth and input bounds.
Per-job plan
Ports that need Next.js-only effects (cache tags, React email rendering) keep a
small authenticated adapter in the owning app, called only when work happened,
until that app itself moves to Cloudflare. Durable business work never relies on
ctx.waitUntil.