@@ -16,10 +16,16 @@ import com.intellij.openapi.vfs.VirtualFile
1616import com.intellij.openapi.wm.WindowManager
1717import com.intellij.ui.ComponentUtil
1818import kotlinx.coroutines.CoroutineScope
19+ import kotlinx.coroutines.Dispatchers
1920import kotlinx.coroutines.Job
2021import kotlinx.coroutines.delay
2122import kotlinx.coroutines.launch
2223import kotlinx.coroutines.yield
24+ import kotlinx.coroutines.delay
25+ import kotlinx.coroutines.runBlocking
26+ import kotlinx.coroutines.withContext
27+ import kotlin.math.min
28+ import software.amazon.awssdk.core.exception.SdkClientException
2329import software.amazon.awssdk.services.codewhispererruntime.model.Completion
2430import software.amazon.awssdk.services.codewhispererruntime.model.OptOutPreference
2531import software.aws.toolkits.core.utils.getLogger
@@ -47,6 +53,7 @@ import software.aws.toolkits.jetbrains.utils.isQExpired
4753import software.aws.toolkits.jetbrains.utils.notifyError
4854import software.aws.toolkits.jetbrains.utils.notifyInfo
4955import software.aws.toolkits.jetbrains.utils.pluginAwareExecuteOnPooledThread
56+ import software.aws.toolkits.jetbrains.utils.runUnderProgressIfNeeded
5057import software.aws.toolkits.resources.message
5158import software.aws.toolkits.telemetry.CodewhispererCompletionType
5259import software.aws.toolkits.telemetry.CodewhispererGettingStartedTask
@@ -137,6 +144,9 @@ fun VirtualFile.toCodeChunk(path: String): Sequence<Chunk> = sequence {
137144}
138145
139146object CodeWhispererUtil {
147+ private const val MAX_RETRY_DELAY_MS = 300000 // 5 minutes in milliseconds
148+ private const val INITIAL_RETRY_DELAY_MS = 1000 // 1 second
149+
140150 fun getCompletionType (completion : Completion ): CodewhispererCompletionType {
141151 val content = completion.content()
142152 val nonBlankLines = content.split(" \n " ).count { it.isNotBlank() }
@@ -161,32 +171,60 @@ object CodeWhispererUtil {
161171 }
162172
163173 // This will be called only when there's a CW connection, but it has expired(either accessToken or refreshToken)
164- // 1. If connection is expired, try to refresh
165- // 2. If not able to refresh, requesting re-login by showing a notification
166- // 3. The notification will be shown
167- // 3.1 At most once per IDE restarts.
168- // 3.2 At most once after IDE restarts,
169- // for example, when user performs security scan or fetch code completion for the first time
170- // Return true if need to re-auth, false otherwise
174+ // 1. Attempt to refresh the connection
175+ // 2. If refresh fails due to network issues (SdkClientException), it will:
176+ // 2.1 Retry the refresh with exponential backoff
177+ // 2.2 Continue retrying indefinitely until successful or a non-network error occurs
178+ // 3. If refresh is successful at any point, it will:
179+ // 3.1 Show a re-authentication prompt if it hasn't been shown yet in this IDE session
180+ // 3.2 Mark the re-auth prompt as shown (if not during plugin startup)
181+ // 3.3 Notify about session configuration if not using Sono
182+ // 4. If a non-network error occurs (not SdkClientException), it will stop retrying and return true
183+ // - Returns false if re-authentication was successful (no further action needed)
184+ // - Returns true if re-authentication failed and manual re-auth is required
171185 fun promptReAuth (project : Project , isPluginStarting : Boolean = false): Boolean {
186+ // Check if re-authentication is needed
172187 if (! isQExpired(project)) return false
173188 val tokenProvider = tokenProvider(project) ? : return false
189+
174190 return try {
175- maybeReauthProviderIfNeeded(project, ReauthSource .CODEWHISPERER , tokenProvider) {
176- runInEdt {
177- if (! CodeWhispererService .hasReAuthPromptBeenShown()) {
178- notifyConnectionExpiredRequestReauth(project)
179- }
180- if (! isPluginStarting) {
181- CodeWhispererService .markReAuthPromptShown()
182- }
183- if (! tokenConnection(project).isSono()) {
184- notifySessionConfiguration(project)
191+ runUnderProgressIfNeeded(project, " Refreshing Connection" , true ) {
192+ var currentDelay = INITIAL_RETRY_DELAY_MS .toLong()
193+ var attempt = 1
194+
195+ while (true ) {
196+ try {
197+ // Attempt to re-authenticate
198+ val result = maybeReauthProviderIfNeeded(project, ReauthSource .CODEWHISPERER , tokenProvider) {
199+ runInEdt {
200+ // Show re-auth prompt if it hasn't been shown yet
201+ if (! CodeWhispererService .hasReAuthPromptBeenShown()) {
202+ notifyConnectionExpiredRequestReauth(project)
203+ }
204+ // Mark re-auth prompt as shown if not during plugin startup
205+ if (! isPluginStarting) {
206+ CodeWhispererService .markReAuthPromptShown()
207+ }
208+ // Notify about session configuration if not using Sono
209+ if (! tokenConnection(project).isSono()) {
210+ notifySessionConfiguration(project)
211+ }
212+ }
213+ }
214+ return @runUnderProgressIfNeeded ! result // Assuming maybeReauthProviderIfNeeded returns true if reauth is needed
215+ } catch (e: SdkClientException ) {
216+ getLogger<CodeWhispererService >().warn(e) { " Attempt $attempt failed. Retrying in $currentDelay ms" }
217+ Thread .sleep(currentDelay)
218+ currentDelay = minOf(MAX_RETRY_DELAY_MS .toLong(), (currentDelay * 2 ).toLong())
219+ attempt++
185220 }
186221 }
222+ // This line should never be reached due to the infinite loop, but it's needed to satisfy the compiler
223+ false
187224 }
188225 } catch (e: Exception ) {
189- getLogger<CodeWhispererService >().warn(e) { " prompt reauth failed with unexpected error" }
226+ // Log any unexpected errors and return true to indicate re-auth is needed
227+ getLogger<CodeWhispererService >().warn(e) { " Prompt reauth failed with unexpected error" }
190228 true
191229 }
192230 }
0 commit comments