Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ The verdict can also simply be wrong for a while. HyperDHT probes reachability o
at startup — it needs 3 of 5 remote nodes to ping the port back — and on its own
never probes again while the public address is unchanged, so one lost round used to
leave a correctly forwarded relay firewalled until the next restart. The relay now
re-runs the probe itself while firewalled: after 1 minute, then backing off to every
15. A relay that flips to reachable minutes after a restart, with
`reachability re-probe passed` in the log, was that case. It does not apply under
re-runs the probe itself while firewalled: after 10, 20 and 30 seconds, then 1, 2
and 5 minutes, then every 5 minutes. A relay that flips to reachable shortly after
a restart, with `reachability re-probe passed` in the log, was that case. It does not apply under
`MIRALL_RELAY_ASSUME_REACHABLE`, where nothing was probed to begin with.

Two states the page separates that `/readyz` does not:
Expand Down
13 changes: 6 additions & 7 deletions src/reprobe.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,25 @@
// _updateNetworkState is dht-rpc internals: the same call its own tick makes,
// minus the same-host guard. Everything about it is optional, so a dht-rpc that
// drops it costs the self-heal and nothing else.
const INTERVAL_MS = 60_000
const MAX_INTERVAL_MS = 15 * 60_000
// Front-loaded: a port that is open answers within seconds, so the first retries
// come quickly and only a port that really is closed settles into the last wait.
const SCHEDULE_MS = [10_000, 20_000, 30_000, 60_000, 120_000, 300_000]

export function startReprobe (dht, {
onResult = null,
onUnsupported = null,
intervalMs = INTERVAL_MS,
maxIntervalMs = MAX_INTERVAL_MS,
scheduleMs = SCHEDULE_MS,
setTimer = setTimeout,
clearTimer = clearTimeout
} = {}) {
let timer = null
let stopped = false
let wait = intervalMs
let attempt = 0

function schedule () {
if (stopped || !dht.firewalled) return
timer = setTimer(probe, wait)
timer = setTimer(probe, scheduleMs[Math.min(attempt++, scheduleMs.length - 1)])
timer.unref?.()
wait = Math.min(wait * 2, maxIntervalMs)
}

async function probe () {
Expand Down
16 changes: 12 additions & 4 deletions test/unit/reprobe.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,20 @@ test('a firewalled node is re-probed until a probe passes, then left alone', asy
assert.deepEqual(results, [true, true, false])
})

test('the wait doubles up to a ceiling, so a truly closed port is not hammered', async () => {
test('the first retries come quickly, then settle on the last wait', async () => {
// An open port answers within seconds, so waiting a minute for the first retry
// is a minute of red for nothing; a closed one must still not be hammered.
const timers = fakeTimers()
startReprobe(fakeDht(Infinity), { ...timers, intervalMs: 1000, maxIntervalMs: 5000 })
startReprobe(fakeDht(Infinity), timers)
const waits = []
for (let i = 0; i < 5; i++) waits.push(await timers.fire())
assert.deepEqual(waits, [1000, 2000, 4000, 5000, 5000])
for (let i = 0; i < 8; i++) waits.push(await timers.fire())
assert.deepEqual(waits, [10_000, 20_000, 30_000, 60_000, 120_000, 300_000, 300_000, 300_000])
})

test('the schedule can be replaced', async () => {
const timers = fakeTimers()
startReprobe(fakeDht(Infinity), { ...timers, scheduleMs: [5, 7] })
assert.deepEqual([await timers.fire(), await timers.fire(), await timers.fire()], [5, 7, 7])
})

test('a probe that throws does not end the retries', async () => {
Expand Down
Loading