NNTP shares: the account's connections shared by priority
Test and deploy / test (push) Successful in 49s
Test and deploy / edge (push) Successful in 1m54s
Test and deploy / deploy (push) Successful in 53s

The coordinator (38) and the prober (8) had a fixed split; with history
paused the coordinator's stood idle while the prober's were its limit.
nntp_shares: each process syncs every 15 s (priority, minimum, what it could
use, what it has open) and gets what it may open: minimums first, then by
priority what each wants, growing only into connections the others closed
or gave up. The pool's size and the prober's lanes follow the grant; the
coordinator doubles its want while work waits for a connection, else asks
for its peak in use and a margin; the prober wants its maximum unless the
queue is dry. Claims scale with the lanes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daan MeijerandClaude Opus 5.5 committed 2026-10-18 11:20:09 +02:00
1 parent 7eba53043a
commit 39480c486f
10 files changed
+373 -75

No files matched your search

+19 -1
View File
@@ -8,6 +8,7 @@ import Settler from '#services/settler'
import RangeBlocks from '#services/range_blocks'
import Leases from '#services/leases'
import NntpPool from '#services/nntp_pool'
import { NntpShare } from '#services/nntp_shares'
import coordinatorConfig from '#config/coordinator'
const TICK_MS = 2000
@@ -43,6 +44,7 @@ export default class CoordinatorRun extends BaseCommand {
const [index, count] = (this.shard ?? '1/1').split('/').map(Number)
if (!(count >= 1 && index >= 1 && index <= count)) throw new Error(`--shard: "i/n" with 1 <= i <= n, not "${this.shard}"`)
const prefetcher = new Prefetcher({ index: index - 1, count })
const share = new NntpShare(count > 1 ? `coordinator ${index}/${count}` : 'coordinator', coordinatorConfig.nntpShares.coordinatorPriority, coordinatorConfig.nntpMinConnections, coordinatorConfig.nntpConnections)
const main = index === 1
let stopping = false
let stopped: () => void
@@ -55,6 +57,21 @@ export default class CoordinatorRun extends BaseCommand {
await done
})
this.logger.info(count > 1 ? `Coordinator started (shard ${index}/${count}${main ? '' : ', prefetching only'}).` : 'Coordinator started.')
// The pool's size: its share of the account's connections, the prober the rest (app/services/nntp_shares.ts).
// Before the first connection: until then it's NNTP_CONNECTIONS.
const first = await NntpPool.share(share)
this.logger.info(`NNTP share: ${first.granted} connections.`)
const sharing = (async () => {
while (!stopping) {
try {
const { was, granted } = await NntpPool.share(share)
if (granted !== was) this.logger.info(`NNTP share: ${granted} connections (was ${was}).`)
} catch (error: any) {
this.logger.error(`NNTP share failed: ${error.message}`)
}
for (let waited = 0; waited < coordinatorConfig.nntpShares.everyMs && !stopping; waited += TICK_MS) await sleep(TICK_MS)
}
})()
// Two loops: the prefetcher's ticks don't wait for merging or planning (a merge round
// of up to MERGE_BUDGET_MS left the connections idle once their targets were fetched).
const prefetching = (async () => {
@@ -126,12 +143,13 @@ export default class CoordinatorRun extends BaseCommand {
}
await sleep(TICK_MS)
}
await Promise.all([prefetching, blocks])
await Promise.all([prefetching, blocks, sharing])
// The chunks in flight, for at most STOP_WAIT_MS (a connection the server holds back can take minutes); then closed.
const waitUntil = Date.now() + STOP_WAIT_MS
while (prefetcher.active > 0 && Date.now() < waitUntil) await sleep(200)
if (prefetcher.active > 0) this.logger.info(`Closing ${prefetcher.active} connections still fetching.`)
await NntpPool.close()
await share.leave().catch(() => {})
this.logger.info('Coordinator stopped.')
stopped!()
await this.terminate()