Skip to content

Recover stale webhook deliveries#367

Open
ag-linden wants to merge 1 commit into
usemarble:mainfrom
ag-linden:fix/webhook-delivery-recovery
Open

Recover stale webhook deliveries#367
ag-linden wants to merge 1 commit into
usemarble:mainfrom
ag-linden:fix/webhook-delivery-recovery

Conversation

@ag-linden

@ag-linden ag-linden commented Jul 1, 2026

Copy link
Copy Markdown

Description

This updates the webhook delivery worker so queue retries can recover delivery rows that were left in sending by an interrupted attempt.

Changes included:

  • Reclaim stale sending deliveries after the outbound delivery timeout window.
  • Keep active sending deliveries on the queue retry path instead of acknowledging a skipped delivery.
  • Preserve existing behavior for terminal or missing deliveries.
  • Add focused worker tests for normal claims, stale sending recovery, active sending retry behavior, and terminal delivery skips.

Motivation and Context

The delivery worker marks a webhookDelivery row as sending before making the outbound POST. If the worker is interrupted before it records the attempt result and final status, the next queue delivery sees the row in sending. Previously that failed the pending/retrying claim and returned successfully, which acknowledged the queue message while leaving the row permanently in progress.

This keeps the current durable claim model, but makes sending recoverable once the prior attempt is stale. Non-stale sending rows still retry later, which avoids treating an actively owned delivery as complete.

How to Test

Passed locally:

corepack pnpm --filter jobs test
corepack pnpm --filter @marble/db db:generate
corepack pnpm exec tsc -p apps/jobs/tsconfig.json --noEmit
git diff --check
corepack pnpm test

Local blockers unrelated to this patch:

corepack pnpm lint

The root lint script invokes pnpx, which is not available in the pinned Corepack pnpm environment. Running the equivalent corepack pnpm exec ultracite check reaches the existing root biome.jsonc and stops on unknown config keys noShadow and noUnnecessaryConditions before checking changed files.

corepack pnpm build

The full build stops in web#build because the local environment does not define MARBLE_API_KEY for the Astro content config.

Screenshots (if applicable)

N/A

Video Demo (if applicable)

N/A

Types of Changes

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • ⚠️ Breaking change (fix or feature that alters existing functionality)
  • 🎨 UI/UX Improvements
  • ⚡ Performance Enhancement
  • 📖 Documentation (updates to README, docs, or comments)

Summary by CodeRabbit

  • Bug Fixes

    • Improved reliability of webhook delivery handling, including better recovery from stuck in-progress deliveries.
    • Prevents duplicate processing when a delivery is already actively being sent.
    • Keeps completed deliveries from being retried incorrectly.
  • Tests

    • Added test coverage for delivery claim scenarios to validate pending, retrying, stale, active, and completed states.
  • Chores

    • Added test tooling and configuration for the jobs package.

@vercel

vercel Bot commented Jul 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
marble-app Skipped Skipped Jul 1, 2026 2:23am

Request Review

@CLAassistant

CLAassistant commented Jul 1, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@vercel vercel Bot temporarily deployed to Preview – marble-app July 1, 2026 02:23 Inactive
@vercel

vercel Bot commented Jul 1, 2026

Copy link
Copy Markdown

@ag-linden is attempting to deploy a commit to the Marble Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds a stale-sending reclaim mechanism for webhook deliveries in the jobs app: a new constant, a claimWebhookDeliveryAttempt function replacing inline claim logic in processDelivery, plus Vitest test infrastructure (config, package script, dependency) and a new test suite covering the claim/reclaim/error/terminal-state scenarios.

Changes

Webhook Delivery Claiming

Layer / File(s) Summary
Stale-sending constant
apps/jobs/src/lib/constants.ts
Adds WEBHOOK_DELIVERY_STALE_SENDING_MS, defined as WEBHOOK_DELIVERY_TIMEOUT_MS * 2.
Claim/reclaim implementation and wiring
apps/jobs/src/consumers/deliveries.ts
Adds exported claimWebhookDeliveryAttempt, which atomically claims pending/retrying deliveries, reclaims stale sending deliveries, throws if actively sending, or returns false; processDelivery now calls this instead of an inline updateMany.
Vitest setup and claim tests
apps/jobs/vitest.config.mjs, apps/jobs/package.json, apps/jobs/src/consumers/deliveries.test.ts
Adds Vitest config with @ alias and test include pattern, a test script and vitest dev dependency, and a test suite with mocked DB helper covering claim, stale reclaim, active-sending error, and terminal-state skip cases.

