Inspection backlog in Postgres; BullMQ gets a working set
5.4M waiting inspection jobs took 3.3 GB of Redis and most of its single thread. Producers now add file ids to inspection_queue; index-scheduler feeds up to INSPECTION_FEED_MAX (50k) into BullMQ. inspection:from-redis moves the existing backlog over once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e7e338213a
commit
35d0b2da65
11 files changed
+219
-24
No files matched your search
+10
-13
@@ -4,6 +4,7 @@ import db from '@adonisjs/lucid/services/db'
|
||||
import QueueService from '#services/QueueService'
|
||||
import ContentService from '#services/ContentService'
|
||||
import { isArchiveStart } from '#lib/content/index'
|
||||
import InspectionQueue, { INSPECTION_PRIORITY } from '#services/InspectionQueue'
|
||||
|
||||
const BATCH = 1000
|
||||
|
||||
@@ -71,19 +72,15 @@ export default class ContentBackfill extends BaseCommand {
|
||||
const jobs = files
|
||||
.filter((f) => (this.archives ? isArchiveStart(f.filename) : this.sfv || this.nicovideo || ContentService.isCandidate(f.filename)))
|
||||
.slice(0, this.limit ? this.limit - queued : undefined)
|
||||
.map((f) => ({
|
||||
name: 'process-collection',
|
||||
data: { fileId: Number(f.id) },
|
||||
// A new id per run: BullMQ ignores an add() for an id it still knows.
|
||||
// Targeted backfills go ahead of whatever is waiting.
|
||||
opts: {
|
||||
jobId: `content-${f.id}-${Date.now()}`,
|
||||
lifo: this.archiveNzbs || this.sevenZip || this.archives || this.sfv,
|
||||
// niconico captures are many (~190k) and only cosmetic: behind everything else.
|
||||
...(this.nicovideo ? { priority: 10 } : {}),
|
||||
},
|
||||
}))
|
||||
if (jobs.length > 0) await QueueService.collectionQueue.addBulk(jobs)
|
||||
.map((f) => Number(f.id))
|
||||
// Into the inspection backlog (InspectionQueue): targeted backfills go ahead of what's waiting;
|
||||
// niconico captures are many (~190k) and only cosmetic, so behind everything else.
|
||||
const priority = this.nicovideo
|
||||
? INSPECTION_PRIORITY.low
|
||||
: this.archiveNzbs || this.sevenZip || this.archives || this.sfv
|
||||
? INSPECTION_PRIORITY.targeted
|
||||
: INSPECTION_PRIORITY.normal
|
||||
if (jobs.length > 0) await InspectionQueue.add(jobs, priority)
|
||||
queued += jobs.length
|
||||
}
|
||||
this.logger.info(`Queued ${queued} files for content inspection.`)
|
||||
|
||||
@@ -5,6 +5,7 @@ import NntpService from '#services/NntpService'
|
||||
import QueueService from '#services/QueueService'
|
||||
import indexerConfig from '#config/indexer'
|
||||
import Shutdown from '#services/Shutdown'
|
||||
import InspectionQueue from '#services/InspectionQueue'
|
||||
import FetchRangeService from '#services/FetchRangeService'
|
||||
import SplitService from '#services/SplitService'
|
||||
import BackfillService from '#services/BackfillService'
|
||||
@@ -108,6 +109,12 @@ export default class IndexScheduler extends BaseCommand {
|
||||
}
|
||||
}
|
||||
|
||||
// The inspection backlog lives in Postgres; BullMQ gets a working set (LOG.md 2026-10-07 (8)).
|
||||
const feedInspection = async () => {
|
||||
const moved = await InspectionQueue.feed()
|
||||
if (moved > 0) this.logger.info(`Inspection: ${moved} files moved from the backlog to the queue.`)
|
||||
}
|
||||
|
||||
const backfill = async () => {
|
||||
try {
|
||||
const queued = await BackfillService.tick()
|
||||
@@ -122,7 +129,7 @@ export default class IndexScheduler extends BaseCommand {
|
||||
// here once ended the loop while the process stayed up, and live indexing stopped for ~3 hours
|
||||
// (LOG.md 2026-10-07 (3)).
|
||||
while (!Shutdown.requested) {
|
||||
for (const step of [schedule, backfill, split]) {
|
||||
for (const step of [schedule, backfill, split, feedInspection]) {
|
||||
try {
|
||||
await step()
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { BaseCommand, flags } from '@adonisjs/core/ace'
|
||||
import { CommandOptions } from '@adonisjs/core/types/ace'
|
||||
import QueueService from '#services/QueueService'
|
||||
import InspectionQueue, { INSPECTION_PRIORITY } from '#services/InspectionQueue'
|
||||
|
||||
/**
|
||||
* One-off (LOG.md 2026-10-07 (8)): moves the inspection jobs waiting in
|
||||
* BullMQ into the inspection_queue table, then empties BullMQ's waiting and
|
||||
* prioritized jobs so Redis gives the memory back. The file id is read from
|
||||
* the job id (file-<id>, content-<id>-<time>), so no job is loaded.
|
||||
* index-scheduler then feeds a working set back in.
|
||||
*/
|
||||
export default class InspectionFromRedis extends BaseCommand {
|
||||
public static commandName = 'inspection:from-redis'
|
||||
public static description = 'Moves waiting inspection jobs from Redis into the inspection_queue table.'
|
||||
|
||||
public static options: CommandOptions = {
|
||||
startApp: true,
|
||||
}
|
||||
|
||||
@flags.boolean({ description: 'Only count, change nothing' })
|
||||
declare dryRun: boolean
|
||||
|
||||
public async run() {
|
||||
const queue = QueueService.collectionQueue
|
||||
const client = await queue.client
|
||||
const prefix = `${queue.opts.prefix ?? 'bull'}:${queue.name}`
|
||||
try {
|
||||
const idOf = (jobId: string) => {
|
||||
const m = /^(?:file|content)-(\d+)/.exec(jobId)
|
||||
return m ? Number(m[1]) : null
|
||||
}
|
||||
let moved = 0
|
||||
let skipped = 0
|
||||
const take = async (ids: string[], priority: number) => {
|
||||
const fileIds = ids.map(idOf).filter((id): id is number => id !== null)
|
||||
skipped += ids.length - fileIds.length
|
||||
if (!this.dryRun) await InspectionQueue.add(fileIds, priority)
|
||||
moved += fileIds.length
|
||||
}
|
||||
const waiting = await client.llen(`${prefix}:wait`)
|
||||
for (let i = 0; i < waiting; i += 50_000) await take(await client.lrange(`${prefix}:wait`, i, i + 49_999), INSPECTION_PRIORITY.normal)
|
||||
// Prioritized jobs were the low-priority niconico captures.
|
||||
const prioritized = await client.zcard(`${prefix}:prioritized`)
|
||||
for (let i = 0; i < prioritized; i += 50_000) await take(await client.zrange(`${prefix}:prioritized`, i, i + 49_999), INSPECTION_PRIORITY.low)
|
||||
this.logger.info(`${this.dryRun ? 'Would move' : 'Moved'} ${moved} jobs into inspection_queue (${skipped} with other ids skipped).`)
|
||||
if (this.dryRun) return
|
||||
// Waiting and prioritized jobs only; active ones finish as they are.
|
||||
await queue.drain()
|
||||
const counts = await queue.getJobCounts('wait', 'prioritized', 'active')
|
||||
this.logger.info(`BullMQ now: ${counts.wait} waiting, ${counts.prioritized} prioritized, ${counts.active} active; table: ${await InspectionQueue.size()}.`)
|
||||
} finally {
|
||||
await QueueService.closeAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user