Complete archive listings by walking RAR headers, at low priority
Test and deploy / test (push) Successful in 1m24s
Test and deploy / deploy (push) Successful in 1m58s

Inspection lists only the entries whose headers are in the first segment
of a first volume. A walk hops from header to header, fetching one segment
per entry and volume, in its own queue (one job per collection worker) on
NNTP connections inspection isn't waiting for. content:walk-archives queues
releases inspected earlier (CONCEPT D40).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daan MeijerandClaude Opus 5.5 committed 2026-10-05 23:32:52 +02:00
1 parent b7ae8267c6
commit f4ded062df
17 files changed
+597 -17

No files matched your search

+70
View File
@@ -0,0 +1,70 @@
import { BaseCommand, args, flags } from '@adonisjs/core/ace'
import { CommandOptions } from '@adonisjs/core/types/ace'
import db from '@adonisjs/lucid/services/db'
import QueueService from '#services/QueueService'
import NntpService from '#services/NntpService'
import ArchiveWalkService from '#services/ArchiveWalkService'
const BATCH = 5000
/**
* Complete archive listings for releases inspected before archive walks
* existed (CONCEPT.md D40): queues those whose listing leaves most of the
* release unaccounted for. worker:collection walks them, one at a time and
* behind inspection. With release ids, walks those right here instead.
*/
export default class ArchiveWalk extends BaseCommand {
public static commandName = 'content:walk-archives'
public static description = 'Queues releases with partial archive listings for a (low priority) header walk.'
public static options: CommandOptions = {
startApp: true,
}
@args.spread({ description: 'Release ids to walk now (instead of queueing)', required: false })
declare releaseIds?: string[]
@flags.number({ description: 'Queue at most this many releases' })
declare limit?: number
@flags.boolean({ description: 'Also releases that were walked before' })
declare again: boolean
public async run() {
try {
if (this.releaseIds?.length) {
for (const id of this.releaseIds) {
this.logger.info(JSON.stringify(await ArchiveWalkService.walk(Number(id))))
}
return
}
let lastId = 0
let queued = 0
while (!this.limit || queued < this.limit) {
const query = db
.from('releases as r')
.where('r.id', '>', lastId)
.whereNotNull('r.contents')
.orderBy('r.id')
.limit(BATCH)
.select('r.id', 'r.size', 'r.contents_walked_at', db.raw(`(
SELECT count(*) FROM jsonb_array_elements(r.contents)) AS n`), db.raw(`(
SELECT coalesce(sum((e->>'size')::bigint), 0) FROM jsonb_array_elements(r.contents) e) AS listed`))
const rows = await query
if (rows.length === 0) break
lastId = Number(rows.at(-1).id)
const jobs = rows
.filter((r) => this.again || !r.contents_walked_at)
.filter((r) => ArchiveWalkService.worthWalking(Number(r.n), Number(r.listed), Number(r.size)))
.slice(0, this.limit ? this.limit - queued : undefined)
.map((r) => ({ name: 'walk', data: { releaseId: Number(r.id) }, opts: { jobId: `walk-${r.id}-${Date.now()}` } }))
if (jobs.length > 0) await QueueService.archiveWalkQueue.addBulk(jobs)
queued += jobs.length
}
this.logger.info(`Queued ${queued} releases for an archive walk.`)
} finally {
await QueueService.closeAll()
await NntpService.shutdown()
}
}
}
+20 -1
View File
@@ -7,6 +7,7 @@ import Shutdown from '#services/Shutdown'
import QueueService from '#services/QueueService'
import NntpService from '#services/NntpService'
import ContentService from '#services/ContentService'
import ArchiveWalkService from '#services/ArchiveWalkService'
import File from '#models/file'
export default class CollectionWorker extends BaseCommand {
@@ -70,10 +71,28 @@ export default class CollectionWorker extends BaseCommand {
this.logger.error(`Collection job ${job?.id} failed: ${err.message}`)
})
// Complete archive listings (D40): background work beside inspection, on the same NNTP pool.
const walkConcurrency = env.get('ARCHIVE_WALK_CONCURRENCY', 1)
const walker = walkConcurrency > 0
? new Worker('archive-walk-queue', async (job) => {
const o = await ArchiveWalkService.walk(Number(job.data.releaseId))
if (o.skipped) return o
this.logger.info(
`Archive walk of release ${o.releaseId}: ${o.before} → ${o.entries} entries from ${o.segments} segments` +
(o.complete ? '' : ` (stopped: ${o.stopped})`) +
(o.renamed ? `, renamed to "${o.renamed}"` : '')
)
return o
}, { connection: queueConfig.connection, concurrency: walkConcurrency })
: null
walker?.on('failed', (job, err) => {
this.logger.error(`Archive walk job ${job?.id} failed: ${err.message}`)
})
this.logger.info('Collection worker started and listening for jobs.')
await Shutdown.wait()
this.logger.info('Shutting down: finishing active jobs...')
await worker.close()
await Promise.all([worker.close(), walker?.close()])
await NntpService.shutdown()
await QueueService.closeAll()
}