Every held poll on Android was failing at ten seconds
`heldHttp` raised `callTimeout` to the hold plus twenty and left `readTimeout`
at OkHttp's default of TEN SECONDS. A held poll asks the relay to say nothing
for up to twenty. So every one died at ten with a SocketTimeoutException, and
the generous call timeout above it never got the chance to matter.
Nothing said so. The failure was swallowed into a retry, the loop looked busy,
and the only trace was a number on the relay: this device took an average of
112 seconds to collect what was waiting for it, against 5 for an iPhone. A
message written into a connection this end had already abandoned waited for
the next poll instead of arriving.
That is why calling an iPhone took forever, why "connecting..." never
resolved, and why the call then ended: the answer was on the relay the whole
time and the dial guard gave up at forty-five seconds waiting for a fetch that
could not succeed.
Measured on the device, before and after, once the loop was made to say what
it was doing:
vm cycle 10125ms held=20s call=false failed=SocketTimeoutException
vm cycle 20304ms held=20s call=false got=0
TWO POLLS, AND I FIXED THE ONE THAT WAS SWITCHED OFF
`MessengerService` and the view model both poll, and the activity stops the
service on the way in -- "exactly one poller at a time". Placing a call is
exactly when the app is on screen, so the service is stopped and the view
model is the poller. The twenty-second hold during a call was fixed in the
service first. The loop that was running kept it.
Both ask for no hold during a call now.
AND THE LOOP CAN SAY WHAT IT COST
One line per cycle: how long it took, how long it was allowed to hold, whether
a call was up, and what came back or what failed. Two rounds were spent
reading an empty trace as "the loop is not running" when it was "the loop has
no voice", and the bug above was invisible for as long as it was because a
failing poll and a quiet one look identical from outside the process.
1619 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7d8b721a49
commit
7ccec9e3d1
@@ -38,6 +38,21 @@ class RelayClient(
|
||||
http
|
||||
.newBuilder()
|
||||
.callTimeout(MAX_WAIT_SECONDS + 20L, TimeUnit.SECONDS)
|
||||
// AND the read timeout, which is the one that was killing it.
|
||||
//
|
||||
// `callTimeout` bounds the whole call; `readTimeout` bounds the
|
||||
// wait between bytes, and OkHttp defaults it to TEN SECONDS. A held
|
||||
// poll asks the relay to say nothing for up to twenty, so every
|
||||
// single one died at ten with a SocketTimeoutException, long before
|
||||
// the generous call timeout above could matter.
|
||||
//
|
||||
// Nothing said so. The failure was swallowed into a retry, the loop
|
||||
// looked like it was working, and the effect showed up only on the
|
||||
// relay: this device took an average of 112 seconds to collect what
|
||||
// was waiting for it against 5 for an iPhone, because a message
|
||||
// written into a connection this end had already abandoned waited
|
||||
// for the next poll instead.
|
||||
.readTimeout(MAX_WAIT_SECONDS + 20L, TimeUnit.SECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package io.flamenet.client
|
||||
|
||||
import java.io.File
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* A poll that asks the relay to hold must survive the hold.
|
||||
*
|
||||
* `heldHttp` raised `callTimeout` and left `readTimeout` at OkHttp's default
|
||||
* of TEN SECONDS. A held poll asks the relay to stay silent for up to twenty,
|
||||
* so every one died at ten with a SocketTimeoutException, and the generous
|
||||
* call timeout above it never got a chance to matter.
|
||||
*
|
||||
* Measured on a real device before the fix, once the loop was made to say what
|
||||
* it was doing:
|
||||
*
|
||||
* vm cycle 10125ms held=20s call=false failed=SocketTimeoutException
|
||||
* vm cycle 10009ms held=20s call=false failed=SocketTimeoutException
|
||||
* vm cycle 10012ms held=20s call=false failed=SocketTimeoutException
|
||||
*
|
||||
* Every single poll failing, silently, because the failure was swallowed into
|
||||
* a retry and the loop looked busy. It showed up only as a number on the
|
||||
* relay: 112 seconds to collect what was waiting, against 5 for an iPhone.
|
||||
*/
|
||||
class AHeldPollOutlivesItsHoldTest {
|
||||
private val relay =
|
||||
File("src/main/kotlin/io/flamenet/client/RelayClient.kt").readText()
|
||||
|
||||
@Test
|
||||
fun `the held client raises the read timeout, not only the call timeout`() {
|
||||
val block = relay.substringAfter("private val heldHttp").substringBefore("}")
|
||||
assertTrue(
|
||||
block.contains(".readTimeout(MAX_WAIT_SECONDS + 20L, TimeUnit.SECONDS)"),
|
||||
"readTimeout left at OkHttp's 10s default kills every held poll",
|
||||
)
|
||||
assertTrue(
|
||||
block.contains(".callTimeout(MAX_WAIT_SECONDS + 20L, TimeUnit.SECONDS)"),
|
||||
"the call timeout has to clear the hold too",
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Both bounds must exceed the longest hold the client will ask for, or the
|
||||
* transport contradicts the protocol.
|
||||
*/
|
||||
@Test
|
||||
fun `both timeouts clear the longest hold the client asks for`() {
|
||||
assertTrue(relay.contains("const val MAX_WAIT_SECONDS = 20"))
|
||||
assertTrue(
|
||||
relay.contains("waitSeconds.coerceIn(0, MAX_WAIT_SECONDS)"),
|
||||
"the hold is clamped to the same constant the timeouts are built from",
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user