The namer re-reads partial releases as the Rust window writes them
Test and deploy / test (push) Successful in 3m10s
Test and deploy / deploy (push) Successful in 5m38s

The pending namer's TypeScript re-index gave a file short of parts on the
server to the range path's fragment rule, so a partial release the edge
wrote (Bob's hex-named uploads in chello, ratcave, font, storage) never came
out again: 10 of 148k named releases moved. RangeIndexer's partialFiles
keeps such a file in its release, marked partial; tools/namer_check.ts
replays sample rows from the corpus (10 of 10 found with it, 1 without).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daan MeijerandClaude Opus 5.5 committed 2026-10-29 10:41:50 +01:00
1 parent 0ccb850ea4
commit 8cf965af80
5 files changed
+102 -1

No files matched your search

+18
View File
@@ -123,6 +123,11 @@ export interface RangeIndexerOptions {
* chunks seen so far), as uploads in busy groups spread over minutes.
*/
edgeArticles?: number
/**
* A file given up short of parts is a file of its release all the same (partial), as the Rust edge's window writes
* it, not a fragment: the pending namer reads such a release again (LOG.md 2026-10-29 (121)).
*/
partialFiles?: boolean
}
const MAX_CANDIDATES = 4
@@ -274,6 +279,18 @@ export class RangeIndexer {
else byFile.set(s.fileKey, [s])
}
for (const list of byFile.values()) {
if (this.options.partialFiles) {
// As the Rust window's files(): one article of a multi-part random file is no release.
if (list.length === 1 && (list[0].number !== 1 || list[0].totalParts > 1) && isRandomFile(list[0].file.name)) {
this.count('per-article', uploadTool(list[0].messageId), 1, 0)
continue
}
// One segment per part (two copies of a file share its key here), the first article seen.
const parts = new Map<number, EdgeSegment>()
for (const s of list.sort((a, b) => a.articleNumber - b.articleNumber)) if (!parts.has(s.number)) parts.set(s.number, s)
this.files([{ fileKey: list[0].fileKey, segments: [...parts.values()].sort((a, b) => a.number - b.number) }])
continue
}
// One article of a "file" of many parts under a random name: a random subject per article, which
// never completes from headers (99.8% of the boneless fragments; LOG.md 2026-10-09).
if (list.length === 1 && isRandomFile(list[0].file.name)) {
@@ -430,6 +447,7 @@ export function chunkOf(key: string, files: AggregatedFile<EdgeSegment>[], uploa
lastArticle,
readable: chunkFiles.some((f) => !isRandomFile(f.name)),
hasPar2: chunkFiles.some((f) => /\.par2$/i.test(f.name)),
partial: files.some((f) => f.segments.length < f.segments[0].totalParts),
payload: encodeChunk(chunkFiles),
}
}