Skip to content

Commit f6d853a

Browse files
authored
Q Code Transform: Fix failed uploads and downloads when credentials expire (#4573)
~11% of API failures in May are due to expired credentials being used (InvalidGrantException). The most common places in the code where this happens is when users are about to upload their code or when about to download the code. This is due to at these points customers may have been inactive for a substantial amount of time (>15 min) either due to long running `mvn` builds or due to being afk before downloading results. This PR fixes this by forcing a refresh through chat when token is marked as NEED_REFRESH or if we get a SssOidcException. Additionally in the case of downloading an archive we need to resume the job after credential refresh such that users can reattempt the download. Detailed flow for the two cases detailed below: ### Flow when token expired when calling CreateUpload: 1. Chat asks user to reauthenticate 2. Notification asks user to reauthenticate 3. Clicking either causes reauthentication flow to run 4. When user successfully authenticates then the user is requested to start a new flow ### Flow when token expired before calling ExportResultArchive 1. Chat asks user to reauthenticate 2. Notification asks user to reauth 3. Clicking either causes reauthentication flow to run 4. When user authenticates successfully then the job is resumed 6. When job fully resumes users are notified to download the results again as normal ### Misc changes (bugfixes / supporting changes) 1. [Supporting change] - Tab opens on resume without user needing to enter /transform 2. [Supporting change] - Added BearerToken listener to listen for authentication successes so that chat could be informed when user reauthenticates 3. [Bug fix / Supporting change] - Removed notification “Job completed when you were away” as it was a duplicate and messes up resume flow (as users can click while we are attempting to resume job which causes issues in chat) 4. [Bug fix / Supporting change] - Removed notification “Job failed when you were away” as it was a duplicate and messes up resume flow (as users can click while we are attempting to resume job which causes issues in chat) 5. [Bug fix] - Removed notification when job failed during download when more specific error notifications are already shown.
1 parent 4a48c96 commit f6d853a

File tree

22 files changed

+576
-226
lines changed

22 files changed

+576
-226
lines changed

plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ArtifactHandler.kt

Lines changed: 145 additions & 61 deletions
Large diffs are not rendered by default.

plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeModernizerManager.kt

Lines changed: 44 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package software.aws.toolkits.jetbrains.services.codemodernizer
44

55
import com.intellij.notification.NotificationAction
66
import com.intellij.openapi.Disposable
7+
import com.intellij.openapi.actionSystem.AnAction
78
import com.intellij.openapi.application.ApplicationManager
89
import com.intellij.openapi.application.runInEdt
910
import com.intellij.openapi.components.PersistentStateComponent
@@ -47,6 +48,7 @@ import software.aws.toolkits.jetbrains.services.codemodernizer.model.JobId
4748
import software.aws.toolkits.jetbrains.services.codemodernizer.model.MAVEN_CONFIGURATION_FILE_NAME
4849
import software.aws.toolkits.jetbrains.services.codemodernizer.model.MavenCopyCommandsResult
4950
import software.aws.toolkits.jetbrains.services.codemodernizer.model.MavenDependencyReportCommandsResult
51+
import software.aws.toolkits.jetbrains.services.codemodernizer.model.UploadFailureReason
5052
import software.aws.toolkits.jetbrains.services.codemodernizer.model.ValidationResult
5153
import software.aws.toolkits.jetbrains.services.codemodernizer.panels.managers.CodeModernizerBottomWindowPanelManager
5254
import software.aws.toolkits.jetbrains.services.codemodernizer.state.CodeModernizerSessionState
@@ -197,6 +199,7 @@ class CodeModernizerManager(private val project: Project) : PersistentStateCompo
197199
*/
198200
fun addCodeModernizeUI(setSelected: Boolean = false, moduleOrProjectNameForFile: String? = null) = runInEdt {
199201
val appModernizerBottomWindow = getBottomToolWindow()
202+
appModernizerBottomWindow.isAvailable = true
200203
if (!appModernizerBottomWindow.contentManager.contents.contains(codeModernizerBottomWindowPanelContent)) {
201204
appModernizerBottomWindow.contentManager.addContent(codeModernizerBottomWindowPanelContent)
202205
}
@@ -249,13 +252,13 @@ class CodeModernizerManager(private val project: Project) : PersistentStateCompo
249252
CodeModernizerSessionState.getInstance(project).currentJobStopTime = Instant.MIN
250253
}
251254

252-
private fun notifyJobFailure(failureReason: String?) {
255+
private fun notifyJobFailure(failureReason: String?, actions: Collection<AnAction> = listOf()) {
253256
val reason = failureReason ?: message("codemodernizer.notification.info.modernize_failed.unknown_failure_reason") // should not happen
254257
notifyStickyInfo(
255258
message("codemodernizer.notification.info.modernize_failed.title"),
256259
reason,
257260
project,
258-
listOf(displayFeedbackNotificationAction())
261+
listOf(displayFeedbackNotificationAction(), *actions.toTypedArray())
259262
)
260263
}
261264

@@ -301,6 +304,8 @@ class CodeModernizerManager(private val project: Project) : PersistentStateCompo
301304
}
302305

303306
fun resumeJob(session: CodeModernizerSession, lastJobId: JobId, currentJobResult: TransformationJob) = projectCoroutineScope(project).launch {
307+
isJobSuccessfullyResumed.set(true)
308+
CodeTransformMessageListener.instance.onTransformResuming()
304309
if (isModernizationJobActive()) {
305310
runInEdt { getBottomToolWindow().show() }
306311
return@launch
@@ -340,6 +345,8 @@ class CodeModernizerManager(private val project: Project) : PersistentStateCompo
340345
managerState.flags[StateFlags.IS_ONGOING] = false
341346
}
342347

348+
fun isJobOngoingInState() = managerState.flags.getOrDefault(StateFlags.IS_ONGOING, false)
349+
343350
fun handleLocalMavenBuildResult(mavenCopyCommandsResult: MavenCopyCommandsResult) {
344351
codeTransformationSession?.setLastMvnBuildResult(mavenCopyCommandsResult)
345352
// Send IDE notifications first
@@ -467,6 +474,9 @@ class CodeModernizerManager(private val project: Project) : PersistentStateCompo
467474
}
468475
}
469476

477+
/**
478+
* Silently try to resume the job, informs users only when job successfully resumed, suppresses exceptions.
479+
*/
470480
fun tryResumeJob(onProjectFirstOpen: Boolean = false) = projectCoroutineScope(project).launch {
471481
try {
472482
val notYetResumed = isResumingJob.compareAndSet(false, true)
@@ -492,40 +502,16 @@ class CodeModernizerManager(private val project: Project) : PersistentStateCompo
492502
val result = session.getJobDetails(lastJobId)
493503
when (result.status()) {
494504
TransformationStatus.COMPLETED -> {
495-
CodeTransformMessageListener.instance.onTransformResuming()
496-
notifyStickyInfo(
497-
message("codemodernizer.manager.job_finished_title"),
498-
message("codemodernizer.manager.job_finished_content"),
499-
project,
500-
listOf(
501-
displayDiffNotificationAction(lastJobId),
502-
displaySummaryNotificationAction(lastJobId),
503-
viewTransformationHubAction()
504-
)
505-
)
506505
resumeJob(session, lastJobId, result)
507506
setJobNotOngoing()
508507
}
509508

510509
TransformationStatus.PARTIALLY_COMPLETED -> {
511-
CodeTransformMessageListener.instance.onTransformResuming()
512-
notifyStickyInfo(
513-
message("codemodernizer.notification.info.modernize_failed.title"),
514-
message("codemodernizer.manager.job_failed_content", result.reason()),
515-
project,
516-
listOf(
517-
displayDiffNotificationAction(lastJobId),
518-
displaySummaryNotificationAction(lastJobId),
519-
displayFeedbackNotificationAction(),
520-
viewTransformationHubAction()
521-
)
522-
)
523510
resumeJob(session, lastJobId, result)
524511
setJobNotOngoing()
525512
}
526513

527514
TransformationStatus.UNKNOWN_TO_SDK_VERSION -> {
528-
CodeTransformMessageListener.instance.onTransformResuming()
529515
notifyStickyInfo(
530516
message("codemodernizer.notification.warn.on_resume.unknown_status_response.title"),
531517
message("codemodernizer.notification.warn.on_resume.unknown_status_response.content"),
@@ -534,13 +520,11 @@ class CodeModernizerManager(private val project: Project) : PersistentStateCompo
534520
}
535521

536522
TransformationStatus.STOPPED, TransformationStatus.STOPPING -> {
537-
CodeTransformMessageListener.instance.onTransformResuming()
523+
// If user stopped the last job, there is no need for us to resume the job
538524
setJobNotOngoing()
539525
}
540526

541527
else -> {
542-
isJobSuccessfullyResumed.set(true)
543-
CodeTransformMessageListener.instance.onTransformResuming()
544528
resumeJob(session, lastJobId, result)
545529
notifyStickyInfo(
546530
message("codemodernizer.manager.job_ongoing_title"),
@@ -561,10 +545,6 @@ class CodeModernizerManager(private val project: Project) : PersistentStateCompo
561545
}
562546
}
563547

564-
private fun viewTransformationHubAction() = NotificationAction.createSimple(message("codemodernizer.notification.info.modernize_complete.view_summary")) {
565-
getBottomToolWindow().show()
566-
}
567-
568548
private fun resumeJobNotificationAction(session: CodeModernizerSession, lastJobId: JobId, currentJobResult: TransformationJob) =
569549
NotificationAction.createSimple(message("codemodernizer.notification.info.modernize_ongoing.view_status")) {
570550
resumeJob(session, lastJobId, currentJobResult)
@@ -593,9 +573,24 @@ class CodeModernizerManager(private val project: Project) : PersistentStateCompo
593573
result.failureReason
594574
)
595575

596-
is CodeModernizerJobCompletedResult.ZipUploadFailed -> notifyJobFailure(
597-
message("codemodernizer.notification.warn.upload_failed", result.failureReason.toString()),
598-
)
576+
is CodeModernizerJobCompletedResult.ZipUploadFailed -> {
577+
if (result.failureReason is UploadFailureReason.CREDENTIALS_EXPIRED) {
578+
setJobNotOngoing()
579+
CodeTransformMessageListener.instance.onCheckAuth()
580+
notifyJobFailure(
581+
message("codemodernizer.notification.warn.upload_failed_expired_credentials.content"),
582+
listOf(
583+
NotificationAction.createSimpleExpiring(message("codemodernizer.notification.warn.action.reauthenticate")) {
584+
CodeTransformMessageListener.instance.onReauthStarted()
585+
}
586+
)
587+
)
588+
} else {
589+
notifyJobFailure(
590+
message("codemodernizer.notification.warn.upload_failed", result.failureReason.toString()),
591+
)
592+
}
593+
}
599594

600595
is CodeModernizerJobCompletedResult.RetryableFailure -> notifyJobFailure(
601596
result.failureReason
@@ -902,4 +897,17 @@ class CodeModernizerManager(private val project: Project) : PersistentStateCompo
902897

903898
return true
904899
}
900+
901+
/**
902+
* When customer attempts to download an artifact and it fails for some reason (credential expiry etc)
903+
* we need to be able to resume the job in order for customers to be able to reattempt the download.
904+
* This sets the job as ongoing in the persistent state so that when tryResumeJob is triggered the
905+
* IDE attempts to resume the job.
906+
*/
907+
fun handleResumableDownloadArtifactFailure(job: JobId) {
908+
// handle the case when user clicks long living notification but has a new job running
909+
val session = this.codeTransformationSession ?: return
910+
if (session.getActiveJobId() != job) return
911+
setJobOngoing(job, session.sessionContext)
912+
}
905913
}

plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeModernizerSession.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import software.amazon.awssdk.services.codewhispererruntime.model.Transformation
1919
import software.amazon.awssdk.services.codewhispererruntime.model.TransformationStatus
2020
import software.amazon.awssdk.services.codewhispererruntime.model.TransformationUserActionStatus
2121
import software.amazon.awssdk.services.codewhispererstreaming.model.TransformationDownloadArtifactType
22+
import software.amazon.awssdk.services.ssooidc.model.SsoOidcException
2223
import software.aws.toolkits.core.utils.error
2324
import software.aws.toolkits.core.utils.exists
2425
import software.aws.toolkits.core.utils.getLogger
@@ -31,6 +32,7 @@ import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeModerni
3132
import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeModernizerSessionContext
3233
import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeModernizerStartJobResult
3334
import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeTransformHilDownloadArtifact
35+
import software.aws.toolkits.jetbrains.services.codemodernizer.model.DownloadArtifactResult
3436
import software.aws.toolkits.jetbrains.services.codemodernizer.model.JobId
3537
import software.aws.toolkits.jetbrains.services.codemodernizer.model.MavenCopyCommandsResult
3638
import software.aws.toolkits.jetbrains.services.codemodernizer.model.MavenDependencyReportCommandsResult
@@ -42,6 +44,7 @@ import software.aws.toolkits.jetbrains.services.codemodernizer.utils.STATES_AFTE
4244
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.STATES_AFTER_STARTED
4345
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.getModuleOrProjectNameForFile
4446
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.getPathToHilDependencyReportDir
47+
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isValidCodeTransformConnection
4548
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.pollTransformationStatusAndPlan
4649
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.toTransformationLanguage
4750
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanSession
@@ -146,6 +149,10 @@ class CodeModernizerSession(
146149
val payload: File?
147150

148151
// Generate zip file
152+
if (!isValidCodeTransformConnection(sessionContext.project)) {
153+
// Creating zip can take some time, so quit early
154+
return CodeModernizerStartJobResult.ZipUploadFailed(UploadFailureReason.CREDENTIALS_EXPIRED)
155+
}
149156
try {
150157
if (isDisposed.get()) {
151158
LOG.warn { "Disposed when about to create zip to upload" }
@@ -181,6 +188,9 @@ class CodeModernizerSession(
181188
}
182189

183190
// Create upload url and upload zip
191+
if (!isValidCodeTransformConnection(sessionContext.project)) {
192+
return CodeModernizerStartJobResult.ZipUploadFailed(UploadFailureReason.CREDENTIALS_EXPIRED)
193+
}
184194
var uploadId = ""
185195
try {
186196
if (shouldStop.get()) {
@@ -218,6 +228,10 @@ class CodeModernizerSession(
218228
state.currentJobStatus = TransformationStatus.FAILED
219229
return CodeModernizerStartJobResult.ZipUploadFailed(UploadFailureReason.OTHER(e.localizedMessage.toString()))
220230
}
231+
} catch (e: SsoOidcException) {
232+
state.putJobHistory(sessionContext, TransformationStatus.FAILED)
233+
state.currentJobStatus = TransformationStatus.FAILED
234+
return CodeModernizerStartJobResult.ZipUploadFailed(UploadFailureReason.CREDENTIALS_EXPIRED)
221235
} catch (e: SdkClientException) {
222236
// Errors from code whisperer client will always be thrown as SdkClientException
223237
state.putJobHistory(sessionContext, TransformationStatus.FAILED)
@@ -486,7 +500,7 @@ class CodeModernizerSession(
486500
// This is a short term solution to check if build log is available by attempting to download it.
487501
// In the long term, we should check if build log is available from transformation metadata.
488502
val downloadArtifactResult = artifactHandler.downloadArtifact(jobId, TransformationDownloadArtifactType.LOGS, true)
489-
if (downloadArtifactResult.artifact != null) {
503+
if (downloadArtifactResult is DownloadArtifactResult.Success) {
490504
val failureReason = result.jobDetails?.reason() ?: message("codemodernizer.notification.warn.maven_failed.content")
491505
return CodeModernizerJobCompletedResult.JobFailedInitialBuild(jobId, failureReason, true)
492506
} else {

0 commit comments

Comments
 (0)