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>
This commit is contained in:
1 parent
fc8ec9ff48
commit
8475969145
7 files changed
+234
-11
No files matched your search
@@ -0,0 +1,51 @@
|
||||
/** 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
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export { XoverClient, parseOverviews, type Overview, type XoverClientOptions } from './xover_client.js'
|
||||
export { parseXrefGroups } from './xref.js'
|
||||
export { parseGroupResponse, type GroupInfo } from './group.js'
|
||||
export { hedge, type Request, type Hedged } from './hedge.js'
|
||||
Reference in new issue
Block a user