Found during Codex review of #26. Pre-existing on master — not introduced by that PR, and reproducible without it.
Problem
#26 fixes stale %processed / %behandlad for a rewritten source filename. But three more caches hold source-derived state keyed by a filename-derived prefix and are not cleared by prFile():
| Cache |
Key |
Cleared by prFile()? |
Cleared by prInitVars()? |
%processed, %behandlad |
$infil |
✅ (as of #26) |
✅ |
%knownToFile |
$fSource |
✅ (line 351) |
✅ |
%form |
$fSource = "${infil}_${sidnr}" |
❌ |
✅ (line 3441) |
%intAct |
$fSource |
❌ |
✅ |
%image |
$iSource = "${fSource}_${bildnr}" |
❌ |
✅ |
Note prFile() already clears %knownToFile but not %form — the same asymmetry that caused #21/#22.
Reproduction
use PDF::Reuse;
sub build { my ($f,$l)=@_; prFile($f); prFontSize(24); prText(72,700,$l); prEnd(); }
build('t.pdf','ALPHA');
prFile('o1.pdf'); prForm('t.pdf'); prEnd();
sleep 2; # ensure the rewrite lands in a later second
build('t.pdf','BRAVO'); # same filename, new content
prFile('o2.pdf'); prForm('t.pdf'); prEnd(); # <-- dies
pass2 DIED: 1785272954 ne 1785272952 aborts
%form{$fSource}[fID] caches the source file's mtime; byggForm() (line 5386) hard-aborts when the current mtime disagrees. Since %form survives prFile(), the stale entry persists across documents.
The sleep 2 is required: without it the rewrite usually lands in the same second and the mtime comparison passes by luck. Same whole-second stat granularity documented in #26.
Severity — lower than #21/#22
This fails loudly with a diagnostic naming both timestamps, rather than producing a corrupt PDF. Content correctness was verified for the same-second case: after a rewrite, prForm emitted the new content (o2.pdf contained BRAVO, not stale ALPHA), so this appears to be a spurious-abort bug, not silent corruption. That should be confirmed more broadly before anyone relies on it.
prInitVars() between documents avoids it entirely.
Why this wasn't folded into #26
%form entries are consumed during emission — [fVALID] at lines 683/701, [fIMAGES] at 743/3507, [fOBJ] at 3508. The invalidation hook added in #26 (dropChangedCache) runs at seven cache-fill sites, which can fire mid-document. Deleting %form{$fSource} from there risks pulling state out from under an in-flight write — a corruption risk strictly worse than the spurious abort it would fix.
So this needs its own design rather than a one-line extension. Options worth weighing:
- Invalidate in
prFile() alongside %knownToFile, deleting every key with the rewritten filename's prefix. Matches where %knownToFile is already handled, and prFile() is a document boundary, so nothing is in flight. Requires a prefix scan (grep /^\Q$name\E_/) since keys are compound.
- Extend the
#26 self-write invalidation to sweep the same prefixes — same placement, same document-boundary safety.
- Relax the
byggForm mtime abort to re-read rather than die. Fixes the symptom but discards a deliberate consistency check; would need to be sure the rest of %form gets refreshed.
Option 1 or 2 look right; the prefix scan is the only real cost, and these hashes are small.
Also worth fixing nearby
analysera() calls its cache preamble (and, since #26, dropChangedCache) before findGet(), which can rewrite $infil — whereas getPage() calls findGet() first. For ordinary prDoc($path) this is harmless because findGet returns the path unchanged for existing local files, but on the prCid replay path (file missing, checkId set → returns $fil . $cid) the cache key, the glob-aliased hash, the opened file, and the final $processed{$infil}{root} assignment can diverge. Pre-existing ordering; called out by the same review. Low priority, but it should be made consistent with getPage() when this area is next touched.
Found during Codex review of #26. Pre-existing on
master— not introduced by that PR, and reproducible without it.Problem
#26 fixes stale
%processed/%behandladfor a rewritten source filename. But three more caches hold source-derived state keyed by a filename-derived prefix and are not cleared byprFile():prFile()?prInitVars()?%processed,%behandlad$infil%knownToFile$fSource%form$fSource="${infil}_${sidnr}"%intAct$fSource%image$iSource="${fSource}_${bildnr}"Note
prFile()already clears%knownToFilebut not%form— the same asymmetry that caused #21/#22.Reproduction
%form{$fSource}[fID]caches the source file's mtime;byggForm()(line 5386) hard-aborts when the current mtime disagrees. Since%formsurvivesprFile(), the stale entry persists across documents.The
sleep 2is required: without it the rewrite usually lands in the same second and the mtime comparison passes by luck. Same whole-secondstatgranularity documented in #26.Severity — lower than #21/#22
This fails loudly with a diagnostic naming both timestamps, rather than producing a corrupt PDF. Content correctness was verified for the same-second case: after a rewrite,
prFormemitted the new content (o2.pdfcontained BRAVO, not stale ALPHA), so this appears to be a spurious-abort bug, not silent corruption. That should be confirmed more broadly before anyone relies on it.prInitVars()between documents avoids it entirely.Why this wasn't folded into #26
%formentries are consumed during emission —[fVALID]at lines 683/701,[fIMAGES]at 743/3507,[fOBJ]at 3508. The invalidation hook added in #26 (dropChangedCache) runs at seven cache-fill sites, which can fire mid-document. Deleting%form{$fSource}from there risks pulling state out from under an in-flight write — a corruption risk strictly worse than the spurious abort it would fix.So this needs its own design rather than a one-line extension. Options worth weighing:
prFile()alongside%knownToFile, deleting every key with the rewritten filename's prefix. Matches where%knownToFileis already handled, andprFile()is a document boundary, so nothing is in flight. Requires a prefix scan (grep /^\Q$name\E_/) since keys are compound.#26self-write invalidation to sweep the same prefixes — same placement, same document-boundary safety.byggFormmtime abort to re-read rather than die. Fixes the symptom but discards a deliberate consistency check; would need to be sure the rest of%formgets refreshed.Option 1 or 2 look right; the prefix scan is the only real cost, and these hashes are small.
Also worth fixing nearby
analysera()calls its cache preamble (and, since #26,dropChangedCache) beforefindGet(), which can rewrite$infil— whereasgetPage()callsfindGet()first. For ordinaryprDoc($path)this is harmless becausefindGetreturns the path unchanged for existing local files, but on theprCidreplay path (file missing, checkId set → returns$fil . $cid) the cache key, the glob-aliased hash, the opened file, and the final$processed{$infil}{root}assignment can diverge. Pre-existing ordering; called out by the same review. Low priority, but it should be made consistent withgetPage()when this area is next touched.