Skip to content

fix(backend): harden cron sync retries#1929

Merged
riderx merged 3 commits into
mainfrom
codex/cron-sync-sub-retries
Apr 22, 2026
Merged

fix(backend): harden cron sync retries#1929
riderx merged 3 commits into
mainfrom
codex/cron-sync-sub-retries

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Apr 21, 2026

Summary (AI generated)

  • retry cron_sync_sub when the trigger fails with transient 5xx / PostgREST-style errors
  • skip stale queue jobs cleanly when the target organization no longer exists
  • preserve real org lookup failures as cannot_get_org instead of misreporting them as org_not_found
  • add unit coverage for retry success and stale-org skip behavior

Motivation (AI generated)

cron_sync_sub queue alerts were retrying noisy transient backend failures and stale queue payloads without any handler-side resilience. The trigger needed the same kind of retry-and-skip behavior already added for other hot queue paths.

Business Impact (AI generated)

This should reduce avoidable billing-sync alert noise, keep queue retries focused on real failures, and prevent stale org jobs from repeatedly surfacing in Discord and PostHog. It lowers operational noise without changing the successful subscription-sync path.

Test Plan (AI generated)

  • bunx vitest run tests/cron_sync_sub.unit.test.ts
  • bunx eslint supabase/functions/_backend/triggers/cron_sync_sub.ts supabase/functions/_backend/utils/plans.ts tests/cron_sync_sub.unit.test.ts
  • bun typecheck

Generated with AI

Summary by CodeRabbit

  • New Features

    • Automatic retry with exponential backoff for subscription sync, tracking attempts.
  • Bug Fixes

    • Sync now skips gracefully when an organization is missing, returning a clear reason.
    • Better distinction between transient (retryable) and permanent errors, with improved final-error logging.
  • Tests

    • Added resilience tests for retry behavior and missing-organization skip.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 859351cf-f920-4941-a7cc-080088bffa60

📥 Commits

Reviewing files that changed from the base of the PR and between 053570e and f7d672b.

📒 Files selected for processing (1)
  • tests/cron_sync_sub.unit.test.ts
✅ Files skipped from review due to trivial changes (1)
  • tests/cron_sync_sub.unit.test.ts

📝 Walkthrough

Walkthrough

Added retry-on-transient-failure logic and enhanced error classification/logging to the cron subscription sync handler; updated org lookup behavior to return 404 vs 500 more precisely; added unit tests for retry and org-not-found paths.

Changes

Cohort / File(s) Summary
Cron trigger + retry logic
supabase/functions/_backend/triggers/cron_sync_sub.ts
Introduced retry wrapper around syncSubscriptionAndEvents, added retry config, HTTP/error classification, improved logging/serialization, special-cased org_not_found to return {status:'skipped'} instead of throwing.
Org lookup behavior
supabase/functions/_backend/utils/plans.ts
Switched getOrgWithCustomerInfo query from .single() to .maybeSingle(), return 500 on DB/user error and 404 when org is missing (distinct handling).
Unit tests
tests/cron_sync_sub.unit.test.ts
Added tests covering retry-on-transient-failure (retry then succeed) and org-not-found (skipped) behaviors; includes mocks for hono, pg, and plans utilities.

Sequence Diagram(s)

(Skipped — changes are localized and sequencing is simple.)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

codex

"I hopped through logs at break of dawn,
retried a stumble until the sync was on.
If an org's absent I skip with grace,
and log the trail of every chase.
— 🐇"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% 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 clearly summarizes the main change: adding retry logic and resilience to the cron sync trigger.
Description check ✅ Passed The description includes a comprehensive summary, motivation, business impact, and test plan; all key required sections are well-documented.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 codex/cron-sync-sub-retries

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

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq Bot commented Apr 21, 2026

Merging this PR will not alter performance

✅ 28 untouched benchmarks


Comparing codex/cron-sync-sub-retries (f7d672b) with main (a221796)

Open in CodSpeed

@riderx riderx marked this pull request as ready for review April 21, 2026 17:11
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tests/cron_sync_sub.unit.test.ts (1)

15-31: Extract shared mock setup to reduce drift.

Line 15-31 and Line 50-65 repeat the same module mocks. A local setup helper would keep these tests easier to maintain.

