fix(pS-usage): close delete-ward residuals — remint on corrupt, named abort keys, truncation protect-all, abort->protect-all set
This commit is contained in:
+31
-7
@@ -145,11 +145,20 @@ UsagePublishPlan planUsagePublish(const std::optional<std::string>& existing,
|
||||
}
|
||||
const std::optional<UsageRecord> theirs = decodeUsageRecord(*existing);
|
||||
if (!theirs) {
|
||||
// Undecodable existing value under MY OWN key: a sibling sharing this key
|
||||
// (copy) always writes decodable records, so this is corruption. Overwrite
|
||||
// with mine — the self-heal restores correct protection for my holds; the
|
||||
// prune side independently ABORTS while an unreadable record is present
|
||||
// (foldUsageRecords), so the corrupt window can never cause a delete.
|
||||
// Undecodable existing value under MY key: corruption (a sibling sharing
|
||||
// this key via copy always writes decodable records). REMINT rather than
|
||||
// overwrite: writing mine over the corrupt key would clear the prune-side
|
||||
// abort, but a same-key sibling B's holds would then be unprotected until
|
||||
// B publishes again. Leaving the corrupt key in place keeps the prune-side
|
||||
// abort firing (foldUsageRecords.abortPrune) so the window where B's holds
|
||||
// might be unprotected can never resolve toward delete. Mine is published
|
||||
// under the new key that remint produces.
|
||||
// NOTE (>16 MB gap): readReasamplerExtState returning nullopt for a value
|
||||
// larger than 16 MB is indistinguishable from "absent" at the publish site;
|
||||
// that narrow case takes the fresh-write branch above rather than remint.
|
||||
// Both outcomes are safe (fresh write is also correct for a truly absent key);
|
||||
// the gap is documented in the header's fail-safe list.
|
||||
plan.remint = true;
|
||||
return plan;
|
||||
}
|
||||
|
||||
@@ -235,9 +244,24 @@ UsageFoldResult foldUsageRecords(
|
||||
for (const std::optional<UsageRecord>& rec : decoded) {
|
||||
if (!rec) {
|
||||
// A present-but-unreadable record: it may protect ANYTHING, so the prune
|
||||
// must halt outright — heldPaths is irrelevant once abortPrune is set (the
|
||||
// caller deletes nothing).
|
||||
// must halt outright. Belt-and-braces: return the PROTECT-ALL set (all
|
||||
// readable records' paths) so the fail-safe holds even under a future
|
||||
// caller that forgets to check abortPrune before using heldPaths. The
|
||||
// abort flag is still the authoritative signal; heldPaths is the
|
||||
// maximum-protection fallback.
|
||||
result.abortPrune = true;
|
||||
// Collect EVERY path from EVERY readable record, bypassing the liveness
|
||||
// filter entirely (on abort the protected set is unknowable, so every
|
||||
// decoded hold must be included regardless of track-guid membership).
|
||||
std::unordered_set<std::string> seen;
|
||||
for (const std::optional<UsageRecord>& r : decoded) {
|
||||
if (!r) continue;
|
||||
for (const UsageHold& h : r->holds) {
|
||||
if (h.relativePath.empty()) continue;
|
||||
if (seen.insert(h.relativePath).second)
|
||||
result.heldPaths.push_back(h.relativePath);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
records.push_back(*rec);
|
||||
|
||||
Reference in New Issue
Block a user