mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
Let a monitor publish what it learned without dialling
RelayObserver 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 — a TCP probe, a DNS failure, a host struck out after repeated silence — 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. Everything else was ruled out before the client ever saw it, so the monitor had nothing to say about 99% of the relays it had just formed an opinion on. record() takes those findings. Same rules as the connection path — a relay that answered is not demoted by one failed probe, and a reachable relay with no measured time is published with no time rather than a fabricated zero. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
da009f36bd
commit
c3c20c6615
+38
@@ -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.
|
||||
*
|
||||
|
||||
+50
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user