@@ -34,6 +34,46 @@ export function getConfigureTimeoutMs(retryCount: number): number {
3434 return Math . min ( CONFIGURE_TIMEOUT_MS * Math . pow ( 2 , retryCount ) , REFRESH_TIMEOUT_MS ) ;
3535}
3636
37+ /**
38+ * Encapsulates the configure retry state machine.
39+ * Tracks consecutive timeout count and decides whether to kill the process.
40+ */
41+ export class ConfigureRetryState {
42+ private _timeoutCount : number = 0 ;
43+
44+ get timeoutCount ( ) : number {
45+ return this . _timeoutCount ;
46+ }
47+
48+ /** Returns the timeout duration for the current attempt (with exponential backoff). */
49+ getTimeoutMs ( ) : number {
50+ return getConfigureTimeoutMs ( this . _timeoutCount ) ;
51+ }
52+
53+ /** Call after a successful configure. Resets the timeout counter. */
54+ onSuccess ( ) : void {
55+ this . _timeoutCount = 0 ;
56+ }
57+
58+ /**
59+ * Call after a configure timeout. Increments the counter and returns
60+ * whether the process should be killed (true = kill, false = let it continue).
61+ */
62+ onTimeout ( ) : boolean {
63+ this . _timeoutCount ++ ;
64+ if ( this . _timeoutCount >= MAX_CONFIGURE_TIMEOUTS_BEFORE_KILL ) {
65+ this . _timeoutCount = 0 ;
66+ return true ; // Kill the process
67+ }
68+ return false ; // Let PET continue
69+ }
70+
71+ /** Call after a non-timeout error or process restart. Resets the counter. */
72+ reset ( ) : void {
73+ this . _timeoutCount = 0 ;
74+ }
75+ }
76+
3777export async function getNativePythonToolsPath ( ) : Promise < string > {
3878 const envsExt = getExtension ( ENVS_EXTENSION_ID ) ;
3979 if ( envsExt ) {
@@ -184,7 +224,7 @@ class NativePythonFinderImpl implements NativePythonFinder {
184224 private startFailed : boolean = false ;
185225 private restartAttempts : number = 0 ;
186226 private isRestarting : boolean = false ;
187- private configureTimeoutCount : number = 0 ;
227+ private readonly configureRetry = new ConfigureRetryState ( ) ;
188228
189229 constructor (
190230 private readonly outputChannel : LogOutputChannel ,
@@ -285,7 +325,7 @@ class NativePythonFinderImpl implements NativePythonFinder {
285325 this . processExited = false ;
286326 this . startFailed = false ;
287327 this . lastConfiguration = undefined ; // Force reconfiguration
288- this . configureTimeoutCount = 0 ;
328+ this . configureRetry . reset ( ) ;
289329
290330 // Start fresh
291331 this . connection = this . start ( ) ;
@@ -611,38 +651,37 @@ class NativePythonFinderImpl implements NativePythonFinder {
611651 }
612652 this . outputChannel . info ( '[pet] configure: Sending configuration update:' , JSON . stringify ( options ) ) ;
613653 // Exponential backoff: 30s, 60s on retry. Capped at REFRESH_TIMEOUT_MS.
614- const timeoutMs = getConfigureTimeoutMs ( this . configureTimeoutCount ) ;
615- if ( this . configureTimeoutCount > 0 ) {
654+ const timeoutMs = this . configureRetry . getTimeoutMs ( ) ;
655+ if ( this . configureRetry . timeoutCount > 0 ) {
616656 this . outputChannel . info (
617- `[pet] configure: Using extended timeout of ${ timeoutMs } ms (retry ${ this . configureTimeoutCount } )` ,
657+ `[pet] configure: Using extended timeout of ${ timeoutMs } ms (retry ${ this . configureRetry . timeoutCount } )` ,
618658 ) ;
619659 }
620660 try {
621661 await sendRequestWithTimeout ( this . connection , 'configure' , options , timeoutMs ) ;
622662 // Only cache after success so failed/timed-out calls will retry
623663 this . lastConfiguration = options ;
624- this . configureTimeoutCount = 0 ;
664+ this . configureRetry . onSuccess ( ) ;
625665 } catch ( ex ) {
626666 // Clear cached config so the next call retries instead of short-circuiting via configurationEquals
627667 this . lastConfiguration = undefined ;
628668 if ( ex instanceof RpcTimeoutError ) {
629- this . configureTimeoutCount ++ ;
630- if ( this . configureTimeoutCount >= MAX_CONFIGURE_TIMEOUTS_BEFORE_KILL ) {
631- // Repeated configure timeouts suggest PET is truly hung — kill and restart
669+ const shouldKill = this . configureRetry . onTimeout ( ) ;
670+ if ( shouldKill ) {
632671 this . outputChannel . error (
633- ` [pet] Configure timed out ${ this . configureTimeoutCount } consecutive times , killing hung process for restart` ,
672+ ' [pet] Configure timed out on consecutive attempts , killing hung process for restart' ,
634673 ) ;
635674 this . killProcess ( ) ;
636675 this . processExited = true ;
637- this . configureTimeoutCount = 0 ;
638676 } else {
639- // First timeout — PET may still be working. Let it continue and retry next call.
640677 this . outputChannel . warn (
641- `[pet] Configure request timed out (attempt ${ this . configureTimeoutCount } /${ MAX_CONFIGURE_TIMEOUTS_BEFORE_KILL } ), ` +
678+ `[pet] Configure request timed out (attempt ${ this . configureRetry . timeoutCount } /${ MAX_CONFIGURE_TIMEOUTS_BEFORE_KILL } ), ` +
642679 'will retry on next request without killing process' ,
643680 ) ;
644681 }
645682 } else {
683+ // Non-timeout errors reset the counter so only consecutive timeouts are counted
684+ this . configureRetry . reset ( ) ;
646685 this . outputChannel . error ( '[pet] configure: Configuration error' , ex ) ;
647686 }
648687 throw ex ;
0 commit comments