Merge branch 'rebuild' into voyager-3
This commit is contained in:
commit
37750fa4b3
1 file changed
+105
@@ -0,0 +1,105 @@
|
||||
import { BaseCommand, args, flags } from '@adonisjs/core/ace'
|
||||
import type { CommandOptions } from '@adonisjs/core/types/ace'
|
||||
import env from '#start/env'
|
||||
import { XoverClient } from '#lib/nntp/index'
|
||||
|
||||
/**
|
||||
* What the news server does with XOVER spans around an article that made a request hang (ranges:dates: one point
|
||||
* per group timed out after 2 minutes, again on a fresh connection; LOG.md 2026-10-25 (105)). Each probe on a
|
||||
* connection of its own with a short timeout: the span asked for, the reply, how many lines, how long. First the
|
||||
* span itself and wider ones around it, then halves of it down to single articles, then a span past `high`.
|
||||
*/
|
||||
export default class NntpProbe extends BaseCommand {
|
||||
static commandName = 'nntp:probe'
|
||||
static description =
|
||||
'Probes XOVER spans around an article that hangs, bisecting to the articles at fault'
|
||||
static options: CommandOptions = { startApp: true }
|
||||
|
||||
@args.string({ description: 'Group' })
|
||||
declare group: string
|
||||
|
||||
@args.string({ description: 'First article of the span that hung' })
|
||||
declare article: string
|
||||
|
||||
@flags.number({ description: 'Articles in the span (default 32)' })
|
||||
declare size?: number
|
||||
|
||||
@flags.number({ description: 'Seconds without data before a probe is given up (default 20)' })
|
||||
declare timeout?: number
|
||||
|
||||
@flags.boolean({ description: 'Only the span itself and the wider ones, no bisection' })
|
||||
declare noBisect: boolean
|
||||
|
||||
private timeoutMs = 20_000
|
||||
private info = { count: 0, low: 0, high: 0 }
|
||||
|
||||
async run() {
|
||||
this.timeoutMs = (this.timeout ?? 20) * 1000
|
||||
const a = Number(this.article)
|
||||
const size = this.size ?? 32
|
||||
const client = await this.connect()
|
||||
this.info = await client.group(this.group)
|
||||
client.close()
|
||||
this.logger.info(
|
||||
`${this.group}: ${this.info.count} articles, ${this.info.low}–${this.info.high}.`
|
||||
)
|
||||
const span = (from: number, to: number) =>
|
||||
[Math.max(this.info.low, from), Math.min(this.info.high, to)] as const
|
||||
this.logger.info('The span and wider ones:')
|
||||
await this.probe(...span(a, a + size - 1))
|
||||
await this.probe(...span(a, a + size * 32 - 1))
|
||||
await this.probe(...span(a - size * 32, a + size - 1))
|
||||
await this.probe(...span(a - 50_000, a + 49_999))
|
||||
this.logger.info('Past the end, and an empty span before it:')
|
||||
await this.probe(this.info.high + 100, this.info.high + 131)
|
||||
await this.probe(this.info.high + 1, this.info.high + 1)
|
||||
if (this.noBisect) return
|
||||
this.logger.info('Halves of the span:')
|
||||
await this.bisect(a, Math.min(this.info.high, a + size - 1))
|
||||
}
|
||||
|
||||
/** Halves of a span that hangs, down to single articles; a half that answers isn't split further. */
|
||||
private async bisect(from: number, to: number) {
|
||||
const hung = await this.probe(from, to)
|
||||
if (!hung || from === to) return
|
||||
const mid = Math.floor((from + to) / 2)
|
||||
await this.bisect(from, mid)
|
||||
await this.bisect(mid + 1, to)
|
||||
}
|
||||
|
||||
/** One XOVER on a fresh connection; true when it timed out. */
|
||||
private async probe(from: number, to: number): Promise<boolean> {
|
||||
const startedAt = Date.now()
|
||||
let client: XoverClient | null = null
|
||||
try {
|
||||
client = await this.connect()
|
||||
await client.group(this.group)
|
||||
const rows = await client.xover(from, to)
|
||||
const t = client.lastTiming
|
||||
this.logger.info(
|
||||
` XOVER ${from}-${to}: ${rows.length} lines in ${Date.now() - startedAt} ms (reply ${t.replyMs.toFixed(0)}, body ${t.bodyMs.toFixed(0)})${rows.length > 0 ? `; first ${rows[0][0]}, last ${rows[rows.length - 1][0]}` : ''}`
|
||||
)
|
||||
return false
|
||||
} catch (error: any) {
|
||||
const timedOut = /timed out/i.test(error.message)
|
||||
this.logger.warning(
|
||||
` XOVER ${from}-${to}: ${error.message} after ${Date.now() - startedAt} ms${client ? `, ${client.buffered} bytes buffered` : ''}`
|
||||
)
|
||||
return timedOut
|
||||
} finally {
|
||||
client?.close()
|
||||
}
|
||||
}
|
||||
|
||||
private connect() {
|
||||
return XoverClient.connect({
|
||||
host: env.get('NNTP_HOST')!,
|
||||
port: env.get('NNTP_PORT', 563),
|
||||
user: env.get('NNTP_USER'),
|
||||
password: env.get('NNTP_PASSWORD'),
|
||||
secure: env.get('NNTP_SECURE', true),
|
||||
compress: true,
|
||||
timeoutMs: this.timeoutMs,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user