Skip to content

Amazon Q Code Transform: Improve download error handling ux #4530

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type" : "feature",
"description" : "Amazon Q Code Transform: Communicate download failure in transform chat, and improve download failure notification"
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.services.amazonq.CODE_TRANSFORM_TROUBLESHOOT_DOC_ARTIFACT
import software.aws.toolkits.jetbrains.services.codemodernizer.client.GumbyClient
import software.aws.toolkits.jetbrains.services.codemodernizer.commands.CodeTransformMessageListener
import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeModernizerArtifact
import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeTransformHilDownloadArtifact
import software.aws.toolkits.jetbrains.services.codemodernizer.model.DownloadFailureReason
Expand Down Expand Up @@ -154,17 +155,18 @@ class ArtifactHandler(private val project: Project, private val clientAdaptor: G
)
}
} catch (e: Exception) {
var errorMessage: String = e.message.orEmpty()
// SdkClientException will be thrown, masking actual issues like SSLHandshakeException underneath
if (e.message.toString().contains(DOWNLOAD_PROXY_WILDCARD_ERROR)) {
notifyUnableToDownload(
DownloadFailureReason.PROXY_WILDCARD_ERROR,
)
errorMessage = message("codemodernizer.notification.warn.download_failed_wildcard.content")
CodeTransformMessageListener.instance.onDownloadFailure(DownloadFailureReason.PROXY_WILDCARD_ERROR)
} else if (e.message.toString().contains(DOWNLOAD_SSL_HANDSHAKE_ERROR)) {
notifyUnableToDownload(
DownloadFailureReason.SSL_HANDSHAKE_ERROR,
)
errorMessage = message("codemodernizer.notification.warn.download_failed_ssl.content")
CodeTransformMessageListener.instance.onDownloadFailure(DownloadFailureReason.SSL_HANDSHAKE_ERROR)
} else {
CodeTransformMessageListener.instance.onDownloadFailure(DownloadFailureReason.OTHER(e.message.toString()))
}
return DownloadArtifactResult(null, "", e.message.orEmpty())
return DownloadArtifactResult(null, "", errorMessage)
} finally {
isCurrentlyDownloading.set(false)
}
Expand Down Expand Up @@ -200,23 +202,6 @@ class ArtifactHandler(private val project: Project, private val clientAdaptor: G
}
}

fun notifyUnableToDownload(error: DownloadFailureReason) {
LOG.error { "Unable to download artifact: $error" }
if (error == DownloadFailureReason.PROXY_WILDCARD_ERROR) {
notifyStickyWarn(
message("codemodernizer.notification.warn.view_diff_failed.title"),
message("codemodernizer.notification.warn.download_failed_wildcard.content", error),
project,
)
} else if (error == DownloadFailureReason.SSL_HANDSHAKE_ERROR) {
notifyStickyWarn(
message("codemodernizer.notification.warn.view_diff_failed.title"),
message("codemodernizer.notification.warn.download_failed_ssl.content", error),
project,
)
}
}

