Seed: finished ranges marked done; relay XOVER timeout 10 minutes
- A range reported to its end but not finished (its node went away at the very end) was leased again with start past end; nodes asked for "from 49208000000 to 49207999999", got 400, and the range failed after five leases. The orchestrator now marks such ranges done before leasing, and runRange reports them done without fetching. - Some old ranges come so slowly that one XOVER of 20k headers takes over two minutes: five ranges failed on "NNTP reply timed out". The relay pool's timeout is SEED_NNTP_TIMEOUT_MS (default 10 minutes, the node's HTTP timeout). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ef09c07d6e
commit
530a752cca
7 files changed
+39
-1
No files matched your search
@@ -61,7 +61,7 @@ class SeedFetchPool {
|
||||
servername: nntpConfig.tlsServername,
|
||||
user: nntpConfig.user,
|
||||
password: nntpConfig.password?.release(),
|
||||
timeoutMs: nntpConfig.commandTimeoutMs,
|
||||
timeoutMs: indexerConfig.seed.nntpTimeoutMs,
|
||||
compress: true,
|
||||
})
|
||||
return { client, usedAt: Date.now() }
|
||||
|
||||
@@ -60,6 +60,12 @@ class SeedOrchestrator {
|
||||
*/
|
||||
async lease(nodeId: number): Promise<SeedLease | null> {
|
||||
if ((await BackfillService.room()) !== true) return null
|
||||
// Fully reported but never finished (the node went away at the very end): nothing left to fetch.
|
||||
await db.rawQuery(
|
||||
`UPDATE seed_ranges SET status = 'done', done_at = now(), node_id = NULL
|
||||
WHERE next_id > end_id AND (status = 'queued' OR (status = 'leased' AND heartbeat_at < now() - make_interval(secs => ?)))`,
|
||||
[LEASE_TTL_SECONDS]
|
||||
)
|
||||
// Ranges that broke node after node stop being handed out.
|
||||
await db.rawQuery(
|
||||
`UPDATE seed_ranges SET status = 'failed', error = coalesce(error, 'lease expired too often')
|
||||
|
||||
@@ -66,6 +66,8 @@ export default {
|
||||
seed: {
|
||||
/** NNTP connections the seed API fetches with for relay nodes (CONCEPT.md D38); 0: relay off. */
|
||||
nntpConnections: env.get('SEED_NNTP_CONNECTIONS', 0),
|
||||
/** A relay XOVER may take this long: some old ranges come at ~1k headers/s (and slower when cold). */
|
||||
nntpTimeoutMs: env.get('SEED_NNTP_TIMEOUT_MS', 10 * 60 * 1000),
|
||||
},
|
||||
|
||||
backfill: {
|
||||
|
||||
@@ -39,6 +39,11 @@ export async function runRange(options: RunRangeOptions): Promise<void> {
|
||||
const idle = options.idle ?? 5
|
||||
const submitEvery = options.submitEvery ?? 5
|
||||
const aggregator = new FileAggregator<IngestSegment>()
|
||||
// A range fully reported before but not finished (its node went away right at the end) resumes past its end.
|
||||
if (options.start > options.end) {
|
||||
await options.sink(emptyBatch(options.end + 1))
|
||||
return
|
||||
}
|
||||
await options.client.group(options.group)
|
||||
|
||||
const fetch = (from: number) => {
|
||||
|
||||
@@ -50,6 +50,7 @@ export default await Env.create(new URL('../', import.meta.url), {
|
||||
NNTP_COMMAND_TIMEOUT_MS: Env.schema.number.optional(),
|
||||
NNTP_TLS_SERVERNAME: Env.schema.string.optional(),
|
||||
SEED_NNTP_CONNECTIONS: Env.schema.number.optional(),
|
||||
SEED_NNTP_TIMEOUT_MS: Env.schema.number.optional(),
|
||||
|
||||
// Indexer (config/indexer.ts)
|
||||
BACKFILL_MAX_QUEUED: Env.schema.number.optional(),
|
||||
|
||||
@@ -128,4 +128,11 @@ test.group('Seed API', (group) => {
|
||||
indexerConfig.seed.nntpConnections = connections
|
||||
}
|
||||
})
|
||||
|
||||
test('a range reported to its end but never finished is marked done, not leased', async ({ client, assert }) => {
|
||||
const token = await SeedOrchestrator.issueToken('late-node')
|
||||
const [range] = await db.table('seed_ranges').insert({ group_name: 'alt.binaries.test', start_id: 1, end_id: 100, next_id: 101 }).returning('id')
|
||||
;(await client.post('/api/v1/seed/lease').headers({ authorization: `Bearer ${token}` })).assertStatus(204)
|
||||
assert.equal((await db.from('seed_ranges').where('id', range.id).first()).status, 'done')
|
||||
})
|
||||
})
|
||||
@@ -37,3 +37,20 @@ test.group('aggregate / FileAggregator', () => {
|
||||
assert.equal(aggregator.openFiles, 0)
|
||||
})
|
||||
})
|
||||
|
||||
test.group('seed / runRange', () => {
|
||||
test('a range already past its end only reports that it is done', async ({ assert }) => {
|
||||
const { runRange } = await import('#lib/seed/runner')
|
||||
const batches: any[] = []
|
||||
await runRange({
|
||||
client: { group: async () => assert.fail('no GROUP'), xover: async () => assert.fail('no XOVER') },
|
||||
group: 'a.b.test',
|
||||
start: 101,
|
||||
end: 100,
|
||||
sink: async (batch) => {
|
||||
batches.push(batch)
|
||||
},
|
||||
})
|
||||
assert.deepEqual(batches.map((b) => [b.next, b.headers, b.files.length]), [[101, 0, 0]])
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user