Skip to content

Commit e39fcc5

Browse files
committed
Creating common util class for retry and backoff
1 parent 000b0e6 commit e39fcc5

File tree

1 file changed

+83
-90
lines changed

1 file changed

+83
-90
lines changed

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererZipUploadManager.kt

Lines changed: 83 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,6 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.codetest.invalidSo
4444
@Service
4545
class CodeWhispererZipUploadManager(private val project: Project) {
4646

47-
private val maxRetryAttempts = 3
48-
private val initialDelay = 200L // 200ms
49-
private val maxBackoff = 1000L // 1 second
50-
5147
fun createUploadUrlAndUpload(
5248
zipFile: File,
5349
artifactType: String,
@@ -90,11 +86,8 @@ class CodeWhispererZipUploadManager(private val project: Project) {
9086
requestHeaders: Map<String, String>?,
9187
featureUseCase: CodeWhispererConstants.FeatureName,
9288
) {
93-
var attempts = 0
94-
var currentDelay = initialDelay
95-
96-
while (attempts < maxRetryAttempts) {
97-
try {
89+
RetryableOperation<Unit>().execute(
90+
operation = {
9891
val uploadIdJson = """{"uploadId":"$uploadId"}"""
9992
HttpRequests.put(url, "application/zip").userAgent(AwsClientManager.getUserAgent()).tuner {
10093
if (requestHeaders.isNullOrEmpty()) {
@@ -115,26 +108,17 @@ class CodeWhispererZipUploadManager(private val project: Project) {
115108
connection.setFixedLengthStreamingMode(fileToUpload.length())
116109
IoUtils.copy(fileToUpload.inputStream(), connection.outputStream)
117110
}
118-
return // Success, exit the function
119-
} catch (e: Exception) {
120-
LOG.debug { "$featureUseCase: Artifact failed to upload in the S3 bucket (attempt ${attempts + 1}): ${e.message}" }
121-
122-
// Determine if the exception is retryable
123-
val isRetryable = when (e) {
124-
is IOException, // Network issues
111+
},
112+
isRetryable = { e ->
113+
when (e) {
114+
is IOException,
125115
is SocketException,
126116
is SocketTimeoutException,
127117
-> true
128118
else -> false
129119
}
130-
attempts++
131-
if (attempts < maxRetryAttempts && isRetryable) {
132-
Thread.sleep(currentDelay)
133-
currentDelay = (currentDelay * 2).coerceAtMost(maxBackoff)
134-
continue
135-
}
136-
137-
// If not retryable or max attempts reached, handle the error
120+
},
121+
errorHandler = { e, attempts ->
138122
val errorMessage = getTelemetryErrorMessage(e, featureUseCase)
139123
when (featureUseCase) {
140124
CodeWhispererConstants.FeatureName.CODE_REVIEW ->
@@ -145,85 +129,56 @@ class CodeWhispererZipUploadManager(private val project: Project) {
145129
"UploadTestArtifactToS3Error",
146130
message("testgen.error.generic_technical_error_message")
147131
)
148-
else -> throw RuntimeException("$errorMessage (after $attempts attempts)") // Adding else for safety check
132+
else -> throw RuntimeException("$errorMessage (after $attempts attempts)")
149133
}
150134
}
151-
}
135+
)
152136
}
137+
153138
fun createUploadUrl(
154139
md5Content: String,
155140
artifactType: String,
156141
uploadTaskType: CodeWhispererConstants.UploadTaskType,
157142
taskName: String,
158143
featureUseCase: CodeWhispererConstants.FeatureName,
159-
): CreateUploadUrlResponse {
160-
var attempts = 0
161-
var currentDelay = initialDelay
162-
var lastException: Exception? = null
163-
164-
while (attempts < maxRetryAttempts) {
165-
try {
166-
return CodeWhispererClientAdaptor.getInstance(project).createUploadUrl(
167-
CreateUploadUrlRequest.builder()
168-
.contentMd5(md5Content)
169-
.artifactType(artifactType)
170-
.uploadIntent(getUploadIntent(uploadTaskType))
171-
.uploadContext(
172-
// For UTG we don't need uploadContext but sending else case as UploadContext
173-
if (uploadTaskType == CodeWhispererConstants.UploadTaskType.CODE_FIX) {
174-
UploadContext.fromCodeFixUploadContext(CodeFixUploadContext.builder().codeFixName(taskName).build())
175-
} else {
176-
UploadContext.fromCodeAnalysisUploadContext(CodeAnalysisUploadContext.builder().codeScanName(taskName).build())
177-
}
178-
)
179-
.build()
180-
)
181-
} catch (e: Exception) {
182-
lastException = e
183-
LOG.debug { "$featureUseCase: Create Upload URL failed (attempt ${attempts + 1}): ${e.message}" }
144+
): CreateUploadUrlResponse = RetryableOperation<CreateUploadUrlResponse>().execute(
145+
operation = {
146+
CodeWhispererClientAdaptor.getInstance(project).createUploadUrl(
147+
CreateUploadUrlRequest.builder()
148+
.contentMd5(md5Content)
149+
.artifactType(artifactType)
150+
.uploadIntent(getUploadIntent(uploadTaskType))
151+
.uploadContext(
152+
// For UTG we don't need uploadContext but sending else case as UploadContext
153+
if (uploadTaskType == CodeWhispererConstants.UploadTaskType.CODE_FIX) {
154+
UploadContext.fromCodeFixUploadContext(CodeFixUploadContext.builder().codeFixName(taskName).build())
155+
} else {
156+
UploadContext.fromCodeAnalysisUploadContext(CodeAnalysisUploadContext.builder().codeScanName(taskName).build())
157+
}
158+
)
159+
.build()
160+
)
161+
},
162+
isRetryable = { e ->
163+
e is ThrottlingException || e is InternalServerException
164+
},
165+
errorHandler = { e, attempts ->
166+
val errorMessage = getTelemetryErrorMessage(e, featureUseCase)
167+
when (featureUseCase) {
168+
CodeWhispererConstants.FeatureName.CODE_REVIEW ->
169+
codeScanServerException("CreateUploadUrlException after $attempts attempts: $errorMessage")
184170

185-
// Don't retry these exceptions
186-
if (e !is ThrottlingException && e !is InternalServerException) {
187-
val errorMessage = getTelemetryErrorMessage(e, featureUseCase)
188-
when (featureUseCase) {
189-
CodeWhispererConstants.FeatureName.CODE_REVIEW ->
190-
codeScanServerException("CreateUploadUrlException: $errorMessage")
191-
CodeWhispererConstants.FeatureName.TEST_GENERATION ->
192-
throw CodeTestException(
193-
"CreateUploadUrlError: $errorMessage",
194-
"CreateUploadUrlError",
195-
message("testgen.error.generic_technical_error_message")
196-
)
197-
else -> throw RuntimeException(errorMessage)
198-
}
199-
}
171+
CodeWhispererConstants.FeatureName.TEST_GENERATION ->
172+
throw CodeTestException(
173+
"CreateUploadUrlError after $attempts attempts: $errorMessage",
174+
"CreateUploadUrlError",
175+
message("testgen.error.generic_technical_error_message")
176+
)
200177

201-
attempts++
202-
if (attempts < maxRetryAttempts) {
203-
Thread.sleep(currentDelay)
204-
currentDelay = (currentDelay * 2).coerceAtMost(maxBackoff)
205-
continue
206-
}
178+
else -> throw RuntimeException("$errorMessage (after $attempts attempts)")
207179
}
208180
}
209-
210-
// If we've exhausted all retries, handle the last exception
211-
val errorMessage = getTelemetryErrorMessage(
212-
lastException ?: Exception("Unknown error"),
213-
featureUseCase
214-
)
215-
when (featureUseCase) {
216-
CodeWhispererConstants.FeatureName.CODE_REVIEW ->
217-
codeScanServerException("CreateUploadUrlException after $maxRetryAttempts attempts: $errorMessage")
218-
CodeWhispererConstants.FeatureName.TEST_GENERATION ->
219-
throw CodeTestException(
220-
"CreateUploadUrlError after $maxRetryAttempts attempts: $errorMessage",
221-
"CreateUploadUrlError",
222-
message("testgen.error.generic_technical_error_message")
223-
)
224-
else -> throw RuntimeException("$errorMessage (after $maxRetryAttempts attempts)") // Adding else for safety check
225-
}
226-
}
181+
)
227182

228183
private fun getUploadIntent(uploadTaskType: CodeWhispererConstants.UploadTaskType): UploadIntent = when (uploadTaskType) {
229184
CodeWhispererConstants.UploadTaskType.SCAN_FILE -> UploadIntent.AUTOMATIC_FILE_SECURITY_SCAN
@@ -258,3 +213,41 @@ fun getTelemetryErrorMessage(e: Exception, featureUseCase: CodeWhispererConstant
258213
else -> message("testgen.message.failed")
259214
}
260215
}
216+
217+
class RetryableOperation<T> {
218+
private var attempts = 0
219+
private var currentDelay = initialDelay
220+
private var lastException: Exception? = null
221+
222+
fun execute(
223+
operation: () -> T,
224+
isRetryable: (Exception) -> Boolean,
225+
errorHandler: (Exception, Int) -> Nothing,
226+
): T {
227+
while (attempts < maxRetryAttempts) {
228+
try {
229+
return operation()
230+
} catch (e: Exception) {
231+
lastException = e
232+
233+
attempts++
234+
if (attempts < maxRetryAttempts && isRetryable(e)) {
235+
Thread.sleep(currentDelay)
236+
currentDelay = (currentDelay * 2).coerceAtMost(maxBackoff)
237+
continue
238+
}
239+
240+
errorHandler(e, attempts)
241+
}
242+
}
243+
244+
// This line should never be reached due to errorHandler throwing exception
245+
throw RuntimeException("Unexpected state after $attempts attempts")
246+
}
247+
248+
companion object {
249+
private const val initialDelay = 100L // milliseconds
250+
private const val maxBackoff = 10000L // milliseconds
251+
private const val maxRetryAttempts = 3
252+
}
253+
}

0 commit comments

Comments
 (0)