fix(network): preserve TCP reconnect backoff on short sessions (#5893)

This commit is contained in:
Jeremiah K
2026-06-21 17:36:57 -05:00
committed by GitHub
parent 0ce0c286cb
commit bf2338ce4e
2 changed files with 78 additions and 4 deletions

View File

@@ -35,6 +35,16 @@ import java.net.SocketTimeoutException
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
/**
* Decides whether to reset the reconnect backoff based on session data and uptime.
*
* Only sessions that lasted at least [thresholdMs] with actual data exchange are considered stable enough to warrant a
* backoff reset. Short sessions — e.g., an ESP32 dumping config then closing the socket — keep the growing backoff so
* the radio has time to recover between attempts.
*/
internal fun shouldResetBackoff(hadData: Boolean, sessionUptimeMs: Long, thresholdMs: Long): Boolean =
hadData && sessionUptimeMs >= thresholdMs
/**
* Shared JVM TCP transport for Meshtastic radios.
*
@@ -76,6 +86,14 @@ class TcpTransport(
const val SOCKET_RETRIES = 18 // 18 * 5s = 90s inactivity before disconnect
const val TIMEOUT_LOG_INTERVAL = 5
private const val MILLIS_PER_SECOND = 1_000L
/**
* Minimum session duration for backoff to reset. Sessions shorter than this that ended in peer-EOF are treated
* as transient firmware-side disconnects (e.g., ESP32 light sleep closing the TCP PhoneAPI session after a
* config dump) and do NOT reset the backoff — the growing delay gives the radio time to recover between
* attempts instead of hammering it at 1 Hz.
*/
const val SHORT_SESSION_THRESHOLD_MS = 30_000L
}
private val codec =
@@ -176,12 +194,18 @@ class TcpTransport(
false
}
// Reset backoff after a connection that successfully exchanged data,
// so transient firmware-side disconnects recover quickly.
if (hadData) {
Logger.d { "$logTag: [$address] Resetting backoff after successful data exchange" }
// Reset backoff only after a session that lasted long enough to indicate a real connection,
// not a short config-dump-then-EOF from a sleeping radio. Short sessions keep the backoff
// growing so the radio has time to recover between reconnect attempts.
val sessionUptime = if (connectionStartTime > 0) nowMillis - connectionStartTime else 0
if (shouldResetBackoff(hadData, sessionUptime, SHORT_SESSION_THRESHOLD_MS)) {
Logger.d { "$logTag: [$address] Resetting backoff after successful data exchange (${sessionUptime}ms)" }
retryCount = 1
backoff = MIN_BACKOFF_MILLIS
} else if (hadData) {
Logger.d {
"$logTag: [$address] Short session (${sessionUptime}ms) — keeping backoff at ${backoff / MILLIS_PER_SECOND}s"
}
}
val delaySec = backoff / MILLIS_PER_SECOND

View File

@@ -0,0 +1,50 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.network.transport
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class ShouldResetBackoffTest {
private val threshold = 30_000L
@Test
fun `no data never resets backoff`() {
assertFalse(shouldResetBackoff(hadData = false, sessionUptimeMs = 0, thresholdMs = threshold))
assertFalse(shouldResetBackoff(hadData = false, sessionUptimeMs = 60_000, thresholdMs = threshold))
}
@Test
fun `data below threshold does not reset backoff`() {
assertFalse(shouldResetBackoff(hadData = true, sessionUptimeMs = 0, thresholdMs = threshold))
assertFalse(shouldResetBackoff(hadData = true, sessionUptimeMs = 1_000, thresholdMs = threshold))
assertFalse(shouldResetBackoff(hadData = true, sessionUptimeMs = 29_999, thresholdMs = threshold))
}
@Test
fun `data at exactly threshold resets backoff`() {
assertTrue(shouldResetBackoff(hadData = true, sessionUptimeMs = 30_000, thresholdMs = threshold))
}
@Test
fun `data above threshold resets backoff`() {
assertTrue(shouldResetBackoff(hadData = true, sessionUptimeMs = 30_001, thresholdMs = threshold))
assertTrue(shouldResetBackoff(hadData = true, sessionUptimeMs = 600_000, thresholdMs = threshold))
}
}