@@ -12,6 +12,8 @@ import software.amazon.awssdk.services.codewhispererruntime.model.CodeAnalysisUp
12
12
import software.amazon.awssdk.services.codewhispererruntime.model.CodeFixUploadContext
13
13
import software.amazon.awssdk.services.codewhispererruntime.model.CreateUploadUrlRequest
14
14
import software.amazon.awssdk.services.codewhispererruntime.model.CreateUploadUrlResponse
15
+ import software.amazon.awssdk.services.codewhispererruntime.model.InternalServerException
16
+ import software.amazon.awssdk.services.codewhispererruntime.model.ThrottlingException
15
17
import software.amazon.awssdk.services.codewhispererruntime.model.UploadContext
16
18
import software.amazon.awssdk.services.codewhispererruntime.model.UploadIntent
17
19
import software.amazon.awssdk.utils.IoUtils
@@ -82,40 +84,50 @@ class CodeWhispererZipUploadManager(private val project: Project) {
82
84
requestHeaders : Map <String , String >? ,
83
85
featureUseCase : CodeWhispererConstants .FeatureName ,
84
86
) {
85
- try {
86
- val uploadIdJson = """ {"uploadId":"$uploadId "}"""
87
- HttpRequests .put(url, " application/zip" ).userAgent(AwsClientManager .getUserAgent()).tuner {
88
- if (requestHeaders.isNullOrEmpty()) {
89
- it.setRequestProperty(CONTENT_MD5 , md5)
90
- it.setRequestProperty(CONTENT_TYPE , APPLICATION_ZIP )
91
- it.setRequestProperty(SERVER_SIDE_ENCRYPTION , AWS_KMS )
92
- if (kmsArn?.isNotEmpty() == true ) {
93
- it.setRequestProperty(SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID , kmsArn)
94
- }
95
- it.setRequestProperty(SERVER_SIDE_ENCRYPTION_CONTEXT , Base64 .getEncoder().encodeToString(uploadIdJson.toByteArray()))
96
- } else {
97
- requestHeaders.forEach { entry ->
98
- it.setRequestProperty(entry.key, entry.value)
87
+ RetryableOperation <Unit >().execute(
88
+ operation = {
89
+ val uploadIdJson = """ {"uploadId":"$uploadId "}"""
90
+ HttpRequests .put(url, " application/zip" ).userAgent(AwsClientManager .getUserAgent()).tuner {
91
+ if (requestHeaders.isNullOrEmpty()) {
92
+ it.setRequestProperty(CONTENT_MD5 , md5)
93
+ it.setRequestProperty(CONTENT_TYPE , APPLICATION_ZIP )
94
+ it.setRequestProperty(SERVER_SIDE_ENCRYPTION , AWS_KMS )
95
+ if (kmsArn?.isNotEmpty() == true ) {
96
+ it.setRequestProperty(SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID , kmsArn)
97
+ }
98
+ it.setRequestProperty(SERVER_SIDE_ENCRYPTION_CONTEXT , Base64 .getEncoder().encodeToString(uploadIdJson.toByteArray()))
99
+ } else {
100
+ requestHeaders.forEach { entry ->
101
+ it.setRequestProperty(entry.key, entry.value)
102
+ }
99
103
}
104
+ }.connect {
105
+ val connection = it.connection as HttpURLConnection
106
+ connection.setFixedLengthStreamingMode(fileToUpload.length())
107
+ IoUtils .copy(fileToUpload.inputStream(), connection.outputStream)
108
+ }
109
+ },
110
+ isRetryable = { e ->
111
+ when (e) {
112
+ is IOException -> true
113
+ else -> false
114
+ }
115
+ },
116
+ errorHandler = { e, attempts ->
117
+ val errorMessage = getTelemetryErrorMessage(e, featureUseCase)
118
+ when (featureUseCase) {
119
+ CodeWhispererConstants .FeatureName .CODE_REVIEW ->
120
+ codeScanServerException(" CreateUploadUrlException: $errorMessage " )
121
+ CodeWhispererConstants .FeatureName .TEST_GENERATION ->
122
+ throw CodeTestException (
123
+ " UploadTestArtifactToS3Error: $errorMessage " ,
124
+ " UploadTestArtifactToS3Error" ,
125
+ message(" testgen.error.generic_technical_error_message" )
126
+ )
127
+ else -> throw RuntimeException (" $errorMessage (after $attempts attempts)" )
100
128
}
101
- }.connect {
102
- val connection = it.connection as HttpURLConnection
103
- connection.setFixedLengthStreamingMode(fileToUpload.length())
104
- IoUtils .copy(fileToUpload.inputStream(), connection.outputStream)
105
- }
106
- } catch (e: Exception ) {
107
- LOG .debug { " $featureUseCase : Artifact failed to upload in the S3 bucket: ${e.message} " }
108
- val errorMessage = getTelemetryErrorMessage(e, featureUseCase)
109
- when (featureUseCase) {
110
- CodeWhispererConstants .FeatureName .CODE_REVIEW -> codeScanServerException(" CreateUploadUrlException: $errorMessage " )
111
- CodeWhispererConstants .FeatureName .TEST_GENERATION -> throw CodeTestException (
112
- " UploadTestArtifactToS3Error: $errorMessage " ,
113
- " UploadTestArtifactToS3Error" ,
114
- message(" testgen.error.generic_technical_error_message" )
115
- )
116
- else -> throw RuntimeException (errorMessage) // Adding else for safety check
117
129
}
118
- }
130
+ )
119
131
}
120
132
121
133
fun createUploadUrl (
@@ -124,35 +136,44 @@ class CodeWhispererZipUploadManager(private val project: Project) {
124
136
uploadTaskType : CodeWhispererConstants .UploadTaskType ,
125
137
taskName : String ,
126
138
featureUseCase : CodeWhispererConstants .FeatureName ,
127
- ): CreateUploadUrlResponse = try {
128
- CodeWhispererClientAdaptor .getInstance(project).createUploadUrl(
129
- CreateUploadUrlRequest .builder()
130
- .contentMd5(md5Content)
131
- .artifactType(artifactType)
132
- .uploadIntent(getUploadIntent(uploadTaskType))
133
- .uploadContext(
134
- // For UTG we don't need uploadContext but sending else case as UploadContext
135
- if (uploadTaskType == CodeWhispererConstants .UploadTaskType .CODE_FIX ) {
136
- UploadContext .fromCodeFixUploadContext(CodeFixUploadContext .builder().codeFixName(taskName).build())
137
- } else {
138
- UploadContext .fromCodeAnalysisUploadContext(CodeAnalysisUploadContext .builder().codeScanName(taskName).build())
139
- }
140
- )
141
- .build()
142
- )
143
- } catch (e: Exception ) {
144
- LOG .debug { " $featureUseCase : Create Upload URL failed: ${e.message} " }
145
- val errorMessage = getTelemetryErrorMessage(e, featureUseCase)
146
- when (featureUseCase) {
147
- CodeWhispererConstants .FeatureName .CODE_REVIEW -> codeScanServerException(" CreateUploadUrlException: $errorMessage " )
148
- CodeWhispererConstants .FeatureName .TEST_GENERATION -> throw CodeTestException (
149
- " CreateUploadUrlError: $errorMessage " ,
150
- " CreateUploadUrlError" ,
151
- message(" testgen.error.generic_technical_error_message" )
139
+ ): CreateUploadUrlResponse = RetryableOperation <CreateUploadUrlResponse >().execute(
140
+ operation = {
141
+ CodeWhispererClientAdaptor .getInstance(project).createUploadUrl(
142
+ CreateUploadUrlRequest .builder()
143
+ .contentMd5(md5Content)
144
+ .artifactType(artifactType)
145
+ .uploadIntent(getUploadIntent(uploadTaskType))
146
+ .uploadContext(
147
+ // For UTG we don't need uploadContext but sending else case as UploadContext
148
+ if (uploadTaskType == CodeWhispererConstants .UploadTaskType .CODE_FIX ) {
149
+ UploadContext .fromCodeFixUploadContext(CodeFixUploadContext .builder().codeFixName(taskName).build())
150
+ } else {
151
+ UploadContext .fromCodeAnalysisUploadContext(CodeAnalysisUploadContext .builder().codeScanName(taskName).build())
152
+ }
153
+ )
154
+ .build()
152
155
)
153
- else -> throw RuntimeException (errorMessage) // Adding else for safety check
156
+ },
157
+ isRetryable = { e ->
158
+ e is ThrottlingException || e is InternalServerException
159
+ },
160
+ errorHandler = { e, attempts ->
161
+ val errorMessage = getTelemetryErrorMessage(e, featureUseCase)
162
+ when (featureUseCase) {
163
+ CodeWhispererConstants .FeatureName .CODE_REVIEW ->
164
+ codeScanServerException(" CreateUploadUrlException after $attempts attempts: $errorMessage " )
165
+
166
+ CodeWhispererConstants .FeatureName .TEST_GENERATION ->
167
+ throw CodeTestException (
168
+ " CreateUploadUrlError after $attempts attempts: $errorMessage " ,
169
+ " CreateUploadUrlError" ,
170
+ message(" testgen.error.generic_technical_error_message" )
171
+ )
172
+
173
+ else -> throw RuntimeException (" $errorMessage (after $attempts attempts)" )
174
+ }
154
175
}
155
- }
176
+ )
156
177
157
178
private fun getUploadIntent (uploadTaskType : CodeWhispererConstants .UploadTaskType ): UploadIntent = when (uploadTaskType) {
158
179
CodeWhispererConstants .UploadTaskType .SCAN_FILE -> UploadIntent .AUTOMATIC_FILE_SECURITY_SCAN
@@ -187,3 +208,41 @@ fun getTelemetryErrorMessage(e: Exception, featureUseCase: CodeWhispererConstant
187
208
else -> message(" testgen.message.failed" )
188
209
}
189
210
}
211
+
212
+ class RetryableOperation <T > {
213
+ private var attempts = 0
214
+ private var currentDelay = INITIAL_DELAY
215
+ private var lastException: Exception ? = null
216
+
217
+ fun execute (
218
+ operation : () -> T ,
219
+ isRetryable : (Exception ) -> Boolean ,
220
+ errorHandler : (Exception , Int ) -> Nothing ,
221
+ ): T {
222
+ while (attempts < MAX_RETRY_ATTEMPTS ) {
223
+ try {
224
+ return operation()
225
+ } catch (e: Exception ) {
226
+ lastException = e
227
+
228
+ attempts++
229
+ if (attempts < MAX_RETRY_ATTEMPTS && isRetryable(e)) {
230
+ Thread .sleep(currentDelay)
231
+ currentDelay = (currentDelay * 2 ).coerceAtMost(MAX_BACKOFF )
232
+ continue
233
+ }
234
+
235
+ errorHandler(e, attempts)
236
+ }
237
+ }
238
+
239
+ // This line should never be reached due to errorHandler throwing exception
240
+ throw RuntimeException (" Unexpected state after $attempts attempts" )
241
+ }
242
+
243
+ companion object {
244
+ private const val INITIAL_DELAY = 100L // milliseconds
245
+ private const val MAX_BACKOFF = 10000L // milliseconds
246
+ private const val MAX_RETRY_ATTEMPTS = 3
247
+ }
248
+ }
0 commit comments