Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
499 changes: 348 additions & 151 deletions src/main/kotlin/com/redhat/devtools/gateway/DevSpacesConnection.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright (c) 2026 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package com.redhat.devtools.gateway

import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.redhat.devtools.gateway.util.WorkspaceSessionProgress
import com.redhat.devtools.gateway.util.checkProgressCanceled

Check warning on line 19 in src/main/kotlin/com/redhat/devtools/gateway/ForwardRecoveryProgress.kt

View workflow job for this annotation

GitHub Actions / Inspect code

Unused import directive

Unused import directive
import com.redhat.devtools.gateway.util.clearProgressText2Safely
import com.redhat.devtools.gateway.util.delayRespectingProgress
import com.redhat.devtools.gateway.util.updateProgress
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds

/**
* Shows delayed progress when port-forward cannot resolve a workspace pod for an extended period.
*
* Progress appears only after [showAfter] of sustained unavailability so brief glitches stay silent.
* Suppressed while pod-roll reconnect or annotated restart handlers own recovery.
*/
internal class ForwardRecoveryProgress(
private val scope: CoroutineScope,
private val sessionCtx: ThinClientSessionContext,
private val isWorkspaceRestartInProgress: () -> Boolean,
private val onCanceled: () -> Unit,
private val showAfter: Duration = DEFAULT_SHOW_AFTER,
) {
private val waitingSinceMillis = AtomicLong(0)
private val progressActive = AtomicBoolean(false)
private var showProgressJob: Job? = null

companion object {
private val DEFAULT_SHOW_AFTER: Duration = 20_000.milliseconds
private val PROGRESS_POLL_DELAY: Duration = 500.milliseconds
const val PROGRESS_TITLE: String = "Reconnecting to workspace"
}

/** Called when no workspace pod is available yet. */
fun onPodUnavailable() {
if (shouldSuppress()) {
reset()
return
}
if (waitingSinceMillis.compareAndSet(0, System.currentTimeMillis())) {
scheduleShowProgress()
}
}

/** Called when a pod is resolved for port-forwarding. */
fun onPodResolved() {
reset()
}

/** Called when a dedicated recovery handler (pod-roll reconnect) takes over. */
fun dismiss() {
reset()
}

private fun shouldSuppress(): Boolean =
isWorkspaceRestartInProgress() || sessionCtx.reconnecting.get()

private fun scheduleShowProgress() {
showProgressJob?.cancel()
showProgressJob = scope.launch {
delay(showAfter)
if (waitingSinceMillis.get() != 0L && !shouldSuppress()) {
maybeShowProgress()
}
}
}

private fun maybeShowProgress() {
if (!progressActive.compareAndSet(false, true)) return
ProgressManager.getInstance().run(object : Task.Backgroundable(null, PROGRESS_TITLE, true) {
override fun run(indicator: ProgressIndicator) {
indicator.isIndeterminate = true
runBlocking {
try {
indicator.updateProgress(WorkspaceSessionProgress.WAITING_FOR_POD, 0.0)
while (waitingSinceMillis.get() != 0L && !shouldSuppress()) {
delayRespectingProgress(indicator, PROGRESS_POLL_DELAY)
}
} catch (e: ProcessCanceledException) {
onCanceled()
throw e
} finally {
indicator.clearProgressText2Safely()
progressActive.set(false)
}
}
}
})
}

private fun reset() {
showProgressJob?.cancel()
showProgressJob = null
waitingSinceMillis.set(0)
}
}
26 changes: 26 additions & 0 deletions src/main/kotlin/com/redhat/devtools/gateway/PodResolution.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2026 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package com.redhat.devtools.gateway

import io.kubernetes.client.openapi.models.V1Pod

/**
* Outcome of resolving the workspace pod for session recovery routing.
*
* Domain vocabulary only — no port-forward or progress semantics.
*/
sealed class PodResolution {
data class Ready(val pod: V1Pod) : PodResolution()
data object Unavailable : PodResolution()
data class RollDelegated(val pod: V1Pod) : PodResolution()
data object RestartSuppressed : PodResolution()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright (c) 2026 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package com.redhat.devtools.gateway

import com.intellij.openapi.diagnostic.thisLogger
import com.redhat.devtools.gateway.openshift.DevWorkspacePods
import com.redhat.devtools.gateway.openshift.PodForwardResolution
import com.redhat.devtools.gateway.util.podLogIdentity
import io.kubernetes.client.openapi.models.V1Pod
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds

/**
* Adapts [WorkspacePodTracker] outcomes to port-forward transport and recovery progress.
*
* While no pod is available, [resolve] polls until a running pod appears before returning.
* On pod roll, the rolled pod is returned immediately so port-forward can re-establish in
* parallel with [ThinClientReconnect] (IDE wait via exec, then thin client via local port).
*/
internal class PortForwardPodResolver(
private val tracker: WorkspacePodTracker,
private val sessionCtx: ThinClientSessionContext,
private val forwardRecovery: ForwardRecoveryProgress,
) {
companion object {
private val POD_POLL_DELAY = DevWorkspacePods.DEFAULT_RECONNECT_DELAY_SECONDS.seconds
}

suspend fun resolve(): PodForwardResolution {
return when (val result = tracker.resolvePod()) {
is PodResolution.Ready -> {
forwardRecovery.onPodResolved()
PodForwardResolution(result.pod)
}
is PodResolution.Unavailable -> {
PodForwardResolution(waitForRunningPod())
}
is PodResolution.RollDelegated -> {
forwardRecovery.dismiss()
thisLogger().info(

Check notice on line 49 in src/main/kotlin/com/redhat/devtools/gateway/PortForwardPodResolver.kt

View workflow job for this annotation

GitHub Actions / Inspect code

Non-distinguishable logging calls

Similar log messages
"Port forward: pod roll to ${podLogIdentity(result.pod)}, " +
"re-establishing forward on local port ${sessionCtx.localPort}"
)
PodForwardResolution(result.pod)
}
is PodResolution.RestartSuppressed -> {
forwardRecovery.dismiss()
PodForwardResolution(waitForRunningPod())
}
}
}

/** Polls until a running pod is available. */
private suspend fun waitForRunningPod(): V1Pod {
forwardRecovery.onPodUnavailable()
thisLogger().info(
"Port forward: waiting for workspace pod on local port ${sessionCtx.localPort}"
)
while (true) {
delay(POD_POLL_DELAY)
when (val result = tracker.resolvePod()) {
is PodResolution.Ready -> {
forwardRecovery.onPodResolved()
return result.pod
}
is PodResolution.RollDelegated -> {
forwardRecovery.dismiss()
thisLogger().info(

Check notice on line 77 in src/main/kotlin/com/redhat/devtools/gateway/PortForwardPodResolver.kt

View workflow job for this annotation

GitHub Actions / Inspect code

Non-distinguishable logging calls

Similar log messages
"Port forward: pod roll to ${podLogIdentity(result.pod)} during wait, " +
"re-establishing forward on local port ${sessionCtx.localPort}"
)
return result.pod
}
is PodResolution.RestartSuppressed -> forwardRecovery.dismiss()
is PodResolution.Unavailable -> Unit
}
}
}
}
Loading
Loading