@@ -24,12 +24,16 @@ import com.intellij.util.net.HttpConfigurable
2424import com.intellij.util.net.JdkProxyProvider
2525import kotlinx.coroutines.CoroutineScope
2626import kotlinx.coroutines.Deferred
27+ import kotlinx.coroutines.Job
2728import kotlinx.coroutines.async
2829import kotlinx.coroutines.channels.BufferOverflow
30+ import kotlinx.coroutines.delay
2931import kotlinx.coroutines.flow.MutableSharedFlow
3032import kotlinx.coroutines.flow.asSharedFlow
3133import kotlinx.coroutines.flow.map
3234import kotlinx.coroutines.future.asCompletableFuture
35+ import kotlinx.coroutines.isActive
36+ import kotlinx.coroutines.launch
3337import kotlinx.coroutines.runBlocking
3438import kotlinx.coroutines.sync.Mutex
3539import kotlinx.coroutines.sync.withLock
@@ -52,6 +56,7 @@ import org.eclipse.lsp4j.jsonrpc.json.JsonRpcMethod
5256import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage
5357import org.eclipse.lsp4j.launch.LSPLauncher
5458import org.slf4j.event.Level
59+ import software.aws.toolkits.core.utils.debug
5560import software.aws.toolkits.core.utils.getLogger
5661import software.aws.toolkits.core.utils.info
5762import 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