From 8cc198d1612257e05e31b33ca079e6854add111b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Apr 2026 02:03:23 +0000 Subject: [PATCH] fix: relay connectivity degradation with Tor by adding WebSocket pings, pool eviction, and reducing backoff Dead WebSocket connections through Tor were going undetected because no ping interval was set, leaving zombie connections that appeared connected but carried no traffic. Additionally, relay error backoff was set to ONE_DAY making recovery impossible without toggling Tor, and the shared OkHttp connection pool retained stale connections across proxy changes. - Add 120s WebSocket ping interval to detect dead Tor connections - Reduce dontTryAgainForALongTime from ONE_DAY to FIVE_MINUTES - Evict shared connection pool when proxy settings change https://claude.ai/code/session_01VFAypytKGzdmuoJAXrb72G --- .../amethyst/service/okhttp/OkHttpClientFactory.kt | 6 ++++++ .../service/okhttp/OkHttpClientFactoryForRelays.kt | 8 ++++++++ .../relay/client/single/basic/BasicRelayClient.kt | 2 +- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt index 73a7ff78b..0dcdbb60b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt @@ -46,10 +46,16 @@ class OkHttpClientFactory( .addNetworkInterceptor(keyDecryptor) .build() + private var lastProxy: Proxy? = null + fun buildHttpClient( proxy: Proxy?, timeoutSeconds: Int, ): OkHttpClient { + if (proxy != lastProxy) { + rootClient.connectionPool.evictAll() + lastProxy = proxy + } val seconds = if (proxy != null) timeoutSeconds * 3 else timeoutSeconds return rootClient .newBuilder() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactoryForRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactoryForRelays.kt index 1729027c3..556c91bf0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactoryForRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactoryForRelays.kt @@ -37,6 +37,7 @@ class OkHttpClientFactoryForRelays( const val DEFAULT_IS_MOBILE: Boolean = false const val DEFAULT_TIMEOUT_ON_WIFI_SECS: Int = 10 const val DEFAULT_TIMEOUT_ON_MOBILE_SECS: Int = 30 + const val WEBSOCKET_PING_INTERVAL_SECS: Long = 120 private fun isEmulator(): Boolean = Build.FINGERPRINT.startsWith("generic") || @@ -76,10 +77,16 @@ class OkHttpClientFactoryForRelays( .addInterceptor(DefaultContentTypeInterceptor(userAgent)) .build() + private var lastProxy: Proxy? = null + fun buildHttpClient( proxy: Proxy?, timeoutSeconds: Int, ): OkHttpClient { + if (proxy != lastProxy) { + rootClient.connectionPool.evictAll() + lastProxy = proxy + } val seconds = if (proxy != null) timeoutSeconds * 3 else timeoutSeconds return rootClient .newBuilder() @@ -87,6 +94,7 @@ class OkHttpClientFactoryForRelays( .connectTimeout(Duration.ofSeconds(seconds.toLong())) .readTimeout(Duration.ofSeconds(seconds.toLong() * 3)) .writeTimeout(Duration.ofSeconds(seconds.toLong() * 3)) + .pingInterval(Duration.ofSeconds(WEBSOCKET_PING_INTERVAL_SECS)) .build() } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt index e83256988..33877f0f8 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt @@ -228,7 +228,7 @@ open class BasicRelayClient( } fun dontTryAgainForALongTime() { - delayToConnectInSeconds = TimeUtils.ONE_DAY + delayToConnectInSeconds = TimeUtils.FIVE_MINUTES } override fun sendOrConnectAndSync(cmd: Command) {