Seed plans fill every uncovered gap, with priorities; cheaper ingest and file storing
- seed:plan plans every part of a group's history no range covers (it started below the lowest known range and left a.b.ath's 13 most recent months out); seed_ranges.priority orders leases (seed:plan --priority). - binaries.part_count, kept by the parts insert in the same statement: claimCompleted compares it instead of counting parts every batch. - file_keys: 64-bit hashes of all file keys; FileStore.insert asks it before probing every files partition. - worker:file stores 100 completed binaries per job with set-based queries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
61b683a71f
commit
693e6c3244
14 files changed
+473
-132
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { createHash, randomBytes } from 'node:crypto'
|
||||
import db from '@adonisjs/lucid/services/db'
|
||||
import { cut, uncovered } from '#lib/seed/plan'
|
||||
import NntpService from '#services/NntpService'
|
||||
import SeedService from '#services/SeedService'
|
||||
import BackfillService from '#services/BackfillService'
|
||||
@@ -77,7 +78,7 @@ class SeedOrchestrator {
|
||||
WHERE id = (
|
||||
SELECT id FROM seed_ranges
|
||||
WHERE status = 'queued' OR (status = 'leased' AND heartbeat_at < now() - make_interval(secs => ?))
|
||||
ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED)
|
||||
ORDER BY priority DESC, id LIMIT 1 FOR UPDATE SKIP LOCKED)
|
||||
RETURNING id, group_name, next_id, end_id`,
|
||||
[nodeId, LEASE_TTL_SECONDS]
|
||||
)
|
||||
@@ -143,9 +144,12 @@ class SeedOrchestrator {
|
||||
* server's oldest article. A group's backfill stops where its seed plan
|
||||
* begins. Ranges are interleaved across groups, newest first.
|
||||
*/
|
||||
async plan(groups: string[], options: { days?: number; size: number }): Promise<{ group: string; from: number; to: number; ranges: number }[]> {
|
||||
async plan(
|
||||
groups: string[],
|
||||
options: { days?: number; size: number; priority?: number }
|
||||
): Promise<{ group: string; from: number; to: number; ranges: number; gaps: number }[]> {
|
||||
const perGroup: { group: string; ranges: [number, number][] }[] = []
|
||||
const summary: { group: string; from: number; to: number; ranges: number }[] = []
|
||||
const summary: { group: string; from: number; to: number; ranges: number; gaps: number }[] = []
|
||||
const conn = await NntpService.acquire()
|
||||
let failed = false
|
||||
try {
|
||||
@@ -159,20 +163,21 @@ class SeedOrchestrator {
|
||||
const date = header ? Date.parse(header.date) : Number.NaN
|
||||
return Number.isNaN(date) ? null : date
|
||||
}
|
||||
// Top: where live indexing began, or the backfill's position, or below earlier seed ranges.
|
||||
// Top: where live indexing began, or as far down as the backfill has come.
|
||||
let top = Number(await articleAtDate(first, last, new Date(group.created_at).getTime(), dateOf))
|
||||
if (group.backfill_next_id !== null) top = Math.min(top, Number(group.backfill_next_id))
|
||||
const backfilling = group.backfill_next_id !== null && group.backfill_until_id !== null && Number(group.backfill_next_id) > Number(group.backfill_until_id)
|
||||
if (backfilling) top = Math.min(top, Number(group.backfill_next_id))
|
||||
const seeded = await db.from('seed_ranges').where('group_name', name).min('start_id as low').first()
|
||||
if (seeded?.low !== null && seeded?.low !== undefined) top = Math.min(top, Number(seeded.low))
|
||||
const bottom = options.days
|
||||
? Number(await articleAtDate(first, last, Date.now() - options.days * 24 * 60 * 60 * 1000, dateOf))
|
||||
: Number(first)
|
||||
|
||||
const ranges: [number, number][] = []
|
||||
for (let end = top - 1; end >= bottom; end -= options.size) ranges.push([Math.max(bottom, end - options.size + 1), end])
|
||||
// Everything below the top that no earlier range covers (in any state). Planning below
|
||||
// the lowest earlier range left a 13-month gap in a.b.ath (LOG.md 2026-10-06 (7)).
|
||||
const planned = await db.from('seed_ranges').where('group_name', name).select('start_id', 'end_id')
|
||||
const gaps = uncovered(bottom, top - 1, planned.map((r) => [Number(r.start_id), Number(r.end_id)] as [number, number]))
|
||||
const ranges = cut(gaps, options.size)
|
||||
perGroup.push({ group: name, ranges })
|
||||
summary.push({ group: name, from: bottom, to: top - 1, ranges: ranges.length })
|
||||
summary.push({ group: name, from: bottom, to: top - 1, ranges: ranges.length, gaps: gaps.length })
|
||||
// The seed plan takes over the rest of this group's backfill.
|
||||
if (backfilling && ranges.length > 0) {
|
||||
await db.from('groups').where('id', group.id).update({ backfill_until_id: group.backfill_next_id })
|
||||
@@ -190,7 +195,7 @@ class SeedOrchestrator {
|
||||
for (let i = 0; perGroup.some((g) => i < g.ranges.length); i++) {
|
||||
for (const g of perGroup) {
|
||||
const range = g.ranges[i]
|
||||
if (range) rows.push({ group_name: g.group, start_id: range[0], end_id: range[1], next_id: range[0] })
|
||||
if (range) rows.push({ group_name: g.group, start_id: range[0], end_id: range[1], next_id: range[0], priority: options.priority ?? 0 })
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < rows.length; i += 5000) await db.table('seed_ranges').multiInsert(rows.slice(i, i + 5000))
|
||||
|
||||
Reference in new issue
Block a user