in packages/core/src/shared/clients/codecatalystClient.ts [738:902]
public async startDevEnvironmentWithProgress(
args: RequiredProps<StartDevEnvironmentRequest, 'id' | 'spaceName' | 'projectName'>,
timeout: Timeout = new Timeout(1000 * 60 * 60)
): Promise<DevEnvironment> {
// Track the status changes chronologically so that we can
// 1. reason about hysterisis (weird flip-flops)
// 2. have visibility in the logs
const statuses = new Array<{ status: string; start: number }>()
let alias: string | undefined
let startAttempts = 0
function statusesToString() {
let s = ''
for (let i = 0; i < statuses.length; i++) {
const item = statuses[i]
const nextItem = i < statuses.length - 1 ? statuses[i + 1] : undefined
const nextTime = nextItem ? nextItem.start : Date.now()
const elapsed = nextTime - item.start
s += `${s ? ' ' : ''}${item.status}/${elapsed}ms`
}
return `[${s}]`
}
function getName(): string {
const fullname = alias ? alias : args.id
const shortname = fullname.substring(0, 19) + (fullname.length > 20 ? '…' : '')
return shortname
}
function failedStartMsg(serviceMsg?: string) {
const LastSeenStatus = statuses[statuses.length - 1]?.status
const serviceMsg_ = serviceMsg ? `${serviceMsg}: ` : ''
return `Dev Environment failed to start (${LastSeenStatus}): ${serviceMsg_}${getName()}`
}
const doLog = (kind: 'debug' | 'error' | 'info', msg: string) => {
const fmt = `${msg} (time: %ds${
startAttempts <= 1 ? '' : ', startAttempts: ' + startAttempts.toString()
}): %s %s`
if (kind === 'debug') {
this.log.debug(fmt, timeout.elapsedTime / 1000, getName(), statusesToString())
} else if (kind === 'error') {
this.log.error(fmt, timeout.elapsedTime / 1000, getName(), statusesToString())
} else {
this.log.info(fmt, timeout.elapsedTime / 1000, getName(), statusesToString())
}
}
const progress = await showMessageWithCancel(
localize('AWS.CodeCatalyst.devenv.message', 'CodeCatalyst'),
timeout
)
progress.report({ message: localize('AWS.CodeCatalyst.devenv.checking', 'Checking status...') })
try {
const devenv = await this.getDevEnvironment(args)
alias = devenv.alias
statuses.push({ status: devenv.status, start: Date.now() })
if (devenv.status === 'RUNNING') {
doLog('debug', 'devenv RUNNING')
timeout.cancel()
// "Debounce" in case caller did not check if the environment was already running.
return devenv
}
} catch {
// Continue.
}
doLog('debug', 'devenv not started, waiting')
const pollDevEnv = waitUntil(
async () => {
if (timeout.completed) {
// TODO: need a better way to "cancel" a `waitUntil`.
throw new CancellationError('user')
}
const LastSeenStatus = statuses[statuses.length - 1]
const elapsed = Date.now() - LastSeenStatus.start
const resp = await this.getDevEnvironment(args)
const serviceReason = (resp.statusReason ?? '').trim()
alias = resp.alias
if (
startAttempts > 2 &&
elapsed > 10000 &&
['STOPPED', 'FAILED'].includes(LastSeenStatus.status) &&
['STOPPED', 'FAILED'].includes(resp.status)
) {
const fails = statuses.filter((o) => o.status === 'FAILED').length
const code = fails === 0 ? 'BadDevEnvState' : 'FailedDevEnv'
if (serviceReason !== '') {
// Service gave a status reason like "Compute limit exceeded", show it to the user.
throw new ToolkitError(failedStartMsg(resp.statusReason), { code: code })
}
// If still STOPPED/FAILED after 10+ seconds, don't keep retrying for 1 hour...
throw new ToolkitError(failedStartMsg(), { code: code })
} else if (['STOPPED', 'FAILED'].includes(resp.status)) {
progress.report({
message: localize('AWS.CodeCatalyst.devenv.resuming', 'Resuming Dev Environment...'),
})
try {
startAttempts++
await this.startDevEnvironment(args)
} catch (e) {
const err = e as AWS.AWSError
// - ServiceQuotaExceededException: account billing limit reached
// - ValidationException: "… creation has failed, cannot start"
// - ConflictException: "Cannot start … because update process is still going on"
// (can happen after "Update Dev Environment")
if (err.code === 'ServiceQuotaExceededException') {
throw new ToolkitError('Dev Environment failed: quota exceeded', {
code: 'ServiceQuotaExceeded',
cause: err,
})
}
doLog('info', `devenv not started (${err.code}), waiting`)
// Continue retrying...
}
} else if (resp.status === 'STOPPING') {
progress.report({
message: localize('AWS.CodeCatalyst.devenv.stopping', 'Waiting for Dev Environment to stop...'),
})
} else {
progress.report({
message: localize('AWS.CodeCatalyst.devenv.starting', 'Opening Dev Environment...'),
})
}
if (LastSeenStatus?.status !== resp.status) {
statuses.push({ status: resp.status, start: Date.now() })
if (resp.status !== 'RUNNING') {
doLog('debug', `devenv not started, waiting`)
}
}
return resp.status === 'RUNNING' ? resp : undefined
},
// note: the `waitUntil` will resolve prior to the real timeout if it is refreshed
{ interval: 1000, timeout: timeout.remainingTime, truthy: true }
)
const devenv = await waitTimeout(pollDevEnv, timeout).catch((e) => {
if (isUserCancelledError(e)) {
doLog('info', 'devenv failed to start (user cancelled)')
e.message = failedStartMsg()
throw e
} else if (e instanceof ToolkitError) {
doLog('error', 'devenv failed to start')
throw e
}
doLog('error', 'devenv failed to start')
throw new ToolkitError(failedStartMsg(), { code: 'Unknown', cause: e })
})
if (!devenv) {
doLog('error', 'devenv failed to start (timeout)')
throw new ToolkitError(failedStartMsg(), { code: 'Timeout' })
}
doLog('info', 'devenv started')
return devenv
}