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>
88 lines
4.3 KiB
TypeScript
88 lines
4.3 KiB
TypeScript
import env from '#start/env'
|
||
import app from '@adonisjs/core/services/app'
|
||
|
||
/** The coordinator (REBUILD.md, part 1). */
|
||
export default {
|
||
/** Articles per range. */
|
||
rangeArticles: env.get('RANGE_ARTICLES', 2_000_000),
|
||
/**
|
||
* Articles per XOVER chunk (spool files, stream frames). Eweka takes
|
||
* ~1.5–2 s per command whatever its size: one connection read 9–15k
|
||
* lines/s in chunks of 25k, ~20k in 50k, ~30k in 100k and 200k (LOG.md
|
||
* 2026-10-09).
|
||
*/
|
||
chunkArticles: env.get('CHUNK_ARTICLES', 100_000),
|
||
/**
|
||
* The live tail (REBUILD.md, coordinator): new articles become ranges
|
||
* every `everySeconds` (one LIST ACTIVE per name prefix), leased and
|
||
* fetched before any history. A group gets a tail range once it has
|
||
* `minArticles` new (a chunk) or its oldest unplanned article waited
|
||
* `maxDelayMinutes`, so a quiet group doesn't make a range every round.
|
||
*/
|
||
tail: {
|
||
everySeconds: env.get('TAIL_EVERY_SECONDS', 120),
|
||
minArticles: env.get('TAIL_MIN_ARTICLES', 100_000),
|
||
maxDelayMinutes: env.get('TAIL_MAX_DELAY_MINUTES', 15),
|
||
/** Before the history's 0 (oldest) … 1000 (newest). */
|
||
priority: -1,
|
||
},
|
||
/** A leased range without a heartbeat for this long goes back to the queue. */
|
||
leaseTtlSeconds: env.get('LEASE_TTL_SECONDS', 15 * 60),
|
||
/** NNTP connections the coordinator fetches headers with. */
|
||
nntpConnections: env.get('NNTP_CONNECTIONS', 4),
|
||
/**
|
||
* A reply with no data for this long fails, and its connection is replaced.
|
||
* Eweka sometimes stops answering on long-lived connections (all 40 quiet,
|
||
* a fresh one answers in seconds; LOG.md 2026-10-09 (14)): at 10 minutes
|
||
* the prefetcher nearly stopped. Data arrives within seconds otherwise.
|
||
*/
|
||
nntpTimeoutMs: env.get('NNTP_TIMEOUT_MS', 2 * 60 * 1000),
|
||
/**
|
||
* A request that got less than this in the last minute (kB) is given up,
|
||
* its connection closed, and the chunk fetched again: a reply that trickles
|
||
* (6,204 → 6,229 kB in a minute) never goes quiet long enough for
|
||
* nntpTimeoutMs, and kept a worker waiting on one chunk for an hour. Not a
|
||
* deadline: some groups send steadily but slowly (a.b.0day.stuffz ~400 kB a
|
||
* minute, 5–7 minutes a chunk), and a fixed 90–360 s gave those up again
|
||
* and again (LOG.md 2026-10-10 (3)).
|
||
*/
|
||
nntpStallBytesPerMinute: env.get('NNTP_STALL_KB_PER_MINUTE', 100) * 1000,
|
||
/**
|
||
* A chunk not fetched after this long is asked again on another
|
||
* connection; the first answer wins and the other connection is closed.
|
||
* Eweka stalls single requests: the same chunk took 27–52 s on one
|
||
* connection and 1–9 s on another at the same moment; median ~3 s
|
||
* (LOG.md 2026-10-09 (15)). Measured in production (40 + 6 connections vs
|
||
* 40, 15 min each): 280k vs 273k articles/s, within the noise; the second
|
||
* request won half the time. Off by default.
|
||
*/
|
||
nntpHedgeAfterMs: env.get('NNTP_HEDGE_AFTER_SECONDS', 10) * 1000,
|
||
/** Of nntpConnections, kept for those second requests (the prefetcher's loops are the rest). 0: no hedging. */
|
||
nntpHedgeConnections: env.get('NNTP_HEDGE_CONNECTIONS', 0),
|
||
/**
|
||
* A connection is closed after this long (when it's free), and a fresh one
|
||
* opened. 0: never. Requests were as often slow (>10 s) on connections
|
||
* under 2 minutes old as on ones over 10 (~25–35%; LOG.md 2026-10-09 (16)).
|
||
*/
|
||
nntpConnectionMaxMs: env.get('NNTP_CONNECTION_MAX_MINUTES', 0) * 60 * 1000,
|
||
spool: {
|
||
dir: env.get('SPOOL_DIR', app.tmpPath('spool')),
|
||
/** Ranges kept fetched ahead, per active lease. */
|
||
aheadPerLease: env.get('SPOOL_AHEAD_PER_LEASE', 4),
|
||
/** At least this many ranges ahead, so the first leases start at once. */
|
||
minAhead: env.get('SPOOL_MIN_AHEAD', 2),
|
||
maxBytes: env.get('SPOOL_MAX_GB', 10) * 1e9,
|
||
/**
|
||
* At most this many connections on one range, and on one group, at a
|
||
* time (0: no limit): spreads the requests over ranges and groups instead
|
||
* of all on the next chunks of the first ranges. Homelab, 40 connections,
|
||
* 12 min each, alternated: unlimited (on ~10 ranges) 281k and 167k
|
||
* articles/s, 2 per range and 4 per group (on ~22) 319k and 296k, with
|
||
* shorter chunk times (11.5 vs 13–24 s; LOG.md 2026-10-09 (18)). Needs
|
||
* targets for it: aheadPerLease 4.
|
||
*/
|
||
perRange: env.get('PREFETCH_PER_RANGE', 2),
|
||
perGroup: env.get('PREFETCH_PER_GROUP', 4),
|
||
},
|
||
}
|