Skip to Content
Internal docs are powered by Nextra Docs Theme.
SystemsPlatformAPI2Rails to API2 migrationPackagesDispute Jira resolution sweep

Dispute Jira resolution sweep

Moves the hourly Disputes::ResolveDoneJiraIssuesJob sweep from Rails/GoodJob to API2/BullMQ. The GoodJob dashboard showed the job succeeding every hour (15 * * * *) ahead of the September 9 cutover.

Class coverage

Rails classProducer(s)API2 ownerState
Disputes::ResolveDoneJiraIssuesJobGoodJob cron resolve_done_jira_issues (15 * * * *)dispute-jira-resolution queue, tick job, scheduler dispute-jira-resolution-tickMigrated; Rails cron disabled by default

The Rails job class remains deployed so already-queued rows can drain and the rollback path works without a deploy.

Contract

  • Scheduler dispute-jira-resolution-tick, cron 15 * * * * UTC — identical to the GoodJob entry — emits one tick job.
  • The tick re-checks the kill switch, then lists candidates with the same predicate the job used: disputes.status = 'open' joined to jira_issues.status_category = 'done', ordered by dispute id, capped at 5,000 per tick (truncated when capped). It fans out one dispute job per row with scannedAt stamped once; any enqueue failure fails the tick (AggregateError) so BullMQ retries it.
  • Job id dispute-jira-resolution-<dispute_id>, 3 attempts, exponential backoff from 10 s. A finished job under the id is removed and re-added, a waiting/active job absorbs the new tick.
  • The dispute worker re-checks the kill switch at execution time (a job queued before the switch flips must not race a Rails rollback), then runs the resolution natively in api2 — Dispute#resolve_from_jira_issue! ported to pure SQL in src/lib/dispute-jira-resolution/resolve.ts: re-check the Jira issue status category, mark the dispute resolved, resolve linked customer complaints, and recompute dispute risk — all org-scoped, matching the ActsAsTenant behavior of the retired loop.
  • A missing dispute (deleted between tick and run) completes as skipped, matching discard_on ActiveRecord::RecordNotFound. Genuine failures raise and BullMQ retries (3 attempts, exponential backoff) — better than the retired job’s log-only in-loop rescue — and the next hourly tick re-drives any still-open dispute.

Rollout controls

ControlStoreSafe stateEffect
pause_api2_dispute_jira_resolutionsFlipper (API2 reads flipper_features)offTick completes as paused without fan-out; queued dispute jobs complete as skipped. Owner: disputes engineering; created 2026-09-09; review/remove by 2026-12-08.
resolve_done_jira_issues crongood_job.rb, enabled_by_default: falsedisabledEnable in the GoodJob dashboard (or revert the line) only after pausing API2.

Rollback order

  1. Enable pause_api2_dispute_jira_resolutions, confirm the next tick logs paused.
  2. Enable the resolve_done_jira_issues cron in the GoodJob dashboard (or revert the line) to restore the hourly Rails sweep. Dispute jobs already queued in BullMQ are idempotent — resolve_from_jira_issue! returns early on already-resolved disputes.

Bake gates

Ownership is asserted only after, in both regions:

  • Two consecutive 15-past-the-hour ticks complete with every candidate enqueued and zero failed dispute jobs.
  • Zero new normal-path GoodJob rows for Disputes::ResolveDoneJiraIssuesJob for 24 hours.
  • Open disputes continue to resolve at the Jira status_category_change_date timestamp with matching complaint and risk-level side effects.

After 24 clean hours, delete Disputes::ResolveDoneJiraIssuesJob, the cron entry and the flag.

Observation queries

SELECT count(*) FROM good_jobs WHERE serialized_params->>'job_class' = 'Disputes::ResolveDoneJiraIssuesJob' AND created_at > now() - interval '24 hours';

Open disputes still linked to done Jira issues (must stay near zero, allowed only within the one-hour sweep lag):

SELECT count(*) FROM disputes d JOIN jira_issues ji ON ji.id = d.jira_issue_id WHERE d.status = 'open' AND ji.status_category = 'done';

BullMQ (redis-cli): ZCARD bull:dispute-jira-resolution:completed, ZCARD bull:dispute-jira-resolution:failed.

Verification

API2 unit tests cover the candidate SQL (PGlite), pause gate, fan-out, truncation, enqueue-failure retry, deterministic job options and dedupe/replace behavior, plus the native per-dispute resolution — complaint resolution, risk recalculation, org scoping, not-done/already-resolved no-ops — against PGlite fixtures. pnpm typecheck and scoped RuboCop are required before push.

Last updated on