Compare commits
2
Commits
f4df25d774
...
37750fa4b3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37750fa4b3 | ||
|
|
e336538d8d |
@@ -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