Merge pull request #3841 from vitorpamplona/feat/observer-out-of-band

Let a monitor publish what it learned without dialling
This commit is contained in:
Vitor Pamplona
2026-08-01 09:49:03 -04:00
committed by GitHub
2 changed files with 88 additions and 0 deletions
@@ -239,6 +239,44 @@ class RelayObserver : RelayConnectionListener {
}
}
/**
* Record a measurement taken OUTSIDE the websocket client — a TCP probe, a
* DNS failure, a host struck out after repeated silence.
*
* This class is a [RelayConnectionListener], so on its own it can only report
* on relays something opened a websocket to. On a large fan-out that is a
* small minority, and it is the wrong minority: the cheap checks that decide
* NOT to dial are precisely the ones that learn a relay is gone, and their
* findings had nowhere to go. Measured on a 16,507-relay list — 104 records
* published, because everything else was ruled out before the client saw it.
*
* A monitor that only reports what it happened to connect to is not a census.
*
* [rttOpenMs] is whatever was actually measured; null means reachable with no
* timing, and no timing is ever invented.
*/
fun record(
relay: NormalizedRelayUrl,
reachable: Boolean,
rttOpenMs: Long? = null,
error: String? = null,
) {
val o = seen.getOrPut(relay) { Observation(relay) }
if (reachable) {
o.reachable = true
o.error = null
// Kept on the Observation, which is never removed — only marked
// reported — so a measurement survives every later flush.
rttOpenMs?.let { o.rttOpenMs = it }
} else {
// Same rule as onCannotConnect: a relay that answered earlier is not
// demoted by one failed probe. The writer decides what record that
// becomes, and "answered, then a probe failed" is not "dead".
o.error = (error ?: "unreachable").take(MAX_TEXT)
}
o.touch()
}
/**
* Everything observed since the last call, marked reported as it is read.
*
@@ -200,6 +200,56 @@ class RelayObserverTest {
assertEquals(first.rttOpenMs, second.rttOpenMs, "the last real measurement still stands")
}
// ---- findings from outside the websocket client ------------------------
@Test
fun `a probe failure is published even though nothing was dialled`() {
// The cheap checks that decide NOT to open a websocket are exactly the
// ones that learn a relay is gone. Without a way in, a listener-only
// observer reports on the small minority it happened to connect to —
// 104 records out of a 16,507-relay list — which is not a census.
val o = RelayObserver()
o.record(url, reachable = false, error = "nodename nor servname provided")
val obs = o.only()
assertFalse(obs.reachable)
assertEquals("nodename nor servname provided", obs.error)
assertNull(obs.rttOpenMs, "a failed probe times nothing")
}
@Test
fun `a probe that connected reports its measured time or none at all`() {
val timed = RelayObserver()
timed.record(url, reachable = true, rttOpenMs = 42)
assertEquals(42L, timed.only().rttOpenMs)
val untimed = RelayObserver()
untimed.record(url, reachable = true)
val obs = untimed.only()
assertTrue(obs.reachable)
assertNull(obs.rttOpenMs, "reachable without a timing must not invent one")
}
@Test
fun `a failed probe does not demote a relay that already answered`() {
// Same rule the connection path follows: one bad probe is not death, and
// only the writer decides what record a mixed history becomes.
val o = RelayObserver()
o.onConnecting(client(url))
o.onConnected(client(url), 1, true)
o.record(url, reachable = false, error = "connect timeout")
assertTrue(o.only().reachable, "it answered; a later probe failure does not erase that")
}
@Test
fun `an out-of-band finding is reported once like any other`() {
val o = RelayObserver()
o.record(url, reachable = false, error = "refused")
assertEquals(1, o.collectUnreported().size)
assertEquals(0, o.collectUnreported().size, "nothing new to say")
}
@Test
fun `each relay is observed on its own`() {
val o = RelayObserver()