Skip to content

fix(workers): monitor dead-letter queues without consuming jobs - #324

Open
henrique221 wants to merge 6 commits into
mainfrom
fix/256-worker-dead-letter-observability
Open

henrique221 wants to merge 6 commits into
mainfrom
fix/256-worker-dead-letter-observability

Conversation

@henrique221

@henrique221 henrique221 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

I added a shared dead-letter queue convention for export, AI suggestions and DBL workers. The API reports DLQ depth every minute through the existing logger, including when the export worker cannot boot because R2 is unavailable. The monitor never consumes or replays jobs. Queue reads run concurrently, failures stay isolated, and monitor shutdown waits at most five seconds.

New DLQ entries get at least 30 days of retention. Longer queue settings and existing job rows stay intact during startup. Both export and AI queues report a policy mismatch. Queue setup creates fresh queues with their final settings and updates existing queues without deleting them.

For legacy non-exclusive queues, I added an explicit offline migration and runbook. It locks the queue/job tables, refuses pending work, and changes policy metadata without deleting queues or history. It supports shared and dedicated pg-boss 12.1.1 partitions and preserves IDs, payloads, errors, states, retry counters, routing and deadlines. Deployment does not run this migration automatically.

Validation passed: 618 unit tests, 8 PostgreSQL 16 integration tests, typecheck, lint, formatting, build and docs checks. Lint has three existing verse-audio warnings. The integration suite runs the real export/AI worker handlers with external services replaced by fixtures, and verifies retry exhaustion, recovery, timeout, retention and preserved messages. It also proves migration refusal with pending work, unchanged history apart from policy metadata, idempotence, and singleton dedupe after both partition migrations. The production logger test verifies flat Application Insights dimensions without sending telemetry.

Live Azure alert rules still need to be configured by the environment owner. No production queue or infrastructure was accessed.

Closes #256.

Screenshots

Local backend smoke on edf3bfd, captured with Playwright from a report of real PostgreSQL 16.13 snapshots and events emitted by the PR's monitor. Controlled pg-boss failures use one immediate retry; R2 and AI services are not called in this capture.

1) Retry stays out of the DLQ

The source job remains in retry. The DLQ has no row, and the monitor reports depth: 0 at info level.

Retry remains on the source queue with DLQ depth zero

2) Terminal failure becomes observable

Exhausting retries creates a retained DLQ row and a structured warning with depth: 1. Two monitor sweeps preserve the captured fields, including state, payload, failure output and the 30-day deadline.

Terminal failure creates a retained DLQ row and a structured warning

3) Existing evidence is preserved

Repeated queue setup preserves the existing DLQ message and its original deadline. A legacy export queue keeps its failed-job history and reports the policy mismatch.

Existing source and dead-letter job evidence survives queue setup

@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 59 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: cfa27274-096d-4ffe-81d6-fc610c72dda2

📥 Commits

Reviewing files that changed from the base of the PR and between daff838 and 896e738.

📒 Files selected for processing (16)
  • .github/workflows/pre-merge.yml
  • docs/runbooks/worker-dead-letter-queues.md
  • package.json
  • src/db/scripts/migrate-worker-queue-policy.ts
  • src/index.ts
  • src/lib/dead-letter-queues.integration.test.ts
  • src/lib/dead-letter-queues.test.ts
  • src/lib/dead-letter-queues.ts
  • src/lib/dead-letter-telemetry.test.ts
  • src/lib/exclusive-worker-queue-migration.ts
  • src/lib/queue.ts
  • src/workers/dbl-sync.worker.test.ts
  • src/workers/dbl-sync.worker.ts
  • src/workers/ingest-bible-text.worker.test.ts
  • src/workers/ingest-bible-text.worker.ts
  • src/workers/standalone-worker.ts

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.

@henrique221 henrique221 self-assigned this Sep 9, 2026
Comment thread src/lib/queue.ts Outdated
Comment thread src/lib/dead-letter-queues.ts Outdated
Comment thread src/lib/dead-letter-queues.ts Outdated
Comment thread src/lib/dead-letter-queues.ts
Comment thread src/lib/queue.ts
Add an explicit offline policy migration that preserves retained jobs and
refuses pending work. Report policy drift for both exclusive worker queues,
avoid redundant creation updates, read DLQ stats concurrently, and bound
monitor shutdown. Cover the migration with real PostgreSQL integration tests.

Refs: #324

@kaseywright kaseywright 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.

Re-reviewed after the latest push (aa96cd0) — the queue-policy migration script, expanded runbook, and added tests resolve the earlier findings well. No correctness bugs found; three cleanup-only items below worth a look before merge.

Comment thread src/lib/exclusive-worker-queue-migration.ts Outdated
Comment thread src/lib/dead-letter-queues.ts Outdated
Comment thread src/lib/dead-letter-queues.ts

@kaseywright kaseywright 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.

Re-reviewed after 4faf47d — the earlier three findings (hardcoded queue names, unconditional updateQueue writes, missing schema-version guard) are all properly resolved with test coverage. A few new items from this commit worth a look before merge.

Comment thread src/index.ts
logger.info(`${signal} received, shutting down server`);
try {
if (audioReclaimInterval) clearInterval(audioReclaimInterval);
await stopDeadLetterMonitor();

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.

await stopDeadLetterMonitor() runs before server.close() in the shutdown sequence. Since the monitor can take up to DLQ_SHUTDOWN_TIMEOUT_MS (5s) to resolve, this delays the HTTP listener closing (and rejecting new connections) by up to 5s, eating into the orchestrator's shutdown grace period for no benefit — the sweep could drain concurrently with server.close() instead. Can you confirm this ordering is intentional?

if (apply) {
// Includes all partitions. No sender, worker or maintenance process can
// change jobs between the pending-work check and the policy update.
await tx`LOCK TABLE pgboss.queue, pgboss.job IN ACCESS EXCLUSIVE MODE`;

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.

This ACCESS EXCLUSIVE lock on pgboss.queue/pgboss.job is acquired before the no-op check below (queue.policy === 'exclusive'). The runbook documents re-running the migration after success as a no-op, but as written it still stalls every queue's job fetch/complete/send system-wide for up to the 5s lock timeout just to discover there's nothing to do. Worth moving the policy check before the lock? Can you confirm this is intentional?

/** Time to investigate new DLQ entries before pg-boss maintenance removes them. */
export const DLQ_RETENTION_SECONDS = 30 * 24 * 60 * 60;
export const DLQ_SHUTDOWN_TIMEOUT_MS = 5_000;
const PG_BOSS_SCHEMA_VERSION = 26;

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.

PG_BOSS_SCHEMA_VERSION = 26 is now hardcoded independently here and in exclusive-worker-queue-migration.ts (also literal 26). A future pg-boss schema bump could get one file updated and not the other, silently disabling DLQ monitoring while the migration script still runs (or vice versa). Worth extracting a shared constant? Can you confirm this is intentional?

Comment thread src/lib/queue.ts
retryBackoff: true,
expireInSeconds: 600,
deadLetter: QUEUE_NAMES.USFM_EXPORT_DLQ,
} as const;

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.

QUEUE_NAMES.USFM_EXPORT_DLQ is still dead in production code — ensureWorkerQueue derives the DLQ name itself as ${name}-dlq rather than consuming this constant, so only a test file references it. Flagged in an earlier round too; leaving two independent spellings of the same fact risks drift if either is renamed. Can you confirm this is intentional?

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.

Worker fleet: dead-letter queue strategy (usfm-export-dlq has no consumer)

2 participants