@@ -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,35 @@ 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+ try {
214+ val ideUrlStr = lastUpdate?.ideUrl
215+ val ideUrl = if (ideUrlStr.isNullOrBlank()) {
216+ null
217+ } else {
218+ URL (ideUrlStr.replace(connectParams.actualWorkspaceId, connectParams.resolvedWorkspaceId))
219+ }
220+ if (lastUpdate?.status?.phase == " running" && ideUrl != null ) {
221+ sendHeartBeatThroughSupervisor(ideUrl, ownerToken, connectParams)
222+ }
223+ } catch (t: Throwable ) {
224+ thisLogger().error(
225+ " gitpod: failed to send additional heartbeat for ${connectParams.resolvedWorkspaceId} " ,
226+ t
227+ )
228+ }
229+ delay(delaySeconds * 1000L )
230+ }
231+ }
232+ }
233+
205234 try {
206235 for (update in updates) {
207236 try {
@@ -518,7 +547,7 @@ class GitpodConnectionProvider : GatewayConnectionProvider {
518547 if (! connectParams.backendPort.isNullOrBlank()) {
519548 resolveJoinLinkUrl + = " ?backendPort=${connectParams.backendPort} "
520549 }
521- var rawResp = fetchWS (resolveJoinLinkUrl, connectParams, ownerToken)
550+ var rawResp = retryFetchWS (resolveJoinLinkUrl, connectParams, ownerToken)
522551 if (rawResp != null ) {
523552 return with (jacksonMapper) {
524553 propertyNamingStrategy = PropertyNamingStrategies .LowerCamelCaseStrategy ()
@@ -531,13 +560,34 @@ class GitpodConnectionProvider : GatewayConnectionProvider {
531560 if (! connectParams.backendPort.isNullOrBlank()) {
532561 resolveJoinLinkUrl + = " ?backendPort=${connectParams.backendPort} "
533562 }
534- rawResp = fetchWS (resolveJoinLinkUrl, connectParams, ownerToken)
563+ rawResp = retryFetchWS (resolveJoinLinkUrl, connectParams, ownerToken)
535564 if (rawResp != null ) {
536565 return JoinLinkResp (- 1 , rawResp)
537566 }
538567 return null
539568 }
540569
570+ private var sendHeartBeatThroughSupervisorLogOnce = false
571+ private suspend fun sendHeartBeatThroughSupervisor (
572+ ideUrl : URL ,
573+ ownerToken : String ,
574+ connectParams : ConnectParams
575+ ) {
576+ val resp = fetchWS(" https://${ideUrl.host} /_supervisor/v1/send_heartbeat" , ownerToken, 2000L )
577+ if (resp.statusCode != 200 ) {
578+ if (! resp.body.isNullOrBlank() && resp.body.contains(" not implemented" )) {
579+ if (! sendHeartBeatThroughSupervisorLogOnce) {
580+ thisLogger().warn(" gitpod: sendHeartbeat ${connectParams.actualWorkspaceId} failed: method is not implemented in supervisor" )
581+ sendHeartBeatThroughSupervisorLogOnce = true
582+ }
583+ return
584+ }
585+ thisLogger().error(" gitpod: sendHeartbeat ${connectParams.actualWorkspaceId} failed: ${resp.statusCode} , body: ${resp.body} " )
586+ return
587+ }
588+ thisLogger().info(" gitpod: =========sendHeartbeat succeed" )
589+ }
590+
541591 private fun resolveCredentials (
542592 host : String ,
543593 port : Int ,
@@ -589,7 +639,7 @@ class GitpodConnectionProvider : GatewayConnectionProvider {
589639 ownerToken : String
590640 ): CreateSSHKeyPairResponse ? {
591641 val value =
592- fetchWS (" https://${ideUrl.host} /_supervisor/v1/ssh_keys/create" , connectParams, ownerToken)
642+ retryFetchWS (" https://${ideUrl.host} /_supervisor/v1/ssh_keys/create" , connectParams, ownerToken)
593643 if (value.isNullOrBlank()) {
594644 return null
595645 }
@@ -604,7 +654,7 @@ class GitpodConnectionProvider : GatewayConnectionProvider {
604654 connectParams : ConnectParams
605655 ): List <SSHHostKey >? {
606656 val hostKeysValue =
607- fetchWS (" https://${ideUrl.host} /_ssh/host_keys" , connectParams, null )
657+ retryFetchWS (" https://${ideUrl.host} /_ssh/host_keys" , connectParams, null )
608658 if (hostKeysValue.isNullOrBlank()) {
609659 return null
610660 }
@@ -671,27 +721,50 @@ class GitpodConnectionProvider : GatewayConnectionProvider {
671721 return acceptHostKey
672722 }
673723
724+ data class HttpResponseData (val statusCode : Int , val body : String? ) {
725+ fun statusCode () = statusCode
726+ fun body () = body
727+ }
728+
674729 private suspend fun fetchWS (
675730 endpointUrl : String ,
676- connectParams : ConnectParams ,
677731 ownerToken : String? ,
732+ timeoutMillis : Long ,
733+ ): HttpResponseData {
734+ var httpRequestBuilder = HttpRequest .newBuilder()
735+ .uri(URI .create(endpointUrl))
736+ .GET ()
737+ .timeout(Duration .ofMillis(timeoutMillis))
738+ if (! ownerToken.isNullOrBlank()) {
739+ httpRequestBuilder = httpRequestBuilder.header(" x-gitpod-owner-token" , ownerToken)
740+ }
741+ val httpRequest = httpRequestBuilder.build()
742+ val responseFuture =
743+ httpClient.sendAsync(httpRequest, HttpResponse .BodyHandlers .ofString())
744+
745+ try {
746+ val response = responseFuture.await()
747+ return HttpResponseData (response.statusCode(), response.body())
748+ } catch (e: Exception ) {
749+ if (responseFuture.isCancelled) {
750+ throw CancellationException ()
751+ }
752+ throw e
753+ }
754+ }
755+
756+ private suspend fun retryFetchWS (
757+ endpointUrl : String ,
758+ connectParams : ConnectParams ,
759+ ownerToken : String?
678760 ): String? {
679761 val maxRequestTimeout = 30 * 1000L
680762 val timeoutDelayGrowFactor = 1.5
681763 var requestTimeout = 2 * 1000L
682764 while (true ) {
683765 coroutineContext.job.ensureActive()
684766 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()
767+ val response = fetchWS(endpointUrl, ownerToken, requestTimeout)
695768 if (response.statusCode() == 200 ) {
696769 return response.body()
697770 }
0 commit comments