♻️ Suggested refactor
 describe('cron_sync_sub resilience', () => {
+  function setupCommonMocks(syncSubscriptionAndEvents: ReturnType<typeof vi.fn>) {
+    vi.doMock('../supabase/functions/_backend/utils/hono.ts', () => ({
+      BRES: { status: 'ok' },
+      middlewareAPISecret: async (_c: unknown, next: () => Promise<void>) => await next(),
+      parseBody: async (c: { req: { json: () => Promise<unknown> } }) => await c.req.json(),
+      simpleError: (errorCode: string, message: string, moreInfo: Record<string, unknown> = {}) => {
+        throw new HTTPException(400, { message, cause: { error: errorCode, ...moreInfo } })
+      },
+    }))
+    vi.doMock('../supabase/functions/_backend/utils/pg.ts', () => ({
+      closeClient: vi.fn(),
+      getDrizzleClient: vi.fn(() => ({ drizzle: true })),
+      getPgClient: vi.fn(() => ({ pg: true })),
+    }))
+    vi.doMock('../supabase/functions/_backend/utils/plans.ts', () => ({
+      syncSubscriptionAndEvents,
+    }))
+  }
+
   it('retries transient cron_sync_sub failures and succeeds', async () => {
     const syncSubscriptionAndEvents = vi.fn()
       .mockRejectedValueOnce({ status: 502, message: 'error code: 502' })
       .mockResolvedValueOnce(undefined)
-
-    vi.doMock(...)
-    vi.doMock(...)
-    vi.doMock(...)
+    setupCommonMocks(syncSubscriptionAndEvents)
   })
 
   it('skips stale cron_sync_sub jobs when the org no longer exists', async () => {
     const syncSubscriptionAndEvents = vi.fn().mockRejectedValue(
       new HTTPException(404, { message: 'Org not found', cause: { error: 'org_not_found' } }),
     )
-
-    vi.doMock(...)
-    vi.doMock(...)
-    vi.doMock(...)
+    setupCommonMocks(syncSubscriptionAndEvents)
   })
 })

Also applies to: 50-65

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/cron_sync_sub.unit.test.ts` around lines 15 - 31, Extract the repeated
vi.doMock blocks into a shared test helper (e.g., export a setupCommonMocks
function) that encapsulates the mocks for
'../supabase/functions/_backend/utils/hono.ts',
'../supabase/functions/_backend/utils/pg.ts', and
'../supabase/functions/_backend/utils/plans.ts' (including the
BRES/middlewareAPISecret/parseBody/simpleError mocks,
closeClient/getDrizzleClient/getPgClient mocks, and the
syncSubscriptionAndEvents passthrough), then replace the duplicated vi.doMock
blocks in tests/cron_sync_sub.unit.test.ts (lines 15-31 and 50-65) with a single
call to that helper; ensure the helper exports the same mock objects/functions
and import/use it in any other test files that repeat these mocks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/cron_sync_sub.unit.test.ts`:
- Around line 15-31: Extract the repeated vi.doMock blocks into a shared test
helper (e.g., export a setupCommonMocks function) that encapsulates the mocks
for '../supabase/functions/_backend/utils/hono.ts',
'../supabase/functions/_backend/utils/pg.ts', and
'../supabase/functions/_backend/utils/plans.ts' (including the
BRES/middlewareAPISecret/parseBody/simpleError mocks,
closeClient/getDrizzleClient/getPgClient mocks, and the
syncSubscriptionAndEvents passthrough), then replace the duplicated vi.doMock
blocks in tests/cron_sync_sub.unit.test.ts (lines 15-31 and 50-65) with a single
call to that helper; ensure the helper exports the same mock objects/functions
and import/use it in any other test files that repeat these mocks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 275d6d3b-8595-4418-ac33-7a235b2ef197

📥 Commits

Reviewing files that changed from the base of the PR and between a221796 and 7e42360.

📒 Files selected for processing (3)
  • supabase/functions/_backend/triggers/cron_sync_sub.ts
  • supabase/functions/_backend/utils/plans.ts
  • tests/cron_sync_sub.unit.test.ts

@sonarqubecloud
Copy link
Copy Markdown

@riderx riderx merged commit 0746ea7 into main Apr 22, 2026
15 checks passed
@riderx riderx deleted the codex/cron-sync-sub-retries branch April 22, 2026 09:34
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.

1 participant