Skip to content

Commit e52d969

Browse files
authored
feat(amazonq): Add restart mechanism when the flare server process is not running (#5711)
* feat(amazonq): Add restart mechanism when the flare server process is not running * address comments * address comments * remove debugging options * fix potential deadlock code * fix unlock error * address comments * remove debug lines
1 parent b57c9d6 commit e52d969

File tree

1 file changed

+71
-1
lines changed
  • plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp

1 file changed

+71
-1
lines changed

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@ import com.intellij.util.net.HttpConfigurable
2424
import com.intellij.util.net.JdkProxyProvider
2525
import kotlinx.coroutines.CoroutineScope
2626
import kotlinx.coroutines.Deferred
27+
import kotlinx.coroutines.Job
2728
import kotlinx.coroutines.async
2829
import kotlinx.coroutines.channels.BufferOverflow
30+
import kotlinx.coroutines.delay
2931
import kotlinx.coroutines.flow.MutableSharedFlow
3032
import kotlinx.coroutines.flow.asSharedFlow
3133
import kotlinx.coroutines.flow.map
3234
import kotlinx.coroutines.future.asCompletableFuture
35+
import kotlinx.coroutines.isActive
36+
import kotlinx.coroutines.launch
3337
import kotlinx.coroutines.runBlocking
3438
import kotlinx.coroutines.sync.Mutex
3539
import kotlinx.coroutines.sync.withLock
@@ -52,6 +56,7 @@ import org.eclipse.lsp4j.jsonrpc.json.JsonRpcMethod
5256
import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage
5357
import org.eclipse.lsp4j.launch.LSPLauncher
5458
import org.slf4j.event.Level
59+
import software.aws.toolkits.core.utils.debug
5560
import software.aws.toolkits.core.utils.getLogger
5661
import software.aws.toolkits.core.utils.info
5762
import software.aws.toolkits.core.utils.warn
@@ -128,6 +133,9 @@ class AmazonQLspService(private val project: Project, private val cs: CoroutineS
128133

129134
val encryptionManager
130135
get() = instance.getCompleted().encryptionManager
136+
private val heartbeatJob: Job
137+
private val restartTimestamps = ArrayDeque<Long>()
138+
private val restartMutex = Mutex() // Separate mutex for restart tracking
131139

132140
val rawEndpoint
133141
get() = instance.getCompleted().rawEndpoint
@@ -166,9 +174,50 @@ class AmazonQLspService(private val project: Project, private val cs: CoroutineS
166174

167175
init {
168176
instance = start()
177+
178+
// Initialize heartbeat job
179+
heartbeatJob = cs.launch {
180+
while (isActive) {
181+
delay(5.seconds) // Check every 5 seconds
182+
val shouldLoop = checkConnectionStatus()
183+
if (!shouldLoop) {
184+
break
185+
}
186+
}
187+
}
188+
}
189+
190+
private suspend fun checkConnectionStatus(): Boolean {
191+
try {
192+
val currentInstance = mutex.withLock { instance }.await()
193+
194+
// Check if the launcher's Future (startListening) is done
195+
// If it's done, that means the connection has been terminated
196+
if (currentInstance.launcherFuture.isDone) {
197+
LOG.debug { "LSP server connection terminated, checking restart limits" }
198+
val canRestart = checkForRemainingRestartAttempts()
199+
if (!canRestart) {
200+
return false
201+
}
202+
LOG.debug { "Restarting LSP server" }
203+
restart()
204+
} else {
205+
LOG.debug { "LSP server is currently running" }
206+
}
207+
} catch (e: Exception) {
208+
LOG.debug(e) { "Connection status check failed, checking restart limits" }
209+
val canRestart = checkForRemainingRestartAttempts()
210+
if (!canRestart) {
211+
return false
212+
}
213+
LOG.debug { "Restarting LSP server" }
214+
restart()
215+
}
216+
return true
169217
}
170218

171219
override fun dispose() {
220+
heartbeatJob.cancel()
172221
}
173222

174223
suspend fun restart() = mutex.withLock {
@@ -195,6 +244,25 @@ class AmazonQLspService(private val project: Project, private val cs: CoroutineS
195244
instance = start()
196245
}
197246

247+
private suspend fun checkForRemainingRestartAttempts(): Boolean = restartMutex.withLock {
248+
val currentTime = System.currentTimeMillis()
249+
250+
while (restartTimestamps.isNotEmpty() &&
251+
currentTime - restartTimestamps.first() > RESTART_WINDOW_MS
252+
) {
253+
restartTimestamps.removeFirst()
254+
}
255+
256+
if (restartTimestamps.size < MAX_RESTARTS) {
257+
restartTimestamps.addLast(currentTime)
258+
return true
259+
}
260+
261+
LOG.info { "Rate limit reached for LSP server restarts. Stop attempting to restart." }
262+
263+
return false
264+
}
265+
198266
suspend fun<T> execute(runnable: suspend AmazonQLspService.(AmazonQLanguageServer) -> T): T {
199267
val lsp = withTimeout(10.seconds) {
200268
val holder = mutex.withLock { instance }.await()
@@ -212,6 +280,8 @@ class AmazonQLspService(private val project: Project, private val cs: CoroutineS
212280

213281
companion object {
214282
private val LOG = getLogger<AmazonQLspService>()
283+
private const val MAX_RESTARTS = 5
284+
private const val RESTART_WINDOW_MS = 3 * 60 * 1000
215285
fun getInstance(project: Project) = project.service<AmazonQLspService>()
216286

217287
@Deprecated("Easy to accidentally freeze EDT")
@@ -241,7 +311,7 @@ private class AmazonQServerInstance(private val project: Project, private val cs
241311
get() = launcher.remoteEndpoint
242312

243313
@Suppress("ForbiddenVoid")
244-
private val launcherFuture: Future<Void>
314+
val launcherFuture: Future<Void>
245315
private val launcherHandler: KillableProcessHandler
246316
val initializeResult: Deferred<InitializeResult>
247317

0 commit comments

Comments
 (0)