Commit b4ccfe7
authored
fix(selfhost): backlog-vs-fresh fairness, convergence-cap shadowing, and maintenance admission feedback loop (#9233)
* fix(selfhost): add bounded age escape for the backlog-vs-fresh lane priority gate
The lane-scoped foreground claim requires beating the best due unclassified
priority, but githubWebhookPriority returns 10 (equal to fresh's own priority,
above backlog's 9) for nearly every webhook other than a fresh PR open/reopen/
synchronize/ready-for-review event. On any repo with CI at least one such row
is essentially always due, so neither lane's priority can ever satisfy the
strict `>` gate and both lane claims permanently fall back to plain priority
ordering -- the exact starvation the fairness mechanism exists to prevent.
Add shouldEscapeLanePriorityGate: once the oldest due row in the lane being
claimed has waited at least DEFAULT_FOREGROUND_LANE_MAX_STARVE_AGE_MS (10
minutes, mirroring maintenance-admission's trickle_max_defer_age), the gate
is bypassed so an old lane row can win on its own merits.
Closes #9153
* fix(selfhost): apply the lane-priority age escape in both queue backends
Wires shouldEscapeLanePriorityGate into claimNextForegroundLane for both the
Postgres and sqlite queue backends: each computes the oldest due row's age
for the lane being claimed (fresh via a dedicated query, backlog by reusing
the row set already fetched for repo selection) and falls back to the plain
`>=` floor once that age crosses the bounded threshold, instead of staying
permanently gated behind an unclassified priority-10 webhook.
Includes regression tests for both backends covering the escape arm (an aged
backlog row wins preferentially) and the control arm (a fresh backlog row
stays gated this cycle), plus a fix to a pre-existing pg-queue test whose
placeholder created_at (an epoch-adjacent 1000ms) incidentally tripped the
new age escape.
Closes #9153
* fix(selfhost): filter repair-exhausted PRs before the backlog-convergence cap
selectBacklogConvergenceCandidates sorted oldest-open-first, sliced to `max`,
and only then did the caller filter out repair-exhausted PRs -- so five old,
permanently-unpublishable PRs could occupy every slot forever, shadowing the
6th+ PR needing convergence behind them indefinitely.
Split the pure ordering out into sortedBacklogConvergenceCandidates (unsliced)
and have sweepRepoBacklogConvergence walk that full order, skipping exhausted
candidates as it goes and stopping once `max` actionable ones are found --
selectBacklogConvergenceCandidates itself becomes a thin slice-after-sort
wrapper over the new function for callers that don't need the exhaustion
filter. Also logs + records an audit event when every examined candidate is
repair-exhausted, so a permanently wedged head of the backlog is visible
instead of the sweep silently returning early every cycle.
Closes #9154
* fix(selfhost): exclude in-flight jobs from live_job_age_high and cache pressure signals
Two compounding defects in maintenance admission:
- oldestLiveRunnableAgeMs was computed over rows that are either 'processing'
or due-and-pending, so a normal in-flight job (a routine multi-minute AI
review) alone pushed the age past maxLiveJobAgeMs for its whole duration,
tripping live_job_age_high with no drain escape and collapsing the entire
maintenance lane -- including the watchdog/alerter that would report an
actual overload -- to the 4-hour trickle backstop. Narrow oldest_runnable's
own FILTER to 'pending AND due' rows only, excluding 'processing' entirely.
- Every claimed maintenance job recomputes maintenancePressureSignals' four
aggregate scans, and a denial returns `true` from processOne(), so the pump
loop immediately claims the next due maintenance row and repeats -- a burst
of N due maintenance rows means 4N sequential scans in one tight loop, a
positive feedback loop (more denials -> more scans -> higher load -> more
denials). Add a short-TTL (default 1.5s, configurable via
MAINTENANCE_ADMISSION_PRESSURE_CACHE_TTL_MS) memoized wrapper shared by the
admission check and the pressureSignals() observability method, in both
the Postgres and sqlite backends. Also add a partial index on
(is_maintenance, status) for the maintenance-lane aggregate, which
previously had no supporting index.
Closes #91551 parent ad8975e commit b4ccfe7
13 files changed
Lines changed: 863 additions & 53 deletions
File tree
- apps/loopover-ui/src/lib
- src
- queue
- selfhost
- test/unit
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
337 | 337 | | |
338 | 338 | | |
339 | 339 | | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
340 | 344 | | |
341 | 345 | | |
342 | 346 | | |
| |||
733 | 737 | | |
734 | 738 | | |
735 | 739 | | |
| 740 | + | |
736 | 741 | | |
737 | 742 | | |
738 | 743 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
258 | 258 | | |
259 | 259 | | |
260 | 260 | | |
261 | | - | |
| 261 | + | |
262 | 262 | | |
263 | 263 | | |
264 | 264 | | |
| |||
1854 | 1854 | | |
1855 | 1855 | | |
1856 | 1856 | | |
1857 | | - | |
| 1857 | + | |
| 1858 | + | |
| 1859 | + | |
| 1860 | + | |
| 1861 | + | |
| 1862 | + | |
| 1863 | + | |
1858 | 1864 | | |
1859 | 1865 | | |
1860 | 1866 | | |
1861 | 1867 | | |
1862 | 1868 | | |
1863 | 1869 | | |
1864 | | - | |
1865 | | - | |
1866 | | - | |
| 1870 | + | |
| 1871 | + | |
| 1872 | + | |
| 1873 | + | |
| 1874 | + | |
| 1875 | + | |
| 1876 | + | |
| 1877 | + | |
| 1878 | + | |
| 1879 | + | |
| 1880 | + | |
| 1881 | + | |
| 1882 | + | |
| 1883 | + | |
| 1884 | + | |
| 1885 | + | |
| 1886 | + | |
| 1887 | + | |
| 1888 | + | |
| 1889 | + | |
| 1890 | + | |
| 1891 | + | |
| 1892 | + | |
| 1893 | + | |
| 1894 | + | |
| 1895 | + | |
| 1896 | + | |
| 1897 | + | |
| 1898 | + | |
| 1899 | + | |
| 1900 | + | |
| 1901 | + | |
| 1902 | + | |
| 1903 | + | |
1867 | 1904 | | |
1868 | 1905 | | |
1869 | 1906 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
35 | | - | |
36 | | - | |
37 | | - | |
38 | | - | |
39 | | - | |
40 | | - | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
41 | 45 | | |
42 | | - | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
| 46 | + | |
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
51 | | - | |
| 51 | + | |
52 | 52 | | |
53 | 53 | | |
54 | | - | |
55 | | - | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
56 | 70 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
99 | | - | |
100 | | - | |
101 | | - | |
102 | | - | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
103 | 106 | | |
104 | 107 | | |
105 | 108 | | |
| |||
129 | 132 | | |
130 | 133 | | |
131 | 134 | | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
132 | 143 | | |
133 | 144 | | |
134 | 145 | | |
| |||
143 | 154 | | |
144 | 155 | | |
145 | 156 | | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
146 | 160 | | |
147 | 161 | | |
148 | 162 | | |
| |||
195 | 209 | | |
196 | 210 | | |
197 | 211 | | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
198 | 216 | | |
199 | 217 | | |
200 | 218 | | |
| |||
0 commit comments