From 885e4e65412c8c718878d6cafd5452dd0729ee4c Mon Sep 17 00:00:00 2001 From: Daan Meijer Date: Mon, 12 Oct 2026 09:42:53 +0200 Subject: [PATCH] Settler: history releases settle after half an hour A release whose last post is 6 hours old gets nothing from the tail, only fragments from neighbouring ranges: settled once unchanged for 30 minutes. Waiting 6 hours after it was read held every history release back from phase 2 after each reset; with the hourly sweeps' resets the prober (and so the par2 merge) never had work. Co-Authored-By: Claude Opus 5.5 --- app/services/settler.ts | 14 +++++++++++--- tests/functional/settler.spec.ts | 12 ++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/services/settler.ts b/app/services/settler.ts index 97c5294..0892aae 100644 --- a/app/services/settler.ts +++ b/app/services/settler.ts @@ -2,11 +2,18 @@ import db from '@adonisjs/lucid/services/db' /** Releases unchanged for this long are settled (ranges are read in order; the merger adds within minutes). */ const QUIET_HOURS = 6 +/** + * History: a release whose last post is QUIET_HOURS old gets nothing new from the tail, only from the + * neighbouring ranges' fragments, which the merger adds within minutes: settled once unchanged this long. + * Waiting QUIET_HOURS after it was read held every history release back from phase 2 for six hours after + * each reset (the hourly sweeps' resets kept the prober idle; LOG.md 2026-10-12 (13)). + */ +const QUIET_HISTORY_MINUTES = 30 const BATCH = 50_000 /** * Settles releases (REBUILD.md, part 8): once nothing has been added for - * QUIET_HOURS, its completeness is decided: complete when every file the + * QUIET_HOURS (QUIET_HISTORY_MINUTES once its last post is that old), its completeness is decided: complete when every file the * [i/N] counter announced was found, unknown without a counter (phase 2 * reads par2 sets for those). Phase 2 picks its work from settled releases. */ @@ -16,8 +23,9 @@ class Settler { while (true) { const { rowCount } = await db.rawQuery( `UPDATE releases SET settled_at = now(), complete = CASE WHEN file_total IS NULL THEN NULL ELSE files >= file_total END - WHERE id IN (SELECT id FROM releases WHERE settled_at IS NULL AND last_seen_at < now() - make_interval(hours => ?) LIMIT ?)`, - [QUIET_HOURS, BATCH] + WHERE id IN (SELECT id FROM releases WHERE settled_at IS NULL AND last_seen_at < now() - make_interval(mins => ?) + AND (last_seen_at < now() - make_interval(hours => ?) OR coalesce(last_posted_at, posted_at) < now() - make_interval(hours => ?)) LIMIT ?)`, + [QUIET_HISTORY_MINUTES, QUIET_HOURS, QUIET_HOURS, BATCH] ) settled += rowCount ?? 0 if ((rowCount ?? 0) < BATCH) return settled diff --git a/tests/functional/settler.spec.ts b/tests/functional/settler.spec.ts index 51cb154..132a3d5 100644 --- a/tests/functional/settler.spec.ts +++ b/tests/functional/settler.spec.ts @@ -16,4 +16,16 @@ test.group('Settler', (group) => { const byName = Object.fromEntries((await db.from('releases').select('name', 'complete', 'settled_at')).map((r) => [r.name, [r.complete, r.settled_at !== null]])) assert.deepEqual(byName, { a: [true, true], b: [false, true], c: [null, true], d: [null, false] }) }) + + test('settles history releases (posted long ago) once unchanged for half an hour', async ({ assert }) => { + const posted = new Date('2015-01-01T00:00:00Z') + const row = (key: string, lastSeen: Date, lastPosted: Date) => ({ + release_key: key.padEnd(40, '0'), identity_key: key.padEnd(40, '0'), name: key, source: 'subject', groups: '[]', files: 1, last_seen_at: lastSeen, posted_at: lastPosted, last_posted_at: lastPosted, + }) + const minutesAgo = (m: number) => new Date(Date.now() - m * 60_000) + await db.table('releases').multiInsert([row('old', minutesAgo(31), posted), row('fresh', minutesAgo(5), posted), row('tail', minutesAgo(31), minutesAgo(40))]) + assert.equal(await Settler.run(), 1) + const settled = (await db.from('releases').whereNotNull('settled_at').select('name')).map((r) => r.name) + assert.deepEqual(settled, ['old']) + }) })