@@ -17,21 +17,23 @@ import { noop } from './utils';
1717
1818// Timeout constants for JSON-RPC requests (in milliseconds)
1919const CONFIGURE_TIMEOUT_MS = 30_000 ; // 30 seconds for configuration
20- const REFRESH_TIMEOUT_MS = 120_000 ; // 2 minutes for full refresh
20+ const MAX_CONFIGURE_TIMEOUT_MS = 60_000 ; // Max configure timeout after retries (60s)
21+ const REFRESH_TIMEOUT_MS = 30_000 ; // 30 seconds for full refresh (with 1 retry = 60s max)
2122const RESOLVE_TIMEOUT_MS = 30_000 ; // 30 seconds for single resolve
2223
2324// Restart/recovery constants
2425const MAX_RESTART_ATTEMPTS = 3 ;
2526const RESTART_BACKOFF_BASE_MS = 1_000 ; // 1 second base, exponential: 1s, 2s, 4s
2627const MAX_CONFIGURE_TIMEOUTS_BEFORE_KILL = 2 ; // Kill on the 2nd consecutive timeout
28+ const MAX_REFRESH_RETRIES = 1 ; // Retry refresh once after timeout
2729
2830/**
2931 * Computes the configure timeout with exponential backoff.
3032 * @param retryCount Number of consecutive configure timeouts so far
31- * @returns Timeout in milliseconds: 30s, 60s, 120s, ... capped at REFRESH_TIMEOUT_MS
33+ * @returns Timeout in milliseconds: 30s, 60s, capped at MAX_CONFIGURE_TIMEOUT_MS (60s)
3234 */
3335export function getConfigureTimeoutMs ( retryCount : number ) : number {
34- return Math . min ( CONFIGURE_TIMEOUT_MS * Math . pow ( 2 , retryCount ) , REFRESH_TIMEOUT_MS ) ;
36+ return Math . min ( CONFIGURE_TIMEOUT_MS * Math . pow ( 2 , retryCount ) , MAX_CONFIGURE_TIMEOUT_MS ) ;
3537}
3638
3739/**
@@ -563,6 +565,41 @@ class NativePythonFinderImpl implements NativePythonFinder {
563565 }
564566
565567 private async doRefresh ( options ?: NativePythonEnvironmentKind | Uri [ ] ) : Promise < NativeInfo [ ] > {
568+ let lastError : unknown ;
569+
570+ for ( let attempt = 0 ; attempt <= MAX_REFRESH_RETRIES ; attempt ++ ) {
571+ try {
572+ return await this . doRefreshAttempt ( options , attempt ) ;
573+ } catch ( ex ) {
574+ lastError = ex ;
575+
576+ // Only retry on timeout errors
577+ if ( ex instanceof RpcTimeoutError && ex . method !== 'configure' ) {
578+ if ( attempt < MAX_REFRESH_RETRIES ) {
579+ this . outputChannel . warn (
580+ `[pet] Refresh timed out (attempt ${ attempt + 1 } /${ MAX_REFRESH_RETRIES + 1 } ), restarting and retrying...` ,
581+ ) ;
582+ // Kill and restart for retry
583+ this . killProcess ( ) ;
584+ this . processExited = true ;
585+ continue ;
586+ }
587+ // Final attempt failed
588+ this . outputChannel . error ( `[pet] Refresh failed after ${ MAX_REFRESH_RETRIES + 1 } attempts` ) ;
589+ }
590+ // Non-timeout errors or final timeout - rethrow
591+ throw ex ;
592+ }
593+ }
594+
595+ // Should not reach here, but TypeScript needs this
596+ throw lastError ;
597+ }
598+
599+ private async doRefreshAttempt (
600+ options : NativePythonEnvironmentKind | Uri [ ] | undefined ,
601+ attempt : number ,
602+ ) : Promise < NativeInfo [ ] > {
566603 await this . ensureProcessRunning ( ) ;
567604 const disposables : Disposable [ ] = [ ] ;
568605 const unresolved : Promise < void > [ ] = [ ] ;
@@ -610,6 +647,9 @@ class NativePythonFinderImpl implements NativePythonFinder {
610647
611648 // Reset restart attempts on successful refresh
612649 this . restartAttempts = 0 ;
650+ if ( attempt > 0 ) {
651+ this . outputChannel . info ( `[pet] Refresh succeeded on retry attempt ${ attempt + 1 } ` ) ;
652+ }
613653 } catch ( ex ) {
614654 // On refresh timeout (not configure — configure handles its own timeout),
615655 // kill the hung process so next request triggers restart
0 commit comments