fix: periodic keep-alive to revive relays in long backoff

When a relay enters a long backoff (5 min, e.g. host unreachable or
a server returned an HTTP error during handshake), the per-relay
delayToConnectInSeconds blocks reconnect attempts for up to 5 minutes.
Without a wakeup, nothing inside NostrClient revisits that relay until
the next subscribe/count/publish.

Add a keep-alive coroutine that calls reconnectIfNeedsTo(false) every
60s while the client is active. The per-relay backoff still gates the
actual reconnect, so dead relays are not hammered, but a relay whose
backoff window has elapsed is reconnected within ~60s of becoming
eligible.

The job lives in scope and is cancelled by close().
This commit is contained in:
Claude
2026-05-04 20:28:02 +00:00
parent 575fe952c6
commit 709e254c15
@@ -42,6 +42,7 @@ import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.IO
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
@@ -157,6 +158,27 @@ class NostrClient(
false,
)
/**
* Periodically wakes up disconnected relays. Without this, a relay
* that hit a long backoff (5 min, e.g. host unreachable or a server
* error code) would stay disconnected forever in the absence of any
* subscription change. The per-relay [BasicRelayClient] backoff still
* gates the actual reconnect attempt, so dead relays are not hammered.
*/
private val keepAliveJob =
scope.launch {
while (true) {
delay(KEEP_ALIVE_INTERVAL_MS)
if (this@NostrClient.isActive) {
relayPool.reconnectIfNeedsTo(ignoreRetryDelays = false)
}
}
}
companion object {
private const val KEEP_ALIVE_INTERVAL_MS = 60_000L
}
override fun reconnect(
onlyIfChanged: Boolean,
ignoreRetryDelays: Boolean,