fun notifyUnableToApplyPatch(patchPath: String, errorMessage: String) {
LOG.error { "Unable to find patch for file: $patchPath" }
notifyStickyWarn(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.intellij.serviceContainer.AlreadyDisposedException
import com.intellij.util.io.HttpRequests
import kotlinx.coroutines.delay
import org.apache.commons.codec.digest.DigestUtils
import software.amazon.awssdk.core.exception.SdkClientException
import software.amazon.awssdk.services.codewhispererruntime.model.ResumeTransformationResponse
import software.amazon.awssdk.services.codewhispererruntime.model.StartTransformationResponse
import software.amazon.awssdk.services.codewhispererruntime.model.TransformationJob
Expand Down Expand Up @@ -54,6 +55,7 @@ import java.time.Instant
import java.util.Base64
import java.util.concurrent.CancellationException
import java.util.concurrent.atomic.AtomicBoolean
import javax.net.ssl.SSLHandshakeException

const val ZIP_SOURCES_PATH = "sources"
const val BUILD_LOG_PATH = "build-logs.txt"
Expand All @@ -62,6 +64,10 @@ const val MAX_ZIP_SIZE = 1000000000 // 1GB
const val HIL_1P_UPGRADE_CAPABILITY = "HIL_1pDependency_VersionUpgrade"
const val EXPLAINABILITY_V1 = "EXPLAINABILITY_V1"

// constants for handling SDKClientException
const val CONNECTION_REFUSED_ERROR: String = "Connection refused"
const val SSL_HANDSHAKE_ERROR: String = "Unable to execute HTTP request: PKIX path building failed"

class CodeModernizerSession(
val sessionContext: CodeModernizerSessionContext,
private val initialPollingSleepDurationMillis: Long = 2000,
Expand Down Expand Up @@ -185,6 +191,10 @@ class CodeModernizerSession(
state.putJobHistory(sessionContext, TransformationStatus.FAILED)
state.currentJobStatus = TransformationStatus.FAILED
return CodeModernizerStartJobResult.ZipUploadFailed(UploadFailureReason.CONNECTION_REFUSED)
} catch (e: SSLHandshakeException) {
state.putJobHistory(sessionContext, TransformationStatus.FAILED)
state.currentJobStatus = TransformationStatus.FAILED
return CodeModernizerStartJobResult.ZipUploadFailed(UploadFailureReason.SSL_HANDSHAKE_ERROR)
} catch (e: HttpRequests.HttpStatusException) {
state.putJobHistory(sessionContext, TransformationStatus.FAILED)
state.currentJobStatus = TransformationStatus.FAILED
Expand All @@ -204,6 +214,17 @@ class CodeModernizerSession(
state.currentJobStatus = TransformationStatus.FAILED
return CodeModernizerStartJobResult.ZipUploadFailed(UploadFailureReason.OTHER(e.localizedMessage.toString()))
}
} catch (e: SdkClientException) {
// Errors from code whisperer client will always be thrown as SdkClientException
state.putJobHistory(sessionContext, TransformationStatus.FAILED)
state.currentJobStatus = TransformationStatus.FAILED
return if (e.message.toString().contains(CONNECTION_REFUSED_ERROR)) {
CodeModernizerStartJobResult.ZipUploadFailed(UploadFailureReason.CONNECTION_REFUSED)
} else if (e.message.toString().contains(SSL_HANDSHAKE_ERROR)) {
CodeModernizerStartJobResult.ZipUploadFailed(UploadFailureReason.SSL_HANDSHAKE_ERROR)
} else {
CodeModernizerStartJobResult.ZipUploadFailed(UploadFailureReason.OTHER(e.localizedMessage.toString()))
}
} catch (e: Exception) {
state.putJobHistory(sessionContext, TransformationStatus.FAILED)
state.currentJobStatus = TransformationStatus.FAILED
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ package software.aws.toolkits.jetbrains.services.codemodernizer.commands
import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeModernizerJobCompletedResult
import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeTransformHilDownloadArtifact
import software.aws.toolkits.jetbrains.services.codemodernizer.model.DownloadFailureReason
import software.aws.toolkits.jetbrains.services.codemodernizer.model.MavenCopyCommandsResult

data class CodeTransformActionMessage(
val command: CodeTransformCommand,
val mavenBuildResult: MavenCopyCommandsResult? = null,
val transformResult: CodeModernizerJobCompletedResult? = null,
val hilDownloadArtifact: CodeTransformHilDownloadArtifact? = null,
val downloadFailure: DownloadFailureReason? = null,
) : AmazonQMessage
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ enum class CodeTransformCommand {
UploadComplete,
TransformComplete,
TransformResuming,
DownloadFailed,
AuthRestored,
StartHil,
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package software.aws.toolkits.jetbrains.services.codemodernizer.commands
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeModernizerJobCompletedResult
import software.aws.toolkits.jetbrains.services.codemodernizer.model.DownloadFailureReason
import software.aws.toolkits.jetbrains.services.codemodernizer.model.MavenCopyCommandsResult

class CodeTransformMessageListener {
Expand Down Expand Up @@ -41,6 +42,10 @@ class CodeTransformMessageListener {
_messages.tryEmit(CodeTransformActionMessage(CodeTransformCommand.TransformResuming))
}

fun onDownloadFailure(failure: DownloadFailureReason) {
_messages.tryEmit(CodeTransformActionMessage(CodeTransformCommand.DownloadFailed, downloadFailure = failure))
}

fun onAuthRestored() {
_messages.tryEmit(CodeTransformActionMessage(CodeTransformCommand.AuthRestored))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import software.aws.toolkits.jetbrains.services.codemodernizer.messages.FormItem
import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeModernizerJobCompletedResult
import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeTransformHilDownloadArtifact
import software.aws.toolkits.jetbrains.services.codemodernizer.model.Dependency
import software.aws.toolkits.jetbrains.services.codemodernizer.model.DownloadFailureReason
import software.aws.toolkits.jetbrains.services.codemodernizer.model.UploadFailureReason
import software.aws.toolkits.jetbrains.services.codemodernizer.model.ValidationResult
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.getModuleOrProjectNameForFile
Expand Down Expand Up @@ -241,6 +242,9 @@ fun buildZipUploadFailedChatMessage(failureReason: UploadFailureReason): String
is UploadFailureReason.CONNECTION_REFUSED -> {
message("codemodernizer.chat.message.upload_failed_connection_refused")
}
is UploadFailureReason.SSL_HANDSHAKE_ERROR -> {
message("codemodernizer.chat.message.upload_failed_ssl_error")
}
is UploadFailureReason.OTHER -> {
message("codemodernizer.chat.message.upload_failed_other", failureReason.errorMessage)
}
Expand Down Expand Up @@ -433,3 +437,28 @@ fun buildHilCannotResumeContent() = CodeTransformChatMessageContent(
startNewTransformFollowUp
),
)

fun buildDownloadFailureChatContent(downloadFailureReason: DownloadFailureReason): CodeTransformChatMessageContent {
val reason = when (downloadFailureReason) {
is DownloadFailureReason.SSL_HANDSHAKE_ERROR -> {
message("codemodernizer.chat.message.download_failed_ssl")
}
is DownloadFailureReason.PROXY_WILDCARD_ERROR -> {
message("codemodernizer.chat.message.download_failed_wildcard")
}
is DownloadFailureReason.OTHER -> {
message("codemodernizer.chat.message.download_failed_other", downloadFailureReason.errorMessage)
}
}

// DownloadFailureReason.OTHER might be retryable, so including buttons to allow retry.
return CodeTransformChatMessageContent(
type = CodeTransformChatMessageType.FinalizedAnswer,
message = reason,
buttons = if (downloadFailureReason is DownloadFailureReason.SSL_HANDSHAKE_ERROR || downloadFailureReason is DownloadFailureReason.OTHER) {
listOf(viewDiffButton, viewSummaryButton)
} else {
null
},
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildCo
import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildCompileLocalFailedChatContent
import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildCompileLocalInProgressChatContent
import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildCompileLocalSuccessChatContent
import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildDownloadFailureChatContent
import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildHilCannotResumeContent
import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildHilErrorContent
import software.aws.toolkits.jetbrains.services.codemodernizer.constants.buildHilInitialContent
Expand Down Expand Up @@ -59,6 +60,7 @@ import software.aws.toolkits.jetbrains.services.codemodernizer.messages.Incoming
import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeModernizerJobCompletedResult
import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeTransformHilDownloadArtifact
import software.aws.toolkits.jetbrains.services.codemodernizer.model.CustomerSelection
import software.aws.toolkits.jetbrains.services.codemodernizer.model.DownloadFailureReason
import software.aws.toolkits.jetbrains.services.codemodernizer.model.JobId
import software.aws.toolkits.jetbrains.services.codemodernizer.model.MavenCopyCommandsResult
import software.aws.toolkits.jetbrains.services.codemodernizer.model.MavenDependencyReportCommandsResult
Expand Down Expand Up @@ -308,6 +310,12 @@ class CodeTransformChatController(
CodeTransformCommand.StartHil -> {
handleHil()
}
CodeTransformCommand.DownloadFailed -> {
val result = message.downloadFailure
if (result != null) {
handleDownloadFailed(message.downloadFailure)
}
}
else -> {
processTransformQuickAction(IncomingCodeTransformMessage.Transform(tabId = activeTabId))
}
Expand Down Expand Up @@ -457,6 +465,11 @@ class CodeTransformChatController(
}
}

private suspend fun handleDownloadFailed(failureReason: DownloadFailureReason) {
codeTransformChatHelper.addNewMessage(buildDownloadFailureChatContent(failureReason))
codeTransformChatHelper.addNewMessage(buildStartNewTransformFollowup())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker: How many failure functions call buildStartNewTransformFollowup? 6-7?

Its out of scope for this PR, but would be a nice to have this abstracted so any failure state that calls buildStartNewTransformFollowup goes through a single function.

}

// Remove open file button after pom.xml is deleted
private suspend fun updatePomPreviewItem() {
val hilPomItemId = codeTransformChatHelper.getHilPomItemId() ?: return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ package software.aws.toolkits.jetbrains.services.codemodernizer.model
sealed class DownloadFailureReason {
object SSL_HANDSHAKE_ERROR : DownloadFailureReason()
object PROXY_WILDCARD_ERROR : DownloadFailureReason()
object OTHER : DownloadFailureReason()
data class OTHER(val errorMessage: String) : DownloadFailureReason()
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ sealed class UploadFailureReason {
data class HTTP_ERROR(val statusCode: Int) : UploadFailureReason()
object PRESIGNED_URL_EXPIRED : UploadFailureReason()
object CONNECTION_REFUSED : UploadFailureReason()
object SSL_HANDSHAKE_ERROR : UploadFailureReason()
data class OTHER(val errorMessage: String) : UploadFailureReason()
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,11 @@ import org.mockito.kotlin.any
import org.mockito.kotlin.doNothing
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.eq
import org.mockito.kotlin.spy
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeModernizerArtifact
import software.aws.toolkits.jetbrains.services.codemodernizer.model.DownloadFailureReason
import software.aws.toolkits.jetbrains.services.codemodernizer.model.InvalidTelemetryReason
import software.aws.toolkits.jetbrains.services.codemodernizer.model.ValidationResult
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.filterOnlyParentFiles
Expand Down Expand Up @@ -53,11 +51,9 @@ class CodeWhispererCodeModernizerTest : CodeWhispererCodeModernizerTestBase() {
fun `ArtifactHandler notifies proxy wildcard error`() = runBlocking {
val handler = spy(ArtifactHandler(project, clientAdaptorSpy))
doThrow(RuntimeException("Dangling meta character '*' near index 0")).whenever(clientAdaptorSpy).downloadExportResultArchive(jobId)
doNothing().whenever(handler).notifyUnableToDownload(eq(DownloadFailureReason.PROXY_WILDCARD_ERROR))
val expectedResult = DownloadArtifactResult(null, "", "Dangling meta character '*' near index 0")
val expectedResult = DownloadArtifactResult(null, "", message("codemodernizer.notification.warn.download_failed_wildcard.content"))
val result = handler.downloadArtifact(jobId)
verify(clientAdaptorSpy, times(1)).downloadExportResultArchive(jobId)
verify(handler, times(1)).notifyUnableToDownload(DownloadFailureReason.PROXY_WILDCARD_ERROR)
assertEquals(expectedResult, result)
}

Expand All @@ -66,15 +62,13 @@ class CodeWhispererCodeModernizerTest : CodeWhispererCodeModernizerTestBase() {
val handler = spy(ArtifactHandler(project, clientAdaptorSpy))
doThrow(RuntimeException("Unable to execute HTTP request: javax.net.ssl.SSLHandshakeException: PKIX path building failed"))
.whenever(clientAdaptorSpy).downloadExportResultArchive(jobId)
doNothing().whenever(handler).notifyUnableToDownload(eq(DownloadFailureReason.SSL_HANDSHAKE_ERROR))
val expectedResult = DownloadArtifactResult(
null,
"",
"Unable to execute HTTP request: javax.net.ssl.SSLHandshakeException: PKIX path building failed"
message("codemodernizer.notification.warn.download_failed_ssl.content")
)
val result = handler.downloadArtifact(jobId)
verify(clientAdaptorSpy, times(1)).downloadExportResultArchive(jobId)
verify(handler, times(1)).notifyUnableToDownload(DownloadFailureReason.SSL_HANDSHAKE_ERROR)
assertEquals(expectedResult, result)
}

Expand Down
Loading
Loading