@@ -47,7 +47,6 @@ import io.gitpod.jetbrains.gateway.common.GitpodConnectionHandleFactory
4747import io.gitpod.jetbrains.icons.GitpodIcons
4848import kotlinx.coroutines.*
4949import kotlinx.coroutines.future.await
50- import java.awt.Component
5150import java.net.URL
5251import java.net.http.HttpClient
5352import java.net.http.HttpRequest
@@ -58,6 +57,7 @@ import javax.swing.JLabel
5857import kotlin.coroutines.coroutineContext
5958import kotlin.io.path.absolutePathString
6059import kotlin.io.path.writeText
60+ import kotlin.random.Random.Default.nextInt
6161
6262@Suppress(" UnstableApiUsage" , " OPT_IN_USAGE" )
6363class GitpodConnectionProvider : GatewayConnectionProvider {
@@ -202,6 +202,39 @@ class GitpodConnectionProvider : GatewayConnectionProvider {
202202
203203 var lastUpdate: WorkspaceInstance ? = null
204204 var canceledByGitpod = false
205+
206+ val ownerToken = client.server.getOwnerToken(connectParams.actualWorkspaceId).await()
207+
208+ if (settings.additionalHeartbeat) {
209+ thisLogger().info(" gitpod: additional heartbeat enabled for ${connectParams.resolvedWorkspaceId} " )
210+ connectionLifetime.launch {
211+ while (isActive) {
212+ val delaySeconds = 30 + nextInt(5 , 15 )
213+ if (thinClientJob?.isActive == true ) {
214+ try {
215+ val ideUrlStr = lastUpdate?.ideUrl
216+ val ideUrl = if (ideUrlStr.isNullOrBlank()) {
217+ null
218+ } else {
219+ URL (ideUrlStr.replace(connectParams.actualWorkspaceId, connectParams.resolvedWorkspaceId))
220+ }
221+ if (lastUpdate?.status?.phase == " running" && ideUrl != null ) {
222+ sendHeartBeatThroughSupervisor(ideUrl, ownerToken, connectParams)
223+ }
224+ } catch (t: Throwable ) {
225+ thisLogger().error(
226+ " gitpod: failed to send additional heartbeat for ${connectParams.resolvedWorkspaceId} " ,
227+ t
228+ )
229+ }
230+ } else {
231+ thisLogger().debug(" gitpod: thinClient is not active, skipping additional heartbeat for ${connectParams.resolvedWorkspaceId} " )
232+ }
233+ delay(delaySeconds * 1000L )
234+ }
235+ }
236+ }
237+
205238 try {
206239 for (update in updates) {
207240 try {
@@ -518,7 +551,7 @@ class GitpodConnectionProvider : GatewayConnectionProvider {
518551 if (! connectParams.backendPort.isNullOrBlank()) {
519552 resolveJoinLinkUrl + = " ?backendPort=${connectParams.backendPort} "
520553 }
521- var rawResp = fetchWS (resolveJoinLinkUrl, connectParams, ownerToken)
554+ var rawResp = retryFetchWS (resolveJoinLinkUrl, connectParams, ownerToken)
522555 if (rawResp != null ) {
523556 return with (jacksonMapper) {
524557 propertyNamingStrategy = PropertyNamingStrategies .LowerCamelCaseStrategy ()
@@ -531,13 +564,34 @@ class GitpodConnectionProvider : GatewayConnectionProvider {
531564 if (! connectParams.backendPort.isNullOrBlank()) {
532565 resolveJoinLinkUrl + = " ?backendPort=${connectParams.backendPort} "
533566 }
534- rawResp = fetchWS (resolveJoinLinkUrl, connectParams, ownerToken)
567+ rawResp = retryFetchWS (resolveJoinLinkUrl, connectParams, ownerToken)
535568 if (rawResp != null ) {
536569 return JoinLinkResp (- 1 , rawResp)
537570 }
538571 return null
539572 }
540573
574+ private var sendHeartBeatThroughSupervisorLogOnce = false
575+ private suspend fun sendHeartBeatThroughSupervisor (
576+ ideUrl : URL ,
577+ ownerToken : String ,
578+ connectParams : ConnectParams
579+ ) {
580+ val resp = fetchWS(" https://${ideUrl.host} /_supervisor/v1/send_heartbeat" , ownerToken, 2000L )
581+ if (resp.statusCode != 200 ) {
582+ if (! resp.body.isNullOrBlank() && resp.body.contains(" not implemented" )) {
583+ if (! sendHeartBeatThroughSupervisorLogOnce) {
584+ thisLogger().warn(" gitpod: sendHeartbeat ${connectParams.actualWorkspaceId} failed: method is not implemented in supervisor" )
585+ sendHeartBeatThroughSupervisorLogOnce = true
586+ }
587+ return
588+ }
589+ thisLogger().error(" gitpod: sendHeartbeat ${connectParams.actualWorkspaceId} failed: ${resp.statusCode} , body: ${resp.body} " )
590+ return
591+ }
592+ thisLogger().debug(" gitpod: sendHeartbeat succeed for ${connectParams.actualWorkspaceId} " )
593+ }
594+
541595 private fun resolveCredentials (
542596 host : String ,
543597 port : Int ,
@@ -589,7 +643,7 @@ class GitpodConnectionProvider : GatewayConnectionProvider {
589643 ownerToken : String
590644 ): CreateSSHKeyPairResponse ? {
591645 val value =
592- fetchWS (" https://${ideUrl.host} /_supervisor/v1/ssh_keys/create" , connectParams, ownerToken)
646+ retryFetchWS (" https://${ideUrl.host} /_supervisor/v1/ssh_keys/create" , connectParams, ownerToken)
593647 if (value.isNullOrBlank()) {
594648 return null
595649 }
@@ -604,7 +658,7 @@ class GitpodConnectionProvider : GatewayConnectionProvider {
604658 connectParams : ConnectParams
605659 ): List <SSHHostKey >? {
606660 val hostKeysValue =
607- fetchWS (" https://${ideUrl.host} /_ssh/host_keys" , connectParams, null )
661+ retryFetchWS (" https://${ideUrl.host} /_ssh/host_keys" , connectParams, null )
608662 if (hostKeysValue.isNullOrBlank()) {
609663 return null
610664 }
@@ -671,27 +725,50 @@ class GitpodConnectionProvider : GatewayConnectionProvider {
671725 return acceptHostKey
672726 }
673727
728+ data class HttpResponseData (val statusCode : Int , val body : String? ) {
729+ fun statusCode () = statusCode
730+ fun body () = body
731+ }
732+
674733 private suspend fun fetchWS (
675734 endpointUrl : String ,
676- connectParams : ConnectParams ,
677735 ownerToken : String? ,
736+ timeoutMillis : Long ,
737+ ): HttpResponseData {
738+ var httpRequestBuilder = HttpRequest .newBuilder()
739+ .uri(URI .create(endpointUrl))
740+ .GET ()
741+ .timeout(Duration .ofMillis(timeoutMillis))
742+ if (! ownerToken.isNullOrBlank()) {
743+ httpRequestBuilder = httpRequestBuilder.header(" x-gitpod-owner-token" , ownerToken)
744+ }
745+ val httpRequest = httpRequestBuilder.build()
746+ val responseFuture =
747+ httpClient.sendAsync(httpRequest, HttpResponse .BodyHandlers .ofString())
748+
749+ try {
750+ val response = responseFuture.await()
751+ return HttpResponseData (response.statusCode(), response.body())
752+ } catch (e: Exception ) {
753+ if (responseFuture.isCancelled) {
754+ throw CancellationException ()
755+ }
756+ throw e
757+ }
758+ }
759+
760+ private suspend fun retryFetchWS (
761+ endpointUrl : String ,
762+ connectParams : ConnectParams ,
763+ ownerToken : String?
678764 ): String? {
679765 val maxRequestTimeout = 30 * 1000L
680766 val timeoutDelayGrowFactor = 1.5
681767 var requestTimeout = 2 * 1000L
682768 while (true ) {
683769 coroutineContext.job.ensureActive()
684770 try {
685- var httpRequestBuilder = HttpRequest .newBuilder()
686- .uri(URI .create(endpointUrl))
687- .GET ()
688- .timeout(Duration .ofMillis(requestTimeout))
689- if (! ownerToken.isNullOrBlank()) {
690- httpRequestBuilder = httpRequestBuilder.header(" x-gitpod-owner-token" , ownerToken)
691- }
692- val httpRequest = httpRequestBuilder.build()
693- val response =
694- httpClient.sendAsync(httpRequest, HttpResponse .BodyHandlers .ofString()).await()
771+ val response = fetchWS(endpointUrl, ownerToken, requestTimeout)
695772 if (response.statusCode() == 200 ) {
696773 return response.body()
697774 }
0 commit comments