🎯 3 (Moderate) | ⏱️ ~25 minutes

Hop hop, the deliveries queue was stuck,
Stale "sending" jobs had run out of luck.
Now claims retry with a timely check,
No more zombie webhooks left to wreck!
🐇✨ Tests all green, my whiskers twitch with glee.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: recovering stale webhook deliveries.
Description check ✅ Passed The description follows the template with clear sections for description, motivation, testing, media placeholders, and change type.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 644adb52ac

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

where: {
id: deliveryId,
status: "sending",
OR: [{ lastAttemptAt: { lt: staleCutoff } }, { lastAttemptAt: null }],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Limit stale claims to remaining attempts

When a worker is interrupted after claiming the last allowed attempt, the row is left sending with attemptCount === maxAttempts; this stale predicate still matches it, then the retry increments attemptCount again and processDelivery only checks attemptNumber >= maxAttempts after making the POST. In that recovery case the customer endpoint can receive a fourth request for the default maxAttempts = 3 (possibly duplicating a request that already reached them), so the stale-reclaim path should guard on remaining attempts or make the delivery terminal before sending again.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 `@apps/jobs/src/consumers/deliveries.ts`:
- Around line 43-45: The delivery claim flow in claimWebhookDeliveryAttempt and
the later webhookDelivery.update* status transitions need a lease check, not
just a boolean claim result. Return claim metadata such as lastAttemptAt or a
dedicated token from claimWebhookDeliveryAttempt, then require every subsequent
state write in deliveries.ts to include that same token in its where clause
before updating the row. Update the later success/failed/retrying writes to
conditional updateMany-style mutations so a reclaimed worker cannot overwrite
the current attempt’s state.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cde6bf5b-884d-4fe2-a6ad-3ff5068bd31f

📥 Commits

Reviewing files that changed from the base of the PR and between c701f52 and 644adb5.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • apps/jobs/package.json
  • apps/jobs/src/consumers/deliveries.test.ts
  • apps/jobs/src/consumers/deliveries.ts
  • apps/jobs/src/lib/constants.ts
  • apps/jobs/vitest.config.mjs

Comment on lines +43 to +45
const claimed = await claimWebhookDeliveryAttempt(db, deliveryId);

if (claim.count === 0) {
if (!claimed) {

Copy link
Copy Markdown
Contributor

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

Fence reclaimed attempts before any later delivery-state write.

After a stale reclaim, two workers can exist for the same delivery. The helper only returns boolean, so the later writes at Line 81, Line 136, Line 146, Line 188, Line 199, and Line 209 are still keyed only by id. If the original worker resumes after the reclaim, it can overwrite the newer attempt's success/failed/retrying state.

Please return claim metadata from claimWebhookDeliveryAttempt (for example the lastAttemptAt written during the claim, or a dedicated lease token) and require every later webhookDelivery.update* in this attempt to match that token in where before mutating the row again.

Possible direction
-  const claimed = await claimWebhookDeliveryAttempt(db, deliveryId);
-  if (!claimed) {
+  const claim = await claimWebhookDeliveryAttempt(db, deliveryId);
+  if (!claim) {
     return;
   }
-export async function claimWebhookDeliveryAttempt(...)
+export async function claimWebhookDeliveryAttempt(...): Promise<{ claimedAt: Date } | null>
...
-    return true;
+    return { claimedAt: now };
...
-    return true;
+    return { claimedAt: now };
...
-  return false;
+  return null;

Then switch the later status transitions to conditional updateMany(...) calls such as where: { id: delivery.id, lastAttemptAt: claim.claimedAt } so a reclaimed worker cannot clobber the current owner.

Also applies to: 217-268

🤖 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 `@apps/jobs/src/consumers/deliveries.ts` around lines 43 - 45, The delivery
claim flow in claimWebhookDeliveryAttempt and the later webhookDelivery.update*
status transitions need a lease check, not just a boolean claim result. Return
claim metadata such as lastAttemptAt or a dedicated token from
claimWebhookDeliveryAttempt, then require every subsequent state write in
deliveries.ts to include that same token in its where clause before updating the
row. Update the later success/failed/retrying writes to conditional
updateMany-style mutations so a reclaimed worker cannot overwrite the current
attempt’s state.

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.

2 participants