music: report what the nightly full actually found
The nightly full rebuilds every album by definition, so "it completed" says nothing about whether it is still needed. Diff the from-scratch build against the index that was already live, just before the slot swaps in, and log the delta: albums added, removed, and entries whose contents changed. A run reporting NONE did no useful work. A string of those is the evidence for retiring the nightly; a delta that keeps coming back names the albums to go and look at instead of guessing. Entries are compared field by field rather than by JSON.stringify: the optional fields are spread conditionally, so two entries built by the same code can serialise with different key order, and stringify would report every album as changed every night. A cache-format upgrade is labelled, since that rebuilds everything legitimately and would otherwise read as total rot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -645,6 +645,57 @@ export async function reindexNow(): Promise<IndexStatus> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ManifestDelta = { added: string[]; removed: string[]; changed: string[] };
|
||||||
|
|
||||||
|
// Compare a from-scratch build against the index that was already live. This exists to answer one
|
||||||
|
// question with evidence rather than opinion: is the nightly full still FINDING anything, or is it
|
||||||
|
// spending 40 minutes of disk rebuilding what the incremental had already got right?
|
||||||
|
//
|
||||||
|
// A run that reports no differences did no useful work. A run of those is the case for retiring the
|
||||||
|
// nightly; a delta that keeps reappearing names the albums to go and look at.
|
||||||
|
//
|
||||||
|
// Compared field-by-field rather than by JSON.stringify, because two entries built by the same code
|
||||||
|
// can serialise with different key order (the optional fields are spread conditionally) and that would
|
||||||
|
// report drift where there is none.
|
||||||
|
function diffManifest(prev: Manifest, next: Manifest): ManifestDelta {
|
||||||
|
const canon = (e: ManifestEntry) =>
|
||||||
|
[e.v, e.cover, e.tracks, e.videos ?? 0, e.images ?? 0, e.disco ?? false, e.lyrics ?? 0, e.posters ?? 0].join('|');
|
||||||
|
|
||||||
|
const added: string[] = [];
|
||||||
|
const changed: string[] = [];
|
||||||
|
for (const [rel, entry] of Object.entries(next.albums)) {
|
||||||
|
const before = prev.albums[rel];
|
||||||
|
if (!before) added.push(rel);
|
||||||
|
else if (canon(before) !== canon(entry)) changed.push(rel);
|
||||||
|
}
|
||||||
|
const removed = Object.keys(prev.albums).filter((rel) => !next.albums[rel]);
|
||||||
|
return { added, removed, changed };
|
||||||
|
}
|
||||||
|
|
||||||
|
function logManifestDelta(prev: Manifest, next: Manifest): void {
|
||||||
|
// A cache-format upgrade rebuilds every album by definition, so the delta is expected and says
|
||||||
|
// nothing about drift. Label it rather than let it read as 6k albums of rot.
|
||||||
|
if (prev.version !== next.version) {
|
||||||
|
console.log(`[music] full reindex: cache format v${prev.version} → v${next.version}, delta below is the upgrade itself`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { added, removed, changed } = diffManifest(prev, next);
|
||||||
|
const total = added.length + removed.length + changed.length;
|
||||||
|
if (total === 0) {
|
||||||
|
console.log('[music] full reindex delta: NONE — the incremental index was already identical');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[music] full reindex delta: +${added.length} added, -${removed.length} removed, ~${changed.length} changed`);
|
||||||
|
const sample = (label: string, rels: string[]) => {
|
||||||
|
for (const rel of rels.slice(0, 5)) console.log(`[music] ${label} ${rel || '.'}`);
|
||||||
|
if (rels.length > 5) console.log(`[music] ${label} …and ${rels.length - 5} more`);
|
||||||
|
};
|
||||||
|
sample('+', added);
|
||||||
|
sample('-', removed);
|
||||||
|
sample('~', changed);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Full, from scratch: build a complete index into a FRESH slot without touching the live one, then swap
|
* Full, from scratch: build a complete index into a FRESH slot without touching the live one, then swap
|
||||||
* it in atomically only on success — a failed rebuild leaves the live index untouched. For the nightly cron.
|
* it in atomically only on success — a failed rebuild leaves the live index untouched. For the nightly cron.
|
||||||
@@ -652,6 +703,8 @@ export async function reindexNow(): Promise<IndexStatus> {
|
|||||||
export function reindexFull(): Promise<IndexStatus> {
|
export function reindexFull(): Promise<IndexStatus> {
|
||||||
return withIndexLock(async () => {
|
return withIndexLock(async () => {
|
||||||
await ensureCacheSetup();
|
await ensureCacheSetup();
|
||||||
|
// Read the live index BEFORE the swap — it is what the delta below is measured against.
|
||||||
|
const live = await loadManifest();
|
||||||
const slot = slotPath(String(Date.now()));
|
const slot = slotPath(String(Date.now()));
|
||||||
await rm(slot, { recursive: true, force: true }).catch(() => {});
|
await rm(slot, { recursive: true, force: true }).catch(() => {});
|
||||||
// Empty prev ⇒ everything rebuilds into the fresh slot.
|
// Empty prev ⇒ everything rebuilds into the fresh slot.
|
||||||
@@ -660,6 +713,7 @@ export function reindexFull(): Promise<IndexStatus> {
|
|||||||
await rm(slot, { recursive: true, force: true }).catch(() => {});
|
await rm(slot, { recursive: true, force: true }).catch(() => {});
|
||||||
console.error('[music] full reindex failed — live index left untouched');
|
console.error('[music] full reindex failed — live index left untouched');
|
||||||
} else {
|
} else {
|
||||||
|
logManifestDelta(live, next);
|
||||||
await activateSlot(slot);
|
await activateSlot(slot);
|
||||||
manifest = next;
|
manifest = next;
|
||||||
manifestLoaded = true;
|
manifestLoaded = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user