59% of settled releases had complete = NULL: without an [i/N] counter the settler left it unknown, and the "phase 2 reads par2 sets for those" step was never built. A release is now complete when every file its par2 sets describe is there by name or by its first-16-KiB hash, or, for an upload renamed after its par2 was made, when it has as many non-par2 files and bytes. Decided when a par2 index is probed, after a par2 merge, and before the pruner drops a set's file list; the batch store, settler and merger keep a decided value instead of resetting it to NULL. releases:par2-complete decides the releases probed before this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
50 lines
2.6 KiB
TypeScript
50 lines
2.6 KiB
TypeScript
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 (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
|
|
* decides those by their par2 sets: app/services/par2_completeness.ts).
|
|
* Phase 2 picks its work from settled releases.
|
|
*/
|
|
class Settler {
|
|
/** Up to `batches` batches of BATCH (all there are without). */
|
|
async run(batches = Infinity): Promise<number> {
|
|
let settled = 0
|
|
for (let batch = 0; batch < batches; batch++) {
|
|
// Their probe candidates become claimable (Prober.claim), the unreadable ones first.
|
|
const { rows } = await db.rawQuery(
|
|
`WITH settled AS (
|
|
UPDATE releases SET settled_at = now(), complete = CASE WHEN file_total IS NULL THEN complete ELSE files >= file_total END
|
|
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 ?)
|
|
RETURNING id, readable, files, bytes, poster, sidecar_only),
|
|
marked AS (UPDATE probe_candidates pc SET ready = true, unreadable = NOT s.readable FROM settled s WHERE pc.release_id = s.id),
|
|
-- A small one of sidecars only (the batch store's sidecar_only) may be an upload's .nfo or .nzb that came apart
|
|
-- from it: the merger's sidecar fold looks at them.
|
|
queued AS (INSERT INTO fold_candidates (release_id) SELECT id FROM settled WHERE sidecar_only AND files <= 3 AND bytes < 1000000 AND poster IS NOT NULL
|
|
ON CONFLICT DO NOTHING)
|
|
SELECT count(*)::int AS n FROM settled`,
|
|
[QUIET_HISTORY_MINUTES, QUIET_HOURS, QUIET_HOURS, BATCH]
|
|
)
|
|
const n = rows[0].n
|
|
settled += n
|
|
if (n < BATCH) return settled
|
|
}
|
|
return settled
|
|
}
|
|
}
|
|
|
|
export default new Settler()
|