Skip to content

Commit aeab62b

Browse files
committed
[JetBrains] Improve port forwarding logic
Tool: gitpod/catfood.gitpod.cloud
1 parent 40c5b7c commit aeab62b

File tree

1 file changed

+135
-12
lines changed

1 file changed

+135
-12
lines changed

components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/AbstractGitpodPortForwardingService.kt

Lines changed: 135 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import kotlinx.coroutines.future.asDeferred
2323
import org.apache.http.client.utils.URIBuilder
2424
import java.util.*
2525
import java.util.concurrent.CompletableFuture
26+
import java.util.concurrent.ConcurrentHashMap
27+
import java.util.concurrent.atomic.AtomicReference
2628

2729
@Suppress("UnstableApiUsage")
2830
abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService {
@@ -35,6 +37,15 @@ abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService
3537
private val ignoredPortsForNotificationService = service<GitpodIgnoredPortsForNotificationService>()
3638
private val lifetime = Lifetime.Eternal.createNested()
3739

40+
// Store current observed ports and their lifetime references
41+
private val portLifetimes = ConcurrentHashMap<Int, Lifetime>()
42+
43+
// Prevent multiple sync operations from executing simultaneously
44+
private val syncInProgress = AtomicReference(false)
45+
46+
// Last observed ports list, used to prevent duplicate processing
47+
private var lastPortsList = listOf<PortsStatus>()
48+
3849
init { start() }
3950

4051
private fun start() {
@@ -86,7 +97,17 @@ abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService
8697
}
8798

8899
override fun onNext(response: Status.PortsStatusResponse) {
89-
application.invokeLater { syncPortsListWithClient(response) }
100+
application.invokeLater {
101+
if (syncInProgress.compareAndSet(false, true)) {
102+
try {
103+
syncPortsListWithClient(response)
104+
} finally {
105+
syncInProgress.set(false)
106+
}
107+
} else {
108+
thisLogger().debug("gitpod: Sync already in progress, skipping update")
109+
}
110+
}
90111
}
91112

92113
override fun onCompleted() {
@@ -110,44 +131,97 @@ abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService
110131
private fun syncPortsListWithClient(response: Status.PortsStatusResponse) {
111132
val ignoredPorts = ignoredPortsForNotificationService.getIgnoredPorts()
112133
val portsList = response.portsList.filter { !ignoredPorts.contains(it.localPort) }
134+
135+
if (portsList == lastPortsList) {
136+
thisLogger().debug("gitpod: Port list unchanged, skipping update")
137+
return
138+
}
139+
lastPortsList = portsList
140+
113141
val portsNumbersFromPortsList = portsList.map { it.localPort }
114142
val servedPorts = portsList.filter { it.served }
115143
val exposedPorts = servedPorts.filter { it.exposed?.url?.isNotBlank() ?: false }
116144
val portsNumbersFromNonServedPorts = portsList.filter { !it.served }.map { it.localPort }
145+
146+
// Clean up unused port lifetimes
147+
val allCurrentPorts = perClientPortForwardingManager.getPorts()
148+
val allPortsToKeep = mutableSetOf<Int>()
149+
150+
// Find ports that need to start forwarding
117151
val servedPortsToStartForwarding = servedPorts.filter {
118152
perClientPortForwardingManager.getPorts(it.localPort).none { p -> p.labels.contains(FORWARDED_PORT_LABEL) }
119153
}
154+
155+
// Find ports that need to start exposing on client
120156
val exposedPortsToStartExposingOnClient = exposedPorts.filter {
121157
perClientPortForwardingManager.getPorts(it.localPort).none { p -> p.labels.contains(EXPOSED_PORT_LABEL) }
122158
}
159+
160+
// Find ports that need to stop forwarding
123161
val forwardedPortsToStopForwarding = perClientPortForwardingManager.getPorts(FORWARDED_PORT_LABEL)
124162
.map { it.hostPortNumber }
125163
.filter { portsNumbersFromNonServedPorts.contains(it) || !portsNumbersFromPortsList.contains(it) }
164+
165+
// Find ports that need to stop exposing on client
126166
val exposedPortsToStopExposingOnClient = perClientPortForwardingManager.getPorts(EXPOSED_PORT_LABEL)
127167
.map { it.hostPortNumber }
128168
.filter { portsNumbersFromNonServedPorts.contains(it) || !portsNumbersFromPortsList.contains(it) }
129169

130-
servedPortsToStartForwarding.forEach { startForwarding(it) }
170+
forwardedPortsToStopForwarding.forEach { stopForwarding(it) }
171+
exposedPortsToStopExposingOnClient.forEach { stopExposingOnClient(it) }
131172

132-
exposedPortsToStartExposingOnClient.forEach { startExposingOnClient(it) }
173+
servedPortsToStartForwarding.forEach {
174+
startForwarding(it)
175+
allPortsToKeep.add(it.localPort)
176+
}
133177

134-
forwardedPortsToStopForwarding.forEach { stopForwarding(it) }
178+
exposedPortsToStartExposingOnClient.forEach {
179+
startExposingOnClient(it)
180+
allPortsToKeep.add(it.localPort)
181+
}
135182

136-
exposedPortsToStopExposingOnClient.forEach { stopExposingOnClient(it) }
183+
portsList.forEach {
184+
updatePortsPresentation(it)
185+
allPortsToKeep.add(it.localPort)
186+
}
187+
188+
cleanupUnusedLifetimes(allPortsToKeep)
189+
}
137190

138-
portsList.forEach { updatePortsPresentation(it) }
191+
private fun cleanupUnusedLifetimes(portsToKeep: Set<Int>) {
192+
val portsToRemove = portLifetimes.keys.filter { !portsToKeep.contains(it) }
193+
portsToRemove.forEach { port ->
194+
portLifetimes[port]?.let { lifetime ->
195+
thisLogger().debug("gitpod: Terminating lifetime for port $port")
196+
lifetime.terminate()
197+
portLifetimes.remove(port)
198+
}
199+
}
139200
}
140201

141202
private fun startForwarding(portStatus: PortsStatus) {
142203
if (isLocalPortForwardingDisabled()) {
143204
return
144205
}
206+
207+
val portLifetime = getOrCreatePortLifetime(portStatus.localPort)
208+
145209
try {
146-
perClientPortForwardingManager.forwardPort(
210+
thisLogger().debug("gitpod: Starting forwarding for port ${portStatus.localPort}")
211+
val port = perClientPortForwardingManager.forwardPort(
147212
portStatus.localPort,
148213
PortType.TCP,
149214
setOf(FORWARDED_PORT_LABEL),
150215
)
216+
217+
portLifetime.onTerminationOrNow {
218+
thisLogger().debug("gitpod: Cleaning up port ${portStatus.localPort} due to lifetime termination")
219+
try {
220+
perClientPortForwardingManager.removePort(port)
221+
} catch (e: Exception) {
222+
thisLogger().warn("gitpod: Failed to remove port on lifetime termination", e)
223+
}
224+
}
151225
} catch (throwable: Throwable) {
152226
if (throwable !is PortAlreadyForwardedException) {
153227
thisLogger().warn("gitpod: Caught an exception while forwarding port: ${throwable.message}")
@@ -156,23 +230,70 @@ abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService
156230
}
157231

158232
private fun stopForwarding(hostPort: Int) {
159-
perClientPortForwardingManager.getPorts(hostPort)
233+
thisLogger().debug("gitpod: Stopping forwarding for port $hostPort")
234+
val portsToRemove = perClientPortForwardingManager.getPorts(hostPort)
160235
.filter { it.labels.contains(FORWARDED_PORT_LABEL) }
161-
.forEach { perClientPortForwardingManager.removePort(it) }
236+
237+
terminatePortLifetime(hostPort)
238+
239+
portsToRemove.forEach {
240+
try {
241+
perClientPortForwardingManager.removePort(it)
242+
} catch (e: Exception) {
243+
thisLogger().warn("gitpod: Failed to remove forwarded port $hostPort", e)
244+
}
245+
}
162246
}
163247

164248
private fun startExposingOnClient(portStatus: PortsStatus) {
165-
perClientPortForwardingManager.exposePort(
249+
val portLifetime = getOrCreatePortLifetime(portStatus.localPort)
250+
251+
thisLogger().debug("gitpod: Starting exposing for port ${portStatus.localPort}")
252+
val port = perClientPortForwardingManager.exposePort(
166253
portStatus.localPort,
167254
portStatus.exposed.url,
168255
setOf(EXPOSED_PORT_LABEL),
169256
)
257+
258+
portLifetime.onTerminationOrNow {
259+
thisLogger().debug("gitpod: Cleaning up exposed port ${portStatus.localPort} due to lifetime termination")
260+
try {
261+
perClientPortForwardingManager.removePort(port)
262+
} catch (e: Exception) {
263+
thisLogger().warn("gitpod: Failed to remove exposed port on lifetime termination", e)
264+
}
265+
}
170266
}
171267

172268
private fun stopExposingOnClient(hostPort: Int) {
173-
perClientPortForwardingManager.getPorts(hostPort)
269+
thisLogger().debug("gitpod: Stopping exposing for port $hostPort")
270+
val portsToRemove = perClientPortForwardingManager.getPorts(hostPort)
174271
.filter { it.labels.contains(EXPOSED_PORT_LABEL) }
175-
.forEach { perClientPortForwardingManager.removePort(it) }
272+
273+
terminatePortLifetime(hostPort)
274+
275+
portsToRemove.forEach {
276+
try {
277+
perClientPortForwardingManager.removePort(it)
278+
} catch (e: Exception) {
279+
thisLogger().warn("gitpod: Failed to remove exposed port $hostPort", e)
280+
}
281+
}
282+
}
283+
284+
private fun getOrCreatePortLifetime(port: Int): Lifetime {
285+
return portLifetimes.computeIfAbsent(port) {
286+
thisLogger().debug("gitpod: Creating new lifetime for port $port")
287+
lifetime.createNested()
288+
}
289+
}
290+
291+
private fun terminatePortLifetime(port: Int) {
292+
portLifetimes[port]?.let { portLifetime ->
293+
thisLogger().debug("gitpod: Terminating lifetime for port $port")
294+
portLifetime.terminate()
295+
portLifetimes.remove(port)
296+
}
176297
}
177298

178299
private fun updatePortsPresentation(portStatus: PortsStatus) {
@@ -212,6 +333,8 @@ abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService
212333
}
213334

214335
override fun dispose() {
336+
portLifetimes.values.forEach { it.terminate() }
337+
portLifetimes.clear()
215338
lifetime.terminate()
216339
}
217340
}

0 commit comments

Comments
 (0)