Seed plans fill every uncovered gap, with priorities; cheaper ingest and file storing
Test and deploy / test (push) Successful in 1m27s
Test and deploy / deploy (push) Failing after 33m7s

- 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:
Daan MeijerandClaude Opus 5.5 committed 2026-10-06 14:18:01 +02:00
1 parent 61b683a71f
commit 693e6c3244
14 files changed
+473 -132

No files matched your search

+16 -11
View File
@@ -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))