@@ -2,14 +2,29 @@ import type { WorkflowRepositoryScope } from "@shared/contracts";
22import type { VcsConfig , VcsProviderConfig } from "../../../env.js" ;
33import { buildOctokit } from "../../lib/github-auth.js" ;
44
5- const GITLAB_PROJECTS_TIMEOUT_MS = 15_000 ;
5+ const GITLAB_PROJECTS_TIMEOUT_MS = 18_000 ;
66
7- // One retry with a short backoff. A provider's 5xx or timeout is usually gone by
8- // the second call, while the pre-sandbox step that owns this listing runs under a
9- // 60s budget: a longer ladder would spend that budget hanging instead of failing
10- // with a reason an operator can act on.
11- const LISTING_MAX_ATTEMPTS = 2 ;
12- const LISTING_RETRY_DELAY_MS = 500 ;
7+ // A few bounded retries with jittered exponential backoff. A provider's 5xx or
8+ // timeout is usually gone within a couple of calls, while the pre-sandbox step
9+ // that owns this listing runs under a 60s budget: a longer ladder would spend
10+ // that budget hanging instead of failing with a reason an operator can act on.
11+ // Worst case is 3 * 18s + <=1.5s of backoff ~= 55.5s, which stays inside that
12+ // budget while surviving a GitLab hiccup that outlasts a single 18s window.
13+ const LISTING_MAX_ATTEMPTS = 3 ;
14+ const LISTING_RETRY_BASE_DELAY_MS = 500 ;
15+ const LISTING_RETRY_MAX_DELAY_MS = 4_000 ;
16+
17+ /** Jittered exponential backoff between listing attempts: after the nth attempt
18+ * fails the next wait is a random span in [0, base * 2^(n-1)] capped at
19+ * LISTING_RETRY_MAX_DELAY_MS. Full jitter de-correlates retries that a shared
20+ * upstream blip fired at once, and the cap keeps the ladder inside the budget. */
21+ function listingRetryDelayMs ( failedAttempt : number ) : number {
22+ const ceiling = Math . min (
23+ LISTING_RETRY_MAX_DELAY_MS ,
24+ LISTING_RETRY_BASE_DELAY_MS * 2 ** ( failedAttempt - 1 ) ,
25+ ) ;
26+ return Math . floor ( Math . random ( ) * ceiling ) ;
27+ }
1328
1429export type VcsProvider = "github" | "gitlab" ;
1530
@@ -62,11 +77,11 @@ export interface RepositoryListingFailure {
6277 * providers that failed instead of a single rejection standing in for all of them.
6378 * Each provider's listing is retried under a bounded policy first.
6479 *
65- * Latency budget for whoever tunes GITLAB_PROJECTS_TIMEOUT_MS next: the retry
66- * doubles the worst case, so a hung provider costs about 30.5s here rather than
67- * 15s , for every caller including the dashboard catalog endpoint. allSettled also
68- * means the slowest provider sets the floor: a fast 401 next to a hung provider
69- * now surfaces at the hung provider's pace instead of immediately.
80+ * Latency budget for whoever tunes GITLAB_PROJECTS_TIMEOUT_MS next: the ladder
81+ * of up to 3 attempts triples the worst case, so a hung provider costs about 55s
82+ * here rather than 18s , for every caller including the dashboard catalog endpoint.
83+ * allSettled also means the slowest provider sets the floor: a fast 401 next to a
84+ * hung provider now surfaces at the hung provider's pace instead of immediately.
7085 */
7186export async function listRepositoriesAcrossProviders (
7287 providers : VcsProviderConfig [ ] ,
@@ -104,7 +119,7 @@ async function listRepositoriesWithRetry(
104119 } catch ( err ) {
105120 lastError = err ;
106121 if ( attempt >= LISTING_MAX_ATTEMPTS || ! isTransientListingError ( err ) ) break ;
107- await new Promise ( ( resolve ) => setTimeout ( resolve , LISTING_RETRY_DELAY_MS ) ) ;
122+ await new Promise ( ( resolve ) => setTimeout ( resolve , listingRetryDelayMs ( attempt ) ) ) ;
108123 }
109124 }
110125 throw lastError ;
0 commit comments