@@ -26,14 +26,18 @@ import java.util.*
26
26
import java.util.concurrent.CompletableFuture
27
27
import java.util.concurrent.ConcurrentHashMap
28
28
import java.util.concurrent.atomic.AtomicReference
29
- import kotlinx.coroutines.sync.Mutex
30
- import kotlinx.coroutines.sync.withLock
29
+ import kotlinx.coroutines.sync.Semaphore
30
+ import kotlinx.coroutines.sync.withPermit
31
31
32
32
@Suppress(" UnstableApiUsage" )
33
33
abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService {
34
34
companion object {
35
35
const val FORWARDED_PORT_LABEL = " ForwardedByGitpod"
36
36
const val EXPOSED_PORT_LABEL = " ExposedByGitpod"
37
+ private const val MAX_CONCURRENT_OPERATIONS = 10
38
+ private const val BATCH_SIZE = 10
39
+ private const val BATCH_DELAY = 100L
40
+ private const val DEBOUNCE_DELAY = 500L
37
41
}
38
42
39
43
private val perClientPortForwardingManager = service<PerClientPortForwardingManager >()
@@ -52,12 +56,8 @@ abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService
52
56
// Debounce job for port updates
53
57
private var debounceJob: Job ? = null
54
58
55
- // Batch size for port operations
56
- private val batchSize = 10
57
- private val batchDelay = 100L
58
-
59
- // Mutex to limit concurrent operations
60
- private val operationMutex = Mutex ()
59
+ // Semaphore to limit concurrent operations
60
+ private val operationSemaphore = Semaphore (MAX_CONCURRENT_OPERATIONS )
61
61
62
62
init { start() }
63
63
@@ -82,7 +82,6 @@ abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService
82
82
is InterruptedException , is CancellationException -> {
83
83
cancel(" gitpod: Stopped observing ports list due to an expected interruption." )
84
84
}
85
-
86
85
else -> {
87
86
thisLogger().warn(
88
87
" gitpod: Got an error while trying to get ports list from Supervisor. " +
@@ -98,9 +97,7 @@ abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService
98
97
99
98
private fun observePortsList (): CompletableFuture <Void > {
100
99
val completableFuture = CompletableFuture <Void >()
101
-
102
100
val statusServiceStub = StatusServiceGrpc .newStub(GitpodManager .supervisorChannel)
103
-
104
101
val portsStatusRequest = Status .PortsStatusRequest .newBuilder().setObserve(true ).build()
105
102
106
103
val portsStatusResponseObserver = object :
@@ -110,12 +107,9 @@ abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService
110
107
}
111
108
112
109
override fun onNext (response : Status .PortsStatusResponse ) {
113
- // Cancel previous debounce job if exists
114
110
debounceJob?.cancel()
115
-
116
- // Create new debounce job
117
111
debounceJob = runJob(lifetime) {
118
- delay(500 )
112
+ delay(DEBOUNCE_DELAY )
119
113
try {
120
114
if (syncInProgress.compareAndSet(false , true )) {
121
115
try {
@@ -142,7 +136,6 @@ abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService
142
136
}
143
137
144
138
statusServiceStub.portsStatus(portsStatusRequest, portsStatusResponseObserver)
145
-
146
139
return completableFuture
147
140
}
148
141
@@ -165,7 +158,6 @@ abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService
165
158
val exposedPorts = servedPorts.filter { it.exposed?.url?.isNotBlank() ? : false }
166
159
val portsNumbersFromNonServedPorts = portsList.filter { ! it.served }.map { it.localPort }
167
160
168
- // Clean up unused port lifetimes
169
161
val allPortsToKeep = mutableSetOf<Int >()
170
162
171
163
val servedPortsToStartForwarding = servedPorts.filter {
@@ -184,52 +176,35 @@ abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService
184
176
.map { it.hostPortNumber }
185
177
.filter { portsNumbersFromNonServedPorts.contains(it) || ! portsNumbersFromPortsList.contains(it) }
186
178
187
- // Process port changes in background with batching
188
179
runJob(lifetime) {
189
180
try {
190
- forwardedPortsToStopForwarding.chunked(batchSize).forEach { batch ->
191
- batch.forEach { port ->
192
- operationMutex.withLock { stopForwarding(port) }
193
- }
194
- delay(batchDelay)
181
+ processPortsInBatches(forwardedPortsToStopForwarding) { port ->
182
+ operationSemaphore.withPermit { stopForwarding(port) }
195
183
}
196
184
197
- exposedPortsToStopExposingOnClient.chunked(batchSize).forEach { batch ->
198
- batch.forEach { port ->
199
- operationMutex.withLock { stopExposingOnClient(port) }
200
- }
201
- delay(batchDelay)
185
+ processPortsInBatches(exposedPortsToStopExposingOnClient) { port ->
186
+ operationSemaphore.withPermit { stopExposingOnClient(port) }
202
187
}
203
188
204
- servedPortsToStartForwarding.chunked(batchSize).forEach { batch ->
205
- batch.forEach { port ->
206
- operationMutex.withLock {
207
- startForwarding(port)
208
- allPortsToKeep.add(port.localPort)
209
- }
189
+ processPortsInBatches(servedPortsToStartForwarding) { port ->
190
+ operationSemaphore.withPermit {
191
+ startForwarding(port)
192
+ allPortsToKeep.add(port.localPort)
210
193
}
211
- delay(batchDelay)
212
194
}
213
195
214
- exposedPortsToStartExposingOnClient.chunked(batchSize).forEach { batch ->
215
- batch.forEach { port ->
216
- operationMutex.withLock {
217
- startExposingOnClient(port)
218
- allPortsToKeep.add(port.localPort)
219
- }
196
+ processPortsInBatches(exposedPortsToStartExposingOnClient) { port ->
197
+ operationSemaphore.withPermit {
198
+ startExposingOnClient(port)
199
+ allPortsToKeep.add(port.localPort)
220
200
}
221
- delay(batchDelay)
222
201
}
223
202
224
- // Update presentation for all ports in batches
225
- portsList.chunked(batchSize).forEach { batch ->
203
+ processPortsInBatches(portsList) { port ->
226
204
application.invokeLater {
227
- batch.forEach {
228
- updatePortsPresentation(it)
229
- allPortsToKeep.add(it.localPort)
230
- }
205
+ updatePortsPresentation(port)
206
+ allPortsToKeep.add(port.localPort)
231
207
}
232
- delay(batchDelay)
233
208
}
234
209
235
210
cleanupUnusedLifetimes(allPortsToKeep)
@@ -239,9 +214,17 @@ abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService
239
214
}
240
215
}
241
216
217
+ private suspend fun <T > processPortsInBatches (ports : List <T >, action : suspend (T ) -> Unit ) {
218
+ ports.chunked(BATCH_SIZE ).forEach { batch ->
219
+ batch.forEach { port ->
220
+ action(port)
221
+ }
222
+ delay(BATCH_DELAY )
223
+ }
224
+ }
225
+
242
226
private fun cleanupUnusedLifetimes (portsToKeep : Set <Int >) {
243
- val portsToRemove = portLifetimes.keys.filter { ! portsToKeep.contains(it) }
244
- portsToRemove.forEach { port ->
227
+ portLifetimes.keys.filter { ! portsToKeep.contains(it) }.forEach { port ->
245
228
portLifetimes[port]?.let { lifetime ->
246
229
thisLogger().debug(" gitpod: Terminating lifetime for port $port " )
247
230
lifetime.terminate()
@@ -251,9 +234,7 @@ abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService
251
234
}
252
235
253
236
private fun startForwarding (portStatus : PortsStatus ) {
254
- if (isLocalPortForwardingDisabled()) {
255
- return
256
- }
237
+ if (isLocalPortForwardingDisabled()) return
257
238
258
239
val portLifetime = getOrCreatePortLifetime(portStatus.localPort)
259
240
@@ -332,12 +313,11 @@ abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService
332
313
}
333
314
}
334
315
335
- private fun getOrCreatePortLifetime (port : Int ): Lifetime {
336
- return portLifetimes.computeIfAbsent(port) {
316
+ private fun getOrCreatePortLifetime (port : Int ): Lifetime =
317
+ portLifetimes.computeIfAbsent(port) {
337
318
thisLogger().debug(" gitpod: Creating new lifetime for port $port " )
338
319
lifetime.createNested()
339
320
}
340
- }
341
321
342
322
private fun terminatePortLifetime (port : Int ) {
343
323
portLifetimes[port]?.let { portLifetime ->
@@ -349,39 +329,42 @@ abstract class AbstractGitpodPortForwardingService : GitpodPortForwardingService
349
329
350
330
private fun updatePortsPresentation (portStatus : PortsStatus ) {
351
331
perClientPortForwardingManager.getPorts(portStatus.localPort).forEach {
352
- if (it.configuration.isForwardedPort()) {
353
- it.presentation.name = portStatus.name
354
- it.presentation.description = portStatus.description
355
- it.presentation.tooltip = " Forwarded"
356
- it.presentation.icon = RowIcon (AllIcons .Actions .Commit )
357
- } else if (it.configuration.isExposedPort()) {
358
- val isPubliclyExposed = (portStatus.exposed.visibility == Status .PortVisibility .public_visibility)
359
-
360
- it.presentation.name = portStatus.name
361
- it.presentation.description = portStatus.description
362
- it.presentation.tooltip = " Exposed (${if (isPubliclyExposed) " Public" else " Private" } )"
363
- it.presentation.icon = if (isPubliclyExposed) {
364
- RowIcon (AllIcons .Actions .Commit )
365
- } else {
366
- RowIcon (AllIcons .Actions .Commit , AllIcons .Diff .Lock )
332
+ when {
333
+ it.configuration.isForwardedPort() -> {
334
+ it.presentation.name = portStatus.name
335
+ it.presentation.description = portStatus.description
336
+ it.presentation.tooltip = " Forwarded"
337
+ it.presentation.icon = RowIcon (AllIcons .Actions .Commit )
338
+ }
339
+ it.configuration.isExposedPort() -> {
340
+ val isPubliclyExposed = (portStatus.exposed.visibility == Status .PortVisibility .public_visibility)
341
+ it.presentation.name = portStatus.name
342
+ it.presentation.description = portStatus.description
343
+ it.presentation.tooltip = " Exposed (${if (isPubliclyExposed) " Public" else " Private" } )"
344
+ it.presentation.icon = if (isPubliclyExposed) {
345
+ RowIcon (AllIcons .Actions .Commit )
346
+ } else {
347
+ RowIcon (AllIcons .Actions .Commit , AllIcons .Diff .Lock )
348
+ }
367
349
}
368
350
}
369
351
}
370
352
}
371
353
372
- override fun getLocalHostUriFromHostPort (hostPort : Int ): Optional <URI > {
373
- val forwardedPort = perClientPortForwardingManager.getPorts(hostPort).firstOrNull {
374
- it.configuration.isForwardedPort()
375
- } ? : return Optional .empty()
376
-
377
- (forwardedPort.configuration as PortConfiguration .PerClientTcpForwarding ).clientPortState.let {
378
- return if (it is ClientPortState .Assigned ) {
379
- Optional .of(URIBuilder ().setScheme(" http" ).setHost(it.clientInterface).setPort(it.clientPort).build())
380
- } else {
381
- Optional .empty()
382
- }
383
- }
384
- }
354
+ override fun getLocalHostUriFromHostPort (hostPort : Int ): Optional <URI > =
355
+ perClientPortForwardingManager.getPorts(hostPort)
356
+ .firstOrNull { it.configuration.isForwardedPort() }
357
+ ?.let { forwardedPort ->
358
+ (forwardedPort.configuration as PortConfiguration .PerClientTcpForwarding )
359
+ .clientPortState
360
+ .let {
361
+ if (it is ClientPortState .Assigned ) {
362
+ Optional .of(URIBuilder ().setScheme(" http" ).setHost(it.clientInterface).setPort(it.clientPort).build())
363
+ } else {
364
+ Optional .empty()
365
+ }
366
+ }
367
+ } ? : Optional .empty()
385
368
386
369
override fun dispose () {
387
370
portLifetimes.values.forEach { it.terminate() }
0 commit comments