Rebuild: give up stalled requests, not slow ones
Test and deploy / test (push) Successful in 24s
Test and deploy / deploy (push) Successful in 56s

a.b.0day.stuffz sends steadily but slowly (~400 kB a minute, 5-7
minutes per 100k chunk): the fixed deadline (90 s, doubling up to 360 s)
gave its chunks up again and again, and two of its ranges' worker
streams timed out. A stall is a request that got less than
NNTP_STALL_KB_PER_MINUTE (100) in the last minute (the stuck replies
got ~25 kB a minute); then its connection is closed and the chunk
fetched again. Replaces NNTP_CHUNK_DEADLINE_SECONDS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daan MeijerandClaude Opus 5.5 committed 2026-10-10 16:51:55 +02:00
1 parent dbccec932c
commit 887df12ead
3 files changed
+29 -24

No files matched your search

+20 -15
View File
@@ -16,6 +16,10 @@ interface Target {
total: number
}
/** A request's bytes in are sampled this often; STALL_SAMPLES of them make the minute a stall is judged over. */
const STALL_SAMPLE_MS = 15_000
const STALL_SAMPLES = 60_000 / STALL_SAMPLE_MS
/** Connection ages the report counts slow first requests by, in minutes. */
const AGE_BUCKETS = [2, 10, Infinity]
@@ -59,8 +63,6 @@ export class Prefetcher {
private fetchingNow = 0
private writingNow = 0
private sampler?: NodeJS.Timeout
/** Deadlines a chunk hit, by range and chunk start: each doubles its next deadline, so a slow chunk still comes. */
private lateChunks = new Map<string, number>()
/** Requests in flight: what, since when, on which connection (the report names the oldest). */
private inFlight = new Set<{ rangeId: number; from: number; to: number; since: number; client: XoverClient }>()
@@ -88,7 +90,7 @@ export class Prefetcher {
idle: 0,
inFlight: 0,
inFlightMin: Infinity,
/** Requests given up at nntpChunkDeadlineMs. */
/** Requests given up as stalled (nntpStallBytesPerMinute). */
deadlines: 0,
ranges: 0,
groups: 0,
@@ -191,15 +193,19 @@ export class Prefetcher {
if (first) age = NntpPool.ageOf(c)
const entry = { rangeId, from: span.from, to: span.to, since: Date.now(), client: c }
this.inFlight.add(entry)
// Bytes in, sampled every STALL_SAMPLE_MS: less than nntpStallBytesPerMinute over the last minute is a stall.
let late = false
const key = `${rangeId}:${span.from}`
const deadlineMs = coordinatorConfig.nntpChunkDeadlineMs * 2 ** Math.min(this.lateChunks.get(key) ?? 0, 4)
const deadline = setTimeout(() => {
late = true
this.stats.deadlines++
this.lateChunks.set(key, (this.lateChunks.get(key) ?? 0) + 1)
c.close()
}, deadlineMs)
const samples: number[] = []
const watch = setInterval(() => {
samples.push(c.buffered)
if (samples.length <= STALL_SAMPLES) return
const minuteAgo = samples.shift()!
if (c.buffered - minuteAgo < coordinatorConfig.nntpStallBytesPerMinute) {
late = true
this.stats.deadlines++
c.close()
}
}, STALL_SAMPLE_MS)
let data: Buffer
let groupMs = 0
try {
@@ -209,12 +215,11 @@ export class Prefetcher {
data = await c.xoverRaw(span.from, span.to)
} catch (error) {
if (!late) throw error
throw new Error(`Chunk ${span.from}–${span.to} of range ${rangeId} not in after ${deadlineMs / 1000} s; fetching it again`)
throw new Error(`Chunk ${span.from}–${span.to} of range ${rangeId} stalled (under ${coordinatorConfig.nntpStallBytesPerMinute / 1000} kB in a minute, ${Math.round(c.buffered / 1000)} kB in); fetching it again`)
} finally {
clearTimeout(deadline)
clearInterval(watch)
this.inFlight.delete(entry)
}
this.lateChunks.delete(key)
if (!cancelled) {
const t = c.lastTiming
this.stats.requests++
@@ -294,7 +299,7 @@ export class Prefetcher {
`loops ${avg(st.fetching).toFixed(1)} fetching, ${avg(st.writing).toFixed(1)} writing, ${avg(st.idle).toFixed(1)} idle; ` +
`a request: GROUP ${per(st.groupMs)} s, XOVER reply ${per(st.replyMs)} s, transfer ${per(st.bodyMs)} s; ` +
`${st.silent} of ${requests} transfers silent >5 s; writing ${(st.writeMs / Math.max(1, chunks)).toFixed(0)} ms a chunk; ` +
`${st.deadlines} given up after ${coordinatorConfig.nntpChunkDeadlineMs / 1000} s; ${this.oldest()}.`
`${st.deadlines} stalled (under ${coordinatorConfig.nntpStallBytesPerMinute / 1000} kB a minute); ${this.oldest()}.`
)
}
this.stats = this.freshStats()