Ask the relay to hold a poll open

The relay has always been willing to hold an inbox request until
something arrives, and the Android client has never asked it to. The
constant naming the hold was declared in MessengerService and read
nowhere, and the comment next to the 500 ms gap already described a
long poll that was not happening.

What that cost, measured against production: two devices sent 1,177
inbox requests in eight minutes, every one of them held open by the
relay for about 30 ms and answered "nothing". Every message and every
call signal waited out the gap to the next poll. Someone had already
noticed the effect on calls and shortened the gap to a second while one
was being set up, which made the polling denser without making the
signal arrive any sooner.

So `wait` reaches the wire. The signed-in mailbox is asked to hold; a
backup home never is, because two held reads in a row would put a
backup mailbox's latency in front of the one nearly all mail arrives
at. A held request needs longer than the ordinary call timeout, so it
gets a clone of the client with room for the hold.

Both loops now pace on whether the relay held the request rather than
on a fixed gap: a poll that took a while was itself the wait, and the
next one goes straight back out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-10 08:18:34 -04:00
co-authored by Claude Opus 5
parent 57411ea703
commit 6584d313ca
6 changed files with 198 additions and 21 deletions
@@ -176,7 +176,8 @@ class MessengerService : Service() {
}
while (true) {
val touched = runCatching { s.receive() }.getOrDefault(emptySet())
val started = System.currentTimeMillis()
val touched = runCatching { s.receive(WAIT_SECONDS) }.getOrDefault(emptySet())
if (touched.isNotEmpty()) notifyMessages(touched.size)
// Reply reminders fire from THIS loop, after `receive()`, and the
// order matters: an inbound message clears the reminder inside
@@ -185,10 +186,13 @@ class MessengerService : Service() {
fireDueReplyReminders(s)
// An unanswered ring has to stop ringing.
expireStaleRing()
// A short gap, not a poll interval: `receive()` is held open by the
// relay for WAIT_SECONDS, so this only paces a relay that answers
// immediately.
delay(500)
// A short gap, not a poll interval: `receive()` asks the relay to
// hold the request open for WAIT_SECONDS, so a poll that took a
// while was already the wait, and going straight back is what keeps
// the mailbox continuously watched. Only a relay that answered at
// once, one that has no hold to offer, gets paced.
val held = System.currentTimeMillis() - started >= 2_000
if (!held) delay(500)
}
}
@@ -3399,6 +3399,7 @@ class MessengerViewModel(
runCatching { session?.conversations?.ensureDeliveryKey() }
}
while (true) {
val tick = System.currentTimeMillis()
// An offer the background service consumed while the app
// was off screen.
PendingIncomingCall.take()?.let { parked ->
@@ -3421,7 +3422,7 @@ class MessengerViewModel(
// here.
val received =
withContext(Dispatchers.IO) {
val got = runCatching { current.receive() }
val got = runCatching { current.receive(POLL_WAIT_SECONDS) }
// Cached plaintext for anything nothing refers
// to any more, an expired message's photo, a
// story past its lifetime.
@@ -3536,9 +3537,12 @@ class MessengerViewModel(
}
}
}
// A call being set up lives or dies on signalling latency,
// the 5 s cadence made every answer feel geological, and
// stacked with the same cadence on the other end.
// A call being set up lives or dies on signalling latency.
// The relay is asked to hold each poll open, so a tick that
// took a while was the wait itself and the next one goes
// straight back out; only a relay that answered at once
// needs pacing, and then a call being set up cannot afford
// the ordinary cadence.
val settingUp =
when (calls?.state) {
CallManager.State.DIALLING,
@@ -3548,7 +3552,12 @@ class MessengerViewModel(
else -> false
}
kotlinx.coroutines.delay(if (settingUp) CALL_SETUP_POLL_MS else POLL_INTERVAL_MS)
val held = System.currentTimeMillis() - tick >= 2_000
if (!held) {
kotlinx.coroutines.delay(
if (settingUp) CALL_SETUP_POLL_MS else POLL_INTERVAL_MS,
)
}
}
}
}
@@ -3570,6 +3579,13 @@ class MessengerViewModel(
const val CALLS_SEEN_AT = "calls_seen_at"
const val POLL_INTERVAL_MS = 5_000L
const val CALL_SETUP_POLL_MS = 1_000L
/**
* How long the relay may hold a poll open. Asking for a hold is what
* turns the cadence above into a ceiling rather than the latency every
* message and call signal actually pays.
*/
const val POLL_WAIT_SECONDS = 20
}
// / The relay said this device was removed.
@@ -1153,8 +1153,8 @@ class Session(
*/
var onCallSignal: ((Long, io.flamenet.e2e.Content.CallSignal, Long) -> Unit)? = null
fun receive(): Set<String> {
val everything = conversations.poll()
fun receive(waitSeconds: Int = 0): Set<String> {
val everything = conversations.poll(waitSeconds = waitSeconds)
// Control traffic consumed during the poll may have carried group-name
// claims.
drainGroupNames()