Files
usenet-indexer/lib/nntp/hedge.ts
T
Daan MeijerandClaude Opus 5.5 8475969145
Test and deploy / test (push) Successful in 24s
Test and deploy / deploy (push) Skipped
Rebuild: hedged chunk fetches and connection max age (both off by default)
A chunk not in after NNTP_HEDGE_AFTER_SECONDS (10) is asked again on one
of NNTP_HEDGE_CONNECTIONS spare connections; the first answer wins and
the other connection is closed (lib/nntp/hedge.ts, tested). Connections
can be retired after NNTP_CONNECTION_MAX_MINUTES (±25%). The prefetch
report counts hedges and slow first requests by connection age.

Homelab, 15 min each: hedging on (40 + 6) 280k articles/s, off 273k.
The second request won about half of its races, but ~25-35% of first
requests were slow (>10 s) at every connection age, so neither helps:
Eweka is slow under our load, not on particular connections. Both
off by default; the age statistics stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:13:44 +02:00

52 lines
1.8 KiB
TypeScript

/** A request that can be given up: cancel() stops it (closing its connection, say). */
export interface Request<T> {
done: Promise<T>
cancel(): void
}
export interface Hedged<T> {
value: T
/** The first request took longer than afterMs. */
late: boolean
/** A second request was started. */
hedged: boolean
/** The second request answered first. */
secondWon: boolean
}
/**
* A hedged request: the first one not answered within afterMs is asked again
* (again() may decline with null: no connection free), and the first answer
* wins; the other request is cancelled. A failure only counts once both failed.
*/
export async function hedge<T>(start: () => Request<T>, again: () => Request<T> | null, afterMs: number): Promise<Hedged<T>> {
const first = start()
let timer: NodeJS.Timeout | undefined
const late = await Promise.race([
first.done.then(
() => false,
() => false
),
new Promise<boolean>((resolve) => (timer = setTimeout(() => resolve(true), afterMs))),
])
clearTimeout(timer)
if (!late) return { value: await first.done, late, hedged: false, secondWon: false }
const second = again()
if (!second) return { value: await first.done, late, hedged: false, secondWon: false }
const settled = new Set<Request<T>>()
for (const r of [first, second]) {
r.done.then(
() => settled.add(r),
() => settled.add(r)
)
}
try {
const winner = await Promise.any([first, second].map((r) => r.done.then((value) => ({ r, value }))))
const loser = winner.r === first ? second : first
if (!settled.has(loser)) loser.cancel()
return { value: winner.value, late, hedged: true, secondWon: winner.r === second }
} catch (error) {
throw error instanceof AggregateError ? error.errors[0] : error
}
}