Skip to content

Commit 4ca9190

Browse files
authored
fix: socket connection timeout (#53)
Context: - okhttp uses an HTTP/2 connection to the coder rest api in order to resolves the workspaces. - HTTP/2 uses a single TCP connection for multiple requests (multiplexing). If the connection is idle, the http server can close that connection, with client side ending in a socket timeout if it doesn't detect the drop in time. - similarly on the client side, if the OS goes into sleep mode, the connection might have been interrupted. HTTP/2 doesn't always detect this quickly, leading to stale streams when Toolbox wakes up. Implementation: - we could try to force the client to use HTTP/1 which creates a TCP connection for each request, but from my testing it seems that configuring a retry strategy when a client attempts to reuse a TCP connection that has unexpectedly closed plus detecting large gaps between the last poll time and socket timeout time allows us to reset the client and create fresh TCP connections. - resolves #13
1 parent e6af3ca commit 4ca9190

File tree

5 files changed

+32
-21
lines changed

5 files changed

+32
-21
lines changed

src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,20 @@ import kotlinx.coroutines.isActive
3030
import kotlinx.coroutines.launch
3131
import kotlinx.coroutines.selects.onTimeout
3232
import kotlinx.coroutines.selects.select
33-
import okhttp3.OkHttpClient
33+
import java.net.SocketTimeoutException
3434
import java.net.URI
3535
import java.net.URL
3636
import kotlin.coroutines.cancellation.CancellationException
3737
import kotlin.time.Duration.Companion.seconds
38+
import kotlin.time.TimeSource
3839
import com.jetbrains.toolbox.api.ui.components.AccountDropdownField as DropDownMenu
3940
import com.jetbrains.toolbox.api.ui.components.AccountDropdownField as dropDownFactory
4041

42+
private val POLL_INTERVAL = 5.seconds
43+
4144
@OptIn(ExperimentalCoroutinesApi::class)
4245
class CoderRemoteProvider(
4346
private val context: CoderToolboxContext,
44-
private val httpClient: OkHttpClient,
4547
) : RemoteProvider("Coder") {
4648
// Current polling job.
4749
private var pollJob: Job? = null
@@ -66,7 +68,7 @@ class CoderRemoteProvider(
6668
private var firstRun = true
6769
private val isInitialized: MutableStateFlow<Boolean> = MutableStateFlow(false)
6870
private var coderHeaderPage = NewEnvironmentPage(context, context.i18n.pnotr(getDeploymentURL()?.first ?: ""))
69-
private val linkHandler = CoderProtocolHandler(context, httpClient, dialogUi, isInitialized)
71+
private val linkHandler = CoderProtocolHandler(context, dialogUi, isInitialized)
7072
override val environments: MutableStateFlow<LoadableState<List<RemoteProviderEnvironment>>> = MutableStateFlow(
7173
LoadableState.Value(emptyList())
7274
)
@@ -77,6 +79,7 @@ class CoderRemoteProvider(
7779
* first time).
7880
*/
7981
private fun poll(client: CoderRestClient, cli: CoderCLIManager): Job = context.cs.launch {
82+
var lastPollTime = TimeSource.Monotonic.markNow()
8083
while (isActive) {
8184
try {
8285
context.logger.debug("Fetching workspace agents from ${client.url}")
@@ -134,16 +137,28 @@ class CoderRemoteProvider(
134137
} catch (_: CancellationException) {
135138
context.logger.debug("${client.url} polling loop canceled")
136139
break
140+
} catch (ex: SocketTimeoutException) {
141+
val elapsed = lastPollTime.elapsedNow()
142+
if (elapsed > POLL_INTERVAL * 2) {
143+
context.logger.info("wake-up from an OS sleep was detected, going to re-initialize the http client...")
144+
client.setupSession()
145+
} else {
146+
context.logger.error(ex, "workspace polling error encountered")
147+
pollError = ex
148+
logout()
149+
break
150+
}
137151
} catch (ex: Exception) {
138-
context.logger.info(ex, "workspace polling error encountered")
152+
context.logger.error(ex, "workspace polling error encountered")
139153
pollError = ex
140154
logout()
141155
break
142156
}
157+
143158
// TODO: Listening on a web socket might be better?
144159
select<Unit> {
145-
onTimeout(5.seconds) {
146-
context.logger.trace("workspace poller waked up by the 5 seconds timeout")
160+
onTimeout(POLL_INTERVAL) {
161+
context.logger.trace("workspace poller waked up by the $POLL_INTERVAL timeout")
147162
}
148163
triggerSshConfig.onReceive { shouldTrigger ->
149164
if (shouldTrigger) {
@@ -152,6 +167,7 @@ class CoderRemoteProvider(
152167
}
153168
}
154169
}
170+
lastPollTime = TimeSource.Monotonic.markNow()
155171
}
156172
}
157173

@@ -329,7 +345,6 @@ class CoderRemoteProvider(
329345
context,
330346
deploymentURL,
331347
token,
332-
httpClient,
333348
::goToEnvironmentsPage,
334349
) { client, cli ->
335350
// Store the URL and token for use next time.

src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
1515
import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
1616
import com.jetbrains.toolbox.api.ui.ToolboxUi
1717
import kotlinx.coroutines.CoroutineScope
18-
import okhttp3.OkHttpClient
1918

2019
/**
2120
* Entry point into the extension.
@@ -35,8 +34,7 @@ class CoderToolboxExtension : RemoteDevExtension {
3534
serviceLocator.getService(LocalizableStringFactory::class.java),
3635
CoderSettingsStore(serviceLocator.getService(PluginSettingsStore::class.java), Environment(), logger),
3736
CoderSecretsStore(serviceLocator.getService(PluginSecretStore::class.java)),
38-
),
39-
OkHttpClient(),
37+
)
4038
)
4139
}
4240
}

src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,19 @@ open class CoderRestClient(
5353
val token: String?,
5454
private val proxyValues: ProxyValues? = null,
5555
private val pluginVersion: String = "development",
56-
existingHttpClient: OkHttpClient? = null,
5756
) {
5857
private val settings = context.settingsStore.readOnly()
59-
private val httpClient: OkHttpClient
60-
private val retroRestClient: CoderV2RestFacade
58+
private lateinit var httpClient: OkHttpClient
59+
private lateinit var retroRestClient: CoderV2RestFacade
6160

6261
lateinit var me: User
6362
lateinit var buildVersion: String
6463

6564
init {
65+
setupSession()
66+
}
67+
68+
fun setupSession() {
6669
val moshi =
6770
Moshi.Builder()
6871
.add(ArchConverter())
@@ -73,7 +76,7 @@ open class CoderRestClient(
7376

7477
val socketFactory = coderSocketFactory(settings.tls)
7578
val trustManagers = coderTrustManagers(settings.tls.caPath)
76-
var builder = existingHttpClient?.newBuilder() ?: OkHttpClient.Builder()
79+
var builder = OkHttpClient.Builder()
7780

7881
if (proxyValues != null) {
7982
builder =
@@ -103,6 +106,7 @@ open class CoderRestClient(
103106
builder
104107
.sslSocketFactory(socketFactory, trustManagers[0] as X509TrustManager)
105108
.hostnameVerifier(CoderHostnameVerifier(settings.tls.altHostname))
109+
.retryOnConnectionFailure(true)
106110
.addInterceptor {
107111
it.proceed(
108112
it.request().newBuilder().addHeader(

src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import kotlinx.coroutines.flow.StateFlow
1616
import kotlinx.coroutines.flow.first
1717
import kotlinx.coroutines.launch
1818
import kotlinx.coroutines.time.withTimeout
19-
import okhttp3.OkHttpClient
2019
import java.net.HttpURLConnection
2120
import java.net.URI
2221
import java.net.URL
@@ -26,7 +25,6 @@ import kotlin.time.toJavaDuration
2625

2726
open class CoderProtocolHandler(
2827
private val context: CoderToolboxContext,
29-
private val httpClient: OkHttpClient?,
3028
private val dialogUi: DialogUi,
3129
private val isInitialized: StateFlow<Boolean>,
3230
) {
@@ -230,8 +228,7 @@ open class CoderProtocolHandler(
230228
deploymentURL.toURL(),
231229
token,
232230
proxyValues = null, // TODO - not sure the above comment applies as we are creating our own http client
233-
PluginManager.pluginInfo.version,
234-
httpClient
231+
PluginManager.pluginInfo.version
235232
)
236233
client.authenticate()
237234
return client

src/main/kotlin/com/coder/toolbox/views/ConnectPage.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import kotlinx.coroutines.Job
1414
import kotlinx.coroutines.flow.MutableStateFlow
1515
import kotlinx.coroutines.flow.StateFlow
1616
import kotlinx.coroutines.launch
17-
import okhttp3.OkHttpClient
1817
import java.net.URL
1918

2019
/**
@@ -24,7 +23,6 @@ class ConnectPage(
2423
private val context: CoderToolboxContext,
2524
private val url: URL,
2625
private val token: String?,
27-
private val httpClient: OkHttpClient,
2826
private val onCancel: () -> Unit,
2927
private val onConnect: (
3028
client: CoderRestClient,
@@ -95,7 +93,6 @@ class ConnectPage(
9593
token,
9694
proxyValues = null,
9795
PluginManager.pluginInfo.version,
98-
httpClient
9996
)
10097
client.authenticate()
10198
updateStatus(context.i18n.ptrl("Checking Coder binary..."), error = null)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy