@@ -11,7 +11,8 @@ const localize = nls.loadMessageBundle()
11
11
import * as AWS from 'aws-sdk'
12
12
import * as logger from '../logger/logger'
13
13
import { ServiceConfigurationOptions } from 'aws-sdk/lib/service'
14
- import { Timeout , waitTimeout , waitUntil } from '../utilities/timeoutUtils'
14
+ import { CancellationError , Timeout , waitTimeout , waitUntil } from '../utilities/timeoutUtils'
15
+ import { isUserCancelledError } from '../../shared/errors'
15
16
import { showMessageWithCancel } from '../utilities/messages'
16
17
import { assertHasProps , ClassToInterfaceType , isNonNullable , RequiredProps } from '../utilities/tsUtils'
17
18
import { AsyncCollection , toCollection } from '../utilities/asyncCollection'
@@ -591,118 +592,154 @@ class CodeCatalystClientInternal {
591
592
*/
592
593
public async startDevEnvironmentWithProgress (
593
594
args : CodeCatalyst . StartDevEnvironmentRequest ,
594
- status : string ,
595
595
timeout : Timeout = new Timeout ( 1000 * 60 * 60 )
596
596
) : Promise < DevEnvironment > {
597
597
// Track the status changes chronologically so that we can
598
598
// 1. reason about hysterisis (weird flip-flops)
599
599
// 2. have visibility in the logs
600
- const lastStatus = new Array < { status : string ; start : number } > ( )
600
+ const statuses = new Array < { status : string ; start : number } > ( )
601
601
let alias : string | undefined
602
-
603
- try {
604
- const devenv = await this . getDevEnvironment ( args )
605
- alias = devenv . alias
606
- lastStatus . push ( { status : devenv . status , start : Date . now ( ) } )
607
- if ( status === 'RUNNING' && devenv . status === 'RUNNING' ) {
608
- // "Debounce" in case caller did not check if the environment was already running.
609
- return devenv
610
- }
611
- } catch {
612
- lastStatus . length = 0
613
- }
602
+ let startAttempts = 0
614
603
615
604
function statusesToString ( ) {
616
605
let s = ''
617
- for ( let i = 0 ; i < lastStatus . length ; i ++ ) {
618
- const item = lastStatus [ i ]
619
- const nextItem = i < lastStatus . length - 1 ? lastStatus [ i + 1 ] : undefined
606
+ for ( let i = 0 ; i < statuses . length ; i ++ ) {
607
+ const item = statuses [ i ]
608
+ const nextItem = i < statuses . length - 1 ? statuses [ i + 1 ] : undefined
620
609
const nextTime = nextItem ? nextItem . start : Date . now ( )
621
610
const elapsed = nextTime - item . start
622
611
s += `${ s ? ' ' : '' } ${ item . status } /${ elapsed } ms`
623
612
}
624
613
return `[${ s } ]`
625
614
}
626
615
627
- const doLog = ( kind : 'debug' | 'info' , msg : string ) => {
628
- const name = ( alias ? alias : args . id ) . substring ( 0 , 19 )
629
- const fmt = `${ msg } (time: %d s): %s %s`
616
+ function getName ( ) : string {
617
+ const fullname = alias ? alias : args . id
618
+ const shortname = fullname . substring ( 0 , 19 ) + ( fullname . length > 20 ? '…' : '' )
619
+ return shortname
620
+ }
621
+
622
+ function failedStartMsg ( ) {
623
+ const lastStatus = statuses [ statuses . length - 1 ] ?. status
624
+ return `Dev Environment failed to start (${ lastStatus } ): ${ getName ( ) } `
625
+ }
626
+
627
+ const doLog = ( kind : 'debug' | 'error' | 'info' , msg : string ) => {
628
+ const fmt = `${ msg } (time: %ds${
629
+ startAttempts <= 1 ? '' : ', startAttempts: ' + startAttempts . toString ( )
630
+ } ): %s %s`
630
631
if ( kind === 'debug' ) {
631
- this . log . debug ( fmt , timeout . elapsedTime / 1000 , name , statusesToString ( ) )
632
+ this . log . debug ( fmt , timeout . elapsedTime / 1000 , getName ( ) , statusesToString ( ) )
633
+ } else if ( kind === 'error' ) {
634
+ this . log . error ( fmt , timeout . elapsedTime / 1000 , getName ( ) , statusesToString ( ) )
632
635
} else {
633
- this . log . info ( fmt , timeout . elapsedTime / 1000 , name , statusesToString ( ) )
636
+ this . log . info ( fmt , timeout . elapsedTime / 1000 , getName ( ) , statusesToString ( ) )
634
637
}
635
638
}
636
639
637
640
const progress = await showMessageWithCancel (
638
641
localize ( 'AWS.CodeCatalyst.devenv.message' , 'CodeCatalyst' ) ,
639
642
timeout
640
643
)
641
- progress . report ( { message : localize ( 'AWS.CodeCatalyst.devenv.checking' , 'checking status...' ) } )
644
+ progress . report ( { message : localize ( 'AWS.CodeCatalyst.devenv.checking' , 'Checking status...' ) } )
645
+
646
+ try {
647
+ const devenv = await this . getDevEnvironment ( args )
648
+ alias = devenv . alias
649
+ statuses . push ( { status : devenv . status , start : Date . now ( ) } )
650
+ if ( devenv . status === 'RUNNING' ) {
651
+ doLog ( 'debug' , 'devenv RUNNING' )
652
+ timeout . cancel ( )
653
+ // "Debounce" in case caller did not check if the environment was already running.
654
+ return devenv
655
+ }
656
+ } catch {
657
+ // Continue.
658
+ }
659
+
660
+ doLog ( 'debug' , 'devenv not started, waiting' )
642
661
643
662
const pollDevEnv = waitUntil (
644
663
async ( ) => {
645
- doLog ( 'debug' , 'devenv not started, waiting' )
646
- // technically this will continue to be called until it reaches its own timeout, need a better way to 'cancel' a `waitUntil`
647
664
if ( timeout . completed ) {
648
- return
665
+ // TODO: need a better way to "cancel" a `waitUntil`.
666
+ throw new CancellationError ( 'user' )
649
667
}
650
668
669
+ const lastStatus = statuses [ statuses . length - 1 ]
670
+ const elapsed = Date . now ( ) - lastStatus . start
651
671
const resp = await this . getDevEnvironment ( args )
652
- const startingStates = lastStatus . filter ( o => o . status === 'STARTING' ) . length
672
+ alias = resp . alias
673
+
653
674
if (
654
- startingStates > 1 &&
655
- lastStatus [ 0 ] . status === 'STARTING' &&
656
- ( resp . status === 'STOPPED' || resp . status === 'STOPPING' )
675
+ lastStatus &&
676
+ [ 'STOPPED' , 'FAILED' ] . includes ( lastStatus . status ) &&
677
+ [ 'STOPPED' , 'FAILED' ] . includes ( resp . status ) &&
678
+ elapsed > 60000 &&
679
+ startAttempts > 2
657
680
) {
658
- throw new ToolkitError ( 'Dev Environment failed to start' , { code : 'BadDevEnvState' } )
659
- }
660
-
661
- if ( resp . status === 'STOPPED' ) {
681
+ // If still STOPPED/FAILED after 60+ seconds, don't keep retrying for 1 hour...
682
+ throw new ToolkitError ( failedStartMsg ( ) , { code : 'FailedDevEnv' } )
683
+ } else if ( [ 'STOPPED' , 'FAILED' ] . includes ( resp . status ) ) {
662
684
progress . report ( {
663
- message : localize ( 'AWS.CodeCatalyst.devenv.stopStart ' , 'Resuming Dev Environment...' ) ,
685
+ message : localize ( 'AWS.CodeCatalyst.devenv.resuming ' , 'Resuming Dev Environment...' ) ,
664
686
} )
665
687
try {
688
+ startAttempts ++
666
689
await this . startDevEnvironment ( args )
667
690
} catch ( e ) {
668
691
const err = e as AWS . AWSError
669
- // May happen after "Update Dev Environment":
670
- // ConflictException: "Cannot start Dev Environment because update process is still going on"
671
- // Continue retrying in that case.
672
- if ( err . code === 'ConflictException' ) {
673
- doLog ( 'debug' , 'devenv not started (ConflictException), waiting' )
674
- } else {
675
- throw new ToolkitError ( 'Dev Environment failed to start' , {
676
- code : 'BadDevEnvState' ,
677
- cause : err ,
678
- } )
679
- }
692
+ // - ValidationException: "… creation has failed, cannot start"
693
+ // - ConflictException: "Cannot start … because update process is still going on"
694
+ // (can happen after "Update Dev Environment")
695
+ doLog ( 'info' , `devenv not started (${ err . code } ), waiting` )
696
+ // Continue retrying...
680
697
}
681
698
} else if ( resp . status === 'STOPPING' ) {
682
699
progress . report ( {
683
- message : localize ( 'AWS.CodeCatalyst.devenv.resuming ' , 'Waiting for Dev Environment to stop...' ) ,
700
+ message : localize ( 'AWS.CodeCatalyst.devenv.stopping ' , 'Waiting for Dev Environment to stop...' ) ,
684
701
} )
685
- } else if ( resp . status === 'FAILED' ) {
686
- throw new ToolkitError ( 'Dev Environment failed to start' , { code : 'FailedDevEnv' } )
687
702
} else {
688
703
progress . report ( {
689
704
message : localize ( 'AWS.CodeCatalyst.devenv.starting' , 'Opening Dev Environment...' ) ,
690
705
} )
691
706
}
692
707
693
- if ( lastStatus [ lastStatus . length - 1 ] . status !== resp . status ) {
694
- lastStatus . push ( { status : resp . status , start : Date . now ( ) } )
708
+ if ( lastStatus ?. status !== resp . status ) {
709
+ statuses . push ( { status : resp . status , start : Date . now ( ) } )
710
+ if ( resp . status !== 'RUNNING' ) {
711
+ doLog ( 'debug' , `devenv not started, waiting` )
712
+ }
695
713
}
696
714
return resp . status === 'RUNNING' ? resp : undefined
697
715
} ,
698
716
// note: the `waitUntil` will resolve prior to the real timeout if it is refreshed
699
717
{ interval : 1000 , timeout : timeout . remainingTime , truthy : true }
700
718
)
701
719
702
- const devenv = await waitTimeout ( pollDevEnv , timeout )
720
+ const devenv = await waitTimeout ( pollDevEnv , timeout ) . catch ( e => {
721
+ const err = e as Error
722
+ const starts = statuses . filter ( o => o . status === 'STARTING' ) . length
723
+ const fails = statuses . filter ( o => o . status === 'FAILED' ) . length
724
+
725
+ if ( ! isUserCancelledError ( e ) ) {
726
+ doLog ( 'error' , 'devenv failed to start' )
727
+ } else {
728
+ doLog ( 'info' , 'devenv failed to start (user cancelled)' )
729
+ err . message = failedStartMsg ( )
730
+ throw err
731
+ }
732
+
733
+ if ( starts > 1 && fails === 0 ) {
734
+ throw new ToolkitError ( failedStartMsg ( ) , { code : 'BadDevEnvState' , cause : err } )
735
+ } else if ( fails > 0 ) {
736
+ throw new ToolkitError ( failedStartMsg ( ) , { code : 'FailedDevEnv' , cause : err } )
737
+ }
738
+ } )
739
+
703
740
if ( ! devenv ) {
704
- doLog ( 'info ' , 'devenv failed to start' )
705
- throw new ToolkitError ( 'Dev Environment failed to start' , { code : 'Timeout' } )
741
+ doLog ( 'error ' , 'devenv failed to start (timeout) ' )
742
+ throw new ToolkitError ( failedStartMsg ( ) , { code : 'Timeout' } )
706
743
}
707
744
doLog ( 'info' , 'devenv started' )
708
745
0 commit comments