From 8b3ddbab94609c9c3d9a9d08cce3b9aab6459f60 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 11:41:44 +0000 Subject: [PATCH] Reduce battery drain from relay reconnects and websocket pings Stop retrying permanently dead relays: track per-relay consecutive connection failures and stop scheduling reconnects once a relay exceeds MAX_RECONNECT_ATTEMPTS. The streak resets on a successful connection, and dead relays still get a fresh chance on the next network change since the ConnectivityService callback calls client.connect() directly. Previously a single unreachable relay kept re-triggering the global reconnect every 60s forever, waking the radio. Increase the OkHttp websocket ping interval from 30s to 90s so the always-on relay connection wakes the radio less often. https://claude.ai/code/session_01SLv8ZEooFiXErAvpARtz2Y --- .../nostrsigner/okhttp/HttpClientManager.kt | 9 ++++- .../relays/NostrClientLoggerListener.kt | 38 ++++++++++++++++++- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/okhttp/HttpClientManager.kt b/app/src/main/java/com/greenart7c3/nostrsigner/okhttp/HttpClientManager.kt index 7e74f3e5..a162d8d9 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/okhttp/HttpClientManager.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/okhttp/HttpClientManager.kt @@ -46,8 +46,13 @@ object HttpClientManager { val DEFAULT_TIMEOUT_ON_WIFI: Duration = Duration.ofSeconds(10L) val DEFAULT_TIMEOUT_ON_MOBILE: Duration = Duration.ofSeconds(30L) - /** How often OkHttp should send WebSocket pings to detect half-closed connections. */ - private val PING_INTERVAL: Duration = Duration.ofSeconds(30L) + /** + * How often OkHttp should send WebSocket pings to detect half-closed connections. + * Kept relatively long to avoid waking the radio every few seconds on the + * always-on relay connection; it only needs to be short enough to surface a + * silent half-close as an explicit failure within a reasonable window. + */ + private val PING_INTERVAL: Duration = Duration.ofSeconds(90L) private var defaultTimeout = DEFAULT_TIMEOUT_ON_WIFI private var defaultHttpClient: OkHttpClient? = null diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/relays/NostrClientLoggerListener.kt b/app/src/main/java/com/greenart7c3/nostrsigner/relays/NostrClientLoggerListener.kt index be90c564..b7300039 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/relays/NostrClientLoggerListener.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/relays/NostrClientLoggerListener.kt @@ -17,6 +17,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.OkMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd +import java.util.concurrent.ConcurrentHashMap import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -55,6 +56,28 @@ class NostrClientLoggerListener( private var reconnectDelay = 5_000L private var lastDisconnectTime = 0L + // Per-relay consecutive connection-failure counts. A relay that fails to + // connect MAX_RECONNECT_ATTEMPTS times in a row is treated as permanently + // dead and stops scheduling reconnects, so an unreachable relay can't keep + // waking the radio every minute forever and draining the battery. The count + // is reset whenever the relay connects successfully (onConnected), and a dead + // relay still gets a fresh chance on the next OS network change, because the + // ConnectivityService network callback calls client.connect(), which retries + // every relay regardless of this backoff state. + private val failureCounts = ConcurrentHashMap() + + private fun scheduleReconnect(relayUrl: String) { + val failures = (failureCounts[relayUrl] ?: 0) + 1 + failureCounts[relayUrl] = failures + if (failures > MAX_RECONNECT_ATTEMPTS) { + if (BuildConfig.DEBUG) { + Log.d(Amber.TAG, "Relay $relayUrl marked dead after $failures failed attempts; skipping reconnect") + } + return + } + reconnectWithBackoff() + } + private fun reconnectWithBackoff() { val now = System.currentTimeMillis() if (now - lastDisconnectTime > 60_000) { @@ -115,7 +138,7 @@ class NostrClientLoggerListener( return } - reconnectWithBackoff() + scheduleReconnect(relay.url.url) super.onCannotConnect(relay, errorMessage) } @@ -166,7 +189,7 @@ class NostrClientLoggerListener( return } - reconnectWithBackoff() + scheduleReconnect(relay.url.url) super.onDisconnected(relay) } @@ -179,6 +202,17 @@ class NostrClientLoggerListener( override fun onConnected(relay: IRelayClient, pingMillis: Int, compressed: Boolean) { if (BuildConfig.DEBUG) Log.d(Amber.TAG, "onConnected: ${relay.url.url} ping: ${pingMillis}ms compressed: $compressed") saveLog(relay.url.url, "onConnected", "Connected") + // Relay recovered: clear its failure streak so it is eligible for the + // normal reconnect-with-backoff path again. + failureCounts.remove(relay.url.url) super.onConnected(relay, pingMillis, compressed) } + + companion object { + // After this many consecutive failures a relay is considered permanently + // dead and is no longer scheduled for reconnection until it recovers on a + // network change. With the 60s backoff cap this is roughly 10 minutes of + // retrying before giving up. + private const val MAX_RECONNECT_ATTEMPTS = 10 + } }