mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(coderd): fast-retry PR discovery after git push (#23579)
## Problem
When chatd pushes a branch and then creates a PR (e.g. `git push`
followed by `gh pr create`), the gitsync background worker often picks
up the stale `chat_diff_statuses` row between the two operations. At
that point no PR exists yet, so the worker skips the row. However, the
acquisition SQL locks the row for **5 minutes** (crash-recovery
interval), creating a dead zone where the PR diff is invisible in the UI
until the user manually navigates to the chat.
### Root cause
1. `git push` triggers `GIT_ASKPASS` → coderd external-auth handler →
`MarkStale()` sets `stale_at = now - 1s`
2. Background worker acquires the row within ~10s, atomically bumps
`stale_at = NOW() + 5 min` (crash-recovery lock)
3. Worker calls `ResolveBranchPullRequest` → no PR exists yet → returns
`nil` → worker skips with `continue`
4. `gh pr create` completes moments later, but uses its own auth (not
`GIT_ASKPASS`), so no second `MarkStale` fires
5. Row is locked for 5 minutes before the worker can retry
Loading the chat works immediately because `GET /chats/{chat}` calls
`resolveChatDiffStatus` synchronously, which discovers the PR inline.
## Fix
When `ResolveBranchPullRequest` returns nil (no PR yet) **and** the row
was recently marked stale (within 2 minutes), apply a short 15-second
backoff via `BackoffChatDiffStatus` instead of letting the 5-minute
acquisition lock stand. Outside the retry window, the worker skips the
row as before — no indefinite fast-polling for branches that never
receive a PR.
To make the "recently marked stale" check work, `updated_at` is no
longer overwritten by the acquisition and backoff SQL queries. This
preserves it as a reliable "last externally changed" timestamp (set by
`MarkStale` or a successful refresh).
### Behavior summary
| Scenario | `updated_at` age | Backoff | Effective retry |
|---|---|---|---|
| Fresh push, no PR yet | < 2 min | 15s (`NoPRBackoff`) | ~15s |
| Old row, no PR | ≥ 2 min | None (skip) | ~5 min (acquisition lock) |
| Error (any age) | Any | 120s (`DiffStatusTTL`) | ~120s |
| Success (any age) | Any | 120s (`DiffStatusTTL`) | ~120s |
## Changes
- **`coderd/database/queries/chats.sql`** — Remove `updated_at = NOW()`
from `AcquireStaleChatDiffStatuses` and `BackoffChatDiffStatus`
- **`coderd/database/queries.sql.go`** — Regenerated
- **`coderd/x/gitsync/worker.go`** — Add `NoPRBackoff` (15s) and
`NoPRRetryWindow` (2 min) constants; apply short backoff only within the
retry window
- **`coderd/x/gitsync/worker_test.go`** — Add
`TestWorker_NoPR_RecentMarkStale_BacksOffShort` and
`TestWorker_NoPR_OldRow_Skips`
This commit is contained in:
@@ -3883,8 +3883,11 @@ WITH acquired AS (
|
||||
-- Claim for 5 minutes. The worker sets the real stale_at
|
||||
-- after refresh. If the worker crashes, rows become eligible
|
||||
-- again after this interval.
|
||||
stale_at = NOW() + INTERVAL '5 minutes',
|
||||
updated_at = NOW()
|
||||
-- NOTE: updated_at is intentionally NOT touched here so
|
||||
-- the worker can read it as "when was this row last
|
||||
-- externally changed" (by MarkStale or a successful
|
||||
-- refresh).
|
||||
stale_at = NOW() + INTERVAL '5 minutes'
|
||||
WHERE
|
||||
chat_id IN (
|
||||
SELECT
|
||||
@@ -4005,8 +4008,11 @@ const backoffChatDiffStatus = `-- name: BackoffChatDiffStatus :exec
|
||||
UPDATE
|
||||
chat_diff_statuses
|
||||
SET
|
||||
stale_at = $1::timestamptz,
|
||||
updated_at = NOW()
|
||||
-- NOTE: updated_at is intentionally NOT touched here so
|
||||
-- the worker can read it as "when was this row last
|
||||
-- externally changed" (by MarkStale or a successful
|
||||
-- refresh).
|
||||
stale_at = $1::timestamptz
|
||||
WHERE
|
||||
chat_id = $2::uuid
|
||||
`
|
||||
|
||||
Reference in New Issue
Block a user