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
This commit is contained in:
Claude
2026-06-12 11:41:44 +00:00
parent f76b22b254
commit 8b3ddbab94
2 changed files with 43 additions and 4 deletions
@@ -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
@@ -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<String, Int>()
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
}
}