@@ -47,7 +47,6 @@ import io.gitpod.jetbrains.gateway.common.GitpodConnectionHandleFactory
47
47
import io.gitpod.jetbrains.icons.GitpodIcons
48
48
import kotlinx.coroutines.*
49
49
import kotlinx.coroutines.future.await
50
- import java.awt.Component
51
50
import java.net.URL
52
51
import java.net.http.HttpClient
53
52
import java.net.http.HttpRequest
@@ -58,6 +57,7 @@ import javax.swing.JLabel
58
57
import kotlin.coroutines.coroutineContext
59
58
import kotlin.io.path.absolutePathString
60
59
import kotlin.io.path.writeText
60
+ import kotlin.random.Random.Default.nextInt
61
61
62
62
@Suppress(" UnstableApiUsage" , " OPT_IN_USAGE" )
63
63
class GitpodConnectionProvider : GatewayConnectionProvider {
@@ -202,6 +202,39 @@ class GitpodConnectionProvider : GatewayConnectionProvider {
202
202
203
203
var lastUpdate: WorkspaceInstance ? = null
204
204
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
+
205
238
try {
206
239
for (update in updates) {
207
240
try {
@@ -518,7 +551,7 @@ class GitpodConnectionProvider : GatewayConnectionProvider {
518
551
if (! connectParams.backendPort.isNullOrBlank()) {
519
552
resolveJoinLinkUrl + = " ?backendPort=${connectParams.backendPort} "
520
553
}
521
- var rawResp = fetchWS (resolveJoinLinkUrl, connectParams, ownerToken)
554
+ var rawResp = retryFetchWS (resolveJoinLinkUrl, connectParams, ownerToken)
522
555
if (rawResp != null ) {
523
556
return with (jacksonMapper) {
524
557
propertyNamingStrategy = PropertyNamingStrategies .LowerCamelCaseStrategy ()
@@ -531,13 +564,34 @@ class GitpodConnectionProvider : GatewayConnectionProvider {
531
564
if (! connectParams.backendPort.isNullOrBlank()) {
532
565
resolveJoinLinkUrl + = " ?backendPort=${connectParams.backendPort} "
533
566
}
534
- rawResp = fetchWS (resolveJoinLinkUrl, connectParams, ownerToken)
567
+ rawResp = retryFetchWS (resolveJoinLinkUrl, connectParams, ownerToken)
535
568
if (rawResp != null ) {
536
569
return JoinLinkResp (- 1 , rawResp)
537
570
}
538
571
return null
539
572
}
540
573
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
+
541
595
private fun resolveCredentials (
542
596
host : String ,
543
597
port : Int ,
@@ -589,7 +643,7 @@ class GitpodConnectionProvider : GatewayConnectionProvider {
589
643
ownerToken : String
590
644
): CreateSSHKeyPairResponse ? {
591
645
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)
593
647
if (value.isNullOrBlank()) {
594
648
return null
595
649
}
@@ -604,7 +658,7 @@ class GitpodConnectionProvider : GatewayConnectionProvider {
604
658
connectParams : ConnectParams
605
659
): List <SSHHostKey >? {
606
660
val hostKeysValue =
607
- fetchWS (" https://${ideUrl.host} /_ssh/host_keys" , connectParams, null )
661
+ retryFetchWS (" https://${ideUrl.host} /_ssh/host_keys" , connectParams, null )
608
662
if (hostKeysValue.isNullOrBlank()) {
609
663
return null
610
664
}
@@ -671,27 +725,50 @@ class GitpodConnectionProvider : GatewayConnectionProvider {
671
725
return acceptHostKey
672
726
}
673
727
728
+ data class HttpResponseData (val statusCode : Int , val body : String? ) {
729
+ fun statusCode () = statusCode
730
+ fun body () = body
731
+ }
732
+
674
733
private suspend fun fetchWS (
675
734
endpointUrl : String ,
676
- connectParams : ConnectParams ,
677
735
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?
678
764
): String? {
679
765
val maxRequestTimeout = 30 * 1000L
680
766
val timeoutDelayGrowFactor = 1.5
681
767
var requestTimeout = 2 * 1000L
682
768
while (true ) {
683
769
coroutineContext.job.ensureActive()
684
770
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)
695
772
if (response.statusCode() == 200 ) {
696
773
return response.body()
697
774
}
0 commit comments