-
Notifications
You must be signed in to change notification settings - Fork 274
fix(amazonq): Adding backoff and retry for export result archive streaming API. #5320
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
Changes from 3 commits
da9928f
74ec75d
93cd2a7
1a6ca91
f4196bc
ab927ba
44b32f3
ac55d73
1f6a2b0
c7f5ce7
7aee910
b54991d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,18 +6,26 @@ package software.aws.toolkits.jetbrains.services.amazonq.clients | |
| import com.intellij.openapi.components.Service | ||
| import com.intellij.openapi.components.service | ||
| import com.intellij.openapi.project.Project | ||
| import kotlinx.coroutines.delay | ||
| import kotlinx.coroutines.future.await | ||
| import software.amazon.awssdk.core.exception.RetryableException | ||
| import software.amazon.awssdk.core.exception.SdkException | ||
| import software.amazon.awssdk.services.codewhispererstreaming.CodeWhispererStreamingAsyncClient | ||
| import software.amazon.awssdk.services.codewhispererstreaming.model.ExportContext | ||
| import software.amazon.awssdk.services.codewhispererstreaming.model.ExportIntent | ||
| import software.amazon.awssdk.services.codewhispererstreaming.model.ExportResultArchiveResponseHandler | ||
| import software.amazon.awssdk.services.codewhispererstreaming.model.ThrottlingException | ||
| import software.amazon.awssdk.services.codewhispererstreaming.model.ValidationException | ||
| import software.aws.toolkits.core.utils.getLogger | ||
| import software.aws.toolkits.core.utils.warn | ||
| import software.aws.toolkits.jetbrains.core.awsClient | ||
| import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager | ||
| import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection | ||
| import java.time.Instant | ||
| import java.util.concurrent.TimeoutException | ||
| import java.util.concurrent.atomic.AtomicReference | ||
| import javax.naming.ServiceUnavailableException | ||
| import kotlin.random.Random | ||
|
|
||
| @Service(Service.Level.PROJECT) | ||
| class AmazonQStreamingClient(private val project: Project) { | ||
|
|
@@ -54,27 +62,42 @@ class AmazonQStreamingClient(private val project: Project) { | |
| val checksum = AtomicReference("") | ||
|
|
||
| try { | ||
| val result = streamingBearerClient().exportResultArchive( | ||
| { | ||
| it.exportId(exportId) | ||
| it.exportIntent(exportIntent) | ||
| it.exportContext(exportContext) | ||
| withRetry( | ||
| block = { | ||
| val result = streamingBearerClient().exportResultArchive( | ||
| { | ||
| it.exportId(exportId) | ||
| it.exportIntent(exportIntent) | ||
| it.exportContext(exportContext) | ||
| }, | ||
| ExportResultArchiveResponseHandler.builder().subscriber( | ||
| ExportResultArchiveResponseHandler.Visitor.builder() | ||
| .onBinaryMetadataEvent { | ||
| checksum.set(it.contentChecksum()) | ||
| }.onBinaryPayloadEvent { | ||
| val payloadBytes = it.bytes().asByteArray() | ||
| byteBufferList.add(payloadBytes) | ||
| }.onDefault { | ||
| LOG.warn { "Received unknown payload stream: $it" } | ||
| } | ||
| .build() | ||
| ) | ||
| .build() | ||
| ) | ||
| result.await() | ||
| }, | ||
| ExportResultArchiveResponseHandler.builder().subscriber( | ||
| ExportResultArchiveResponseHandler.Visitor.builder() | ||
| .onBinaryMetadataEvent { | ||
| checksum.set(it.contentChecksum()) | ||
| }.onBinaryPayloadEvent { | ||
| val payloadBytes = it.bytes().asByteArray() | ||
| byteBufferList.add(payloadBytes) | ||
| }.onDefault { | ||
| LOG.warn { "Received unknown payload stream: $it" } | ||
| } | ||
| .build() | ||
| ) | ||
| .build() | ||
| isRetryable = { e -> | ||
| when (e) { | ||
| is ValidationException, | ||
| is ThrottlingException, | ||
| is ServiceUnavailableException, | ||
| is SdkException, | ||
| is TimeoutException, | ||
| -> true | ||
| else -> false | ||
| } | ||
| } | ||
| ) | ||
| result.await() | ||
| } catch (e: Exception) { | ||
| onError(e) | ||
| throw e | ||
|
|
@@ -85,8 +108,43 @@ class AmazonQStreamingClient(private val project: Project) { | |
| return byteBufferList | ||
| } | ||
|
|
||
| /** | ||
|
||
| * Helper function to implement retry logic with exponential backoff and jitter | ||
| * | ||
| * @param block The suspend function to execute with retry logic | ||
| * @param isRetryable A function that determines if an exception should trigger a retry | ||
| * @return The result of the block execution | ||
| */ | ||
| private suspend fun <T> withRetry( | ||
| block: suspend () -> T, | ||
| isRetryable: (Exception) -> Boolean = { it is RetryableException }, | ||
| ): T { | ||
| var currentDelay = INITIAL_DELAY | ||
| var attempt = 0 | ||
|
|
||
| while (true) { | ||
| try { | ||
| return block() | ||
| } catch (e: Exception) { | ||
| attempt++ | ||
| if (attempt >= MAX_RETRY_ATTEMPTS || !isRetryable(e)) { | ||
| throw e | ||
| } | ||
|
|
||
| // Calculate delay with exponential backoff and jitter | ||
| currentDelay = (currentDelay * 2).coerceAtMost(MAX_BACKOFF) | ||
| val jitteredDelay = currentDelay * (0.5 + Random.nextDouble(0.5)) | ||
|
|
||
| delay(jitteredDelay.toLong()) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| companion object { | ||
| private val LOG = getLogger<AmazonQStreamingClient>() | ||
| private const val INITIAL_DELAY = 100L // milliseconds | ||
| private const val MAX_BACKOFF = 10000L // milliseconds | ||
| private const val MAX_RETRY_ATTEMPTS = 3 | ||
|
|
||
| fun getInstance(project: Project) = project.service<AmazonQStreamingClient>() | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.