mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-05 19:08:23 +00:00
Merge pull request #466 from greenart7c3/claude/wizardly-ramanujan-i240dv
Implement relay backoff and battery optimization
This commit is contained in:
@@ -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 com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -55,6 +56,21 @@ class NostrClientLoggerListener(
|
||||
private var reconnectDelay = 5_000L
|
||||
private var lastDisconnectTime = 0L
|
||||
|
||||
// Counts the failure against the relay and only schedules a reconnect while it
|
||||
// is still worth retrying. Once a relay is dead, RelayHealthTracker also makes
|
||||
// NotificationSubscription.updateFilter drop it from the subscription relay
|
||||
// set, so Quartz stops opening sockets to it every 30s. The streak resets on a
|
||||
// successful connection (onConnected) or a network change / manual reconnect.
|
||||
private fun scheduleReconnect(relay: NormalizedRelayUrl) {
|
||||
if (!RelayHealthTracker.recordFailure(relay)) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(Amber.TAG, "Relay ${relay.url} marked dead; skipping reconnect")
|
||||
}
|
||||
return
|
||||
}
|
||||
reconnectWithBackoff()
|
||||
}
|
||||
|
||||
private fun reconnectWithBackoff() {
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastDisconnectTime > 60_000) {
|
||||
@@ -115,7 +131,7 @@ class NostrClientLoggerListener(
|
||||
return
|
||||
}
|
||||
|
||||
reconnectWithBackoff()
|
||||
scheduleReconnect(relay.url)
|
||||
super.onCannotConnect(relay, errorMessage)
|
||||
}
|
||||
|
||||
@@ -166,7 +182,7 @@ class NostrClientLoggerListener(
|
||||
return
|
||||
}
|
||||
|
||||
reconnectWithBackoff()
|
||||
scheduleReconnect(relay.url)
|
||||
super.onDisconnected(relay)
|
||||
}
|
||||
|
||||
@@ -179,6 +195,9 @@ 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 (and the subscription) again.
|
||||
RelayHealthTracker.recordSuccess(relay.url)
|
||||
super.onConnected(relay, pingMillis, compressed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.greenart7c3.nostrsigner.relays
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Tracks consecutive connection failures per relay so the app can stop retrying
|
||||
* relays that are permanently unreachable.
|
||||
*
|
||||
* Quartz's relay pool is driven by the relays referenced in active subscriptions:
|
||||
* [NotificationSubscription.updateFilter] re-subscribes every 30s, and any relay
|
||||
* present in that map is (re)connected by the pool. Without this tracker an
|
||||
* offline relay stays in the map forever, so a socket is opened to it on every
|
||||
* refresh, needlessly waking the radio and draining the battery.
|
||||
*
|
||||
* A relay is considered dead after [MAX_RECONNECT_ATTEMPTS] consecutive failures.
|
||||
* Dead relays are excluded from the subscription relay sets so the pool drops
|
||||
* them entirely. The streak is cleared when a relay connects successfully
|
||||
* ([recordSuccess]), and [reset] is called on every OS network change and on a
|
||||
* manual reconnect so previously-dead relays get a fresh chance.
|
||||
*/
|
||||
object RelayHealthTracker {
|
||||
private const val MAX_RECONNECT_ATTEMPTS = 10
|
||||
|
||||
private val failureCounts = ConcurrentHashMap<NormalizedRelayUrl, Int>()
|
||||
|
||||
/** Records a failed connection attempt. Returns true while the relay is still worth retrying. */
|
||||
fun recordFailure(relay: NormalizedRelayUrl): Boolean {
|
||||
val failures = failureCounts.merge(relay, 1, Int::plus) ?: 1
|
||||
return failures <= MAX_RECONNECT_ATTEMPTS
|
||||
}
|
||||
|
||||
/** Clears the failure streak after a successful connection. */
|
||||
fun recordSuccess(relay: NormalizedRelayUrl) {
|
||||
failureCounts.remove(relay)
|
||||
}
|
||||
|
||||
/** True once a relay has failed [MAX_RECONNECT_ATTEMPTS] times in a row without recovering. */
|
||||
fun isDead(relay: NormalizedRelayUrl): Boolean = (failureCounts[relay] ?: 0) > MAX_RECONNECT_ATTEMPTS
|
||||
|
||||
/** Forgets all failure history so every relay is eligible to be retried again. */
|
||||
fun reset() {
|
||||
failureCounts.clear()
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import com.greenart7c3.nostrsigner.BuildConfig
|
||||
import com.greenart7c3.nostrsigner.BuildFlavorChecker
|
||||
import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.okhttp.HttpClientManager
|
||||
import com.greenart7c3.nostrsigner.relays.RelayHealthTracker
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
import kotlin.collections.set
|
||||
@@ -37,6 +38,9 @@ class ConnectivityService : Service() {
|
||||
if (Amber.instance.settings.killSwitch.value) return
|
||||
|
||||
if (lastNetwork != null && lastNetwork != network) {
|
||||
// New network: give previously-dead relays a fresh chance. The
|
||||
// 30s updateFilter tick re-adds them to the subscription set.
|
||||
RelayHealthTracker.reset()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (!Amber.instance.client.isActive()) {
|
||||
Amber.instance.client.connect()
|
||||
@@ -58,6 +62,10 @@ class ConnectivityService : Service() {
|
||||
if (Amber.instance.settings.killSwitch.value) return
|
||||
|
||||
val changed = Amber.instance.updateNetworkCapabilities(networkCapabilities)
|
||||
if (changed) {
|
||||
// Transport changed (e.g. wifi <-> mobile): retry dead relays.
|
||||
RelayHealthTracker.reset()
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d(
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.util.Log
|
||||
import com.greenart7c3.nostrsigner.Amber
|
||||
import com.greenart7c3.nostrsigner.BuildFlavorChecker
|
||||
import com.greenart7c3.nostrsigner.LocalPreferences
|
||||
import com.greenart7c3.nostrsigner.relays.RelayHealthTracker
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayConnectionListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
@@ -90,11 +91,19 @@ class NotificationSubscription(
|
||||
val connPubKey = conn.localPubKey
|
||||
indexEntries[connPubKey] = LocalKeyAccountIndex.Match(account.npub, conn.localKey)
|
||||
val subKey = "${account.hexKey}_$connPubKey"
|
||||
|
||||
// Exclude relays that have been declared dead so Quartz stops opening
|
||||
// a socket to them on every 30s refresh. They are re-added once
|
||||
// RelayHealthTracker is reset (network change / manual reconnect) or
|
||||
// they connect successfully again.
|
||||
val connRelays = conn.relays.ifEmpty { Amber.instance.getSavedRelays(account) }
|
||||
.filterNot { RelayHealthTracker.isDead(it) }
|
||||
if (connRelays.isEmpty()) continue
|
||||
|
||||
activeSubKeys.add(subKey)
|
||||
if (!subIds.containsKey(subKey)) {
|
||||
subIds[subKey] = UUID.randomUUID().toString()
|
||||
}
|
||||
val connRelays = conn.relays.ifEmpty { Amber.instance.getSavedRelays(account) }
|
||||
client.subscribe(
|
||||
subIds[subKey]!!,
|
||||
connRelays.associateWith {
|
||||
@@ -112,24 +121,27 @@ class NotificationSubscription(
|
||||
|
||||
// Main account subscription only for legacy connections (no localKey)
|
||||
if (hasLegacyConnections) {
|
||||
activeSubKeys.add(account.hexKey)
|
||||
if (!subIds.containsKey(account.hexKey)) {
|
||||
subIds[account.hexKey] = UUID.randomUUID().toString()
|
||||
}
|
||||
val relays = Amber.instance.getSavedRelays(account)
|
||||
client.subscribe(
|
||||
subIds[account.hexKey]!!,
|
||||
relays.associateWith {
|
||||
listOf(
|
||||
Filter(
|
||||
kinds = listOf(NostrConnectEvent.KIND),
|
||||
tags = mapOf("p" to listOf(account.hexKey)),
|
||||
limit = 1,
|
||||
since = since,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.filterNot { RelayHealthTracker.isDead(it) }
|
||||
if (relays.isNotEmpty()) {
|
||||
activeSubKeys.add(account.hexKey)
|
||||
if (!subIds.containsKey(account.hexKey)) {
|
||||
subIds[account.hexKey] = UUID.randomUUID().toString()
|
||||
}
|
||||
client.subscribe(
|
||||
subIds[account.hexKey]!!,
|
||||
relays.associateWith {
|
||||
listOf(
|
||||
Filter(
|
||||
kinds = listOf(NostrConnectEvent.KIND),
|
||||
tags = mapOf("p" to listOf(account.hexKey)),
|
||||
limit = 1,
|
||||
since = since,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,16 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.greenart7c3.nostrsigner.Amber
|
||||
import com.greenart7c3.nostrsigner.relays.RelayHealthTracker
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ReconnectReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Amber.instance.applicationIOScope.launch {
|
||||
Amber.instance.reconnect()
|
||||
// Manual reconnect: clear dead-relay state and re-run the filters so any
|
||||
// relay previously dropped from the pool is re-added before reconnecting.
|
||||
RelayHealthTracker.reset()
|
||||
Amber.instance.checkForNewRelaysAndUpdateAllFilters(shouldReconnect = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user