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>
52 lines
1.8 KiB
TypeScript
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
|
|
}
|
|
}
|