Repository navigation
GC: handle Iceberg files outside the table's declared location - #12765
Conversation
Nessie GC failed with 'IllegalArgumentException: Path must be relative: s3://...' when an Iceberg snapshot references files that are not located under the 'location' declared in the table's metadata - for example data files written via 'write.data.path', or files kept from a previous table location. A single such file aborted the whole GC run. Such files can never be expressed relative to the content's base location, so: * FileReference now allows an absolute path() for files outside base() (base() itself must still be absolute); absolutePath() already resolves this case correctly. * PerContentDeleteExpired puts those files into the live-files bloom filter under their absolute URI and, when at least one such reference exists, the sweep additionally matches listed files by their absolute URI. This keeps a live file that is referenced absolutely but happens to be located under one of the content's (swept) base locations - for example an older table location - from being deleted. * Files outside all base locations are never listed and therefore never deleted. Locations declared via table properties (write.data.path etc.) are deliberately NOT added as swept base locations, because such locations can be shared by multiple tables (e.g. the object-storage layout), and sweeping them per-content would delete other contents' live files. Fixes projectnessie#10817 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2d0093b to
75192b5
Compare
dimas-b
left a comment
There was a problem hiding this comment.
Thanks for your contribution, @Beaglefoot ! The PR looks good to me overall. I wonder what @snazy thinks too.
| expireStats.totalFiles++; | ||
| if (filter.mightContain(f.path())) { | ||
| if (filter.mightContain(f.path()) | ||
| || (matchAbsolutePaths && filter.mightContain(f.absolutePath()))) { |
There was a problem hiding this comment.
matchAbsolutePaths complicates the method's logic a bit 🤔 Is it really necessary? Why not always check filter.mightContain(f.absolutePath())? It should not add too much overhead, I hope. WDYT?
There was a problem hiding this comment.
Agreed — the flag was a micro-optimization to skip the second lookup when no absolute references exist, but the extra lookup is negligible next to the object-store listing, and a bloom false positive can only keep a file (the safe direction). Removed in 26a7076, the method signature is back to its original shape.
| if (path.isAbsolute()) { | ||
| liveFilesStats.absolutePaths++; | ||
| } | ||
| filter.put(path); |
There was a problem hiding this comment.
nit: it might be best to do this right above line 149 for the sake of clarity since the if statement does not affect this action.
Drop the matchAbsolutePaths flag - the second bloom-filter lookup is cheap compared to the object-store listing, and a false positive can only keep a file. Move the unconditional filter.put() above the absolute-path counting. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes #10817
Problem
Nessie GC fails with
IllegalArgumentException: Path must be relative: s3://...when an Iceberg snapshot references files that are not located under thelocationdeclared in the table's metadata — for example data files written viawrite.data.path(common with the object-storage layout, as in the issue report), or files kept from a previous table location. A single such file aborts the whole GC run during the expire phase:We hit this in production against a large repository (the failing content had ~1.7M files); the issue reporter hit the same with pyiceberg +
write.data.path.Change
Such files can never be expressed relative to the content's base location, so they are now tracked by their absolute URI instead of failing:
FileReferencenow allows an absolutepath()for files outsidebase()(base()itself must still be absolute).absolutePath()already resolves this case correctly (StorageUri.resolvereturns an absolute child unchanged), and the file deleters only ever useabsolutePath().IcebergContentToFiles.extractFilesRelativizeneeds no code change:StorageUri.relativizealready passes non-relativizable URIs through unchanged.PerContentDeleteExpiredputs those files into the live-files bloom filter under their absolute URI, and — only when at least one such reference exists for the content — the sweep additionally matches listed files by their absolute URI. This keeps a live file that is referenced absolutely but happens to be located under one of the content's swept base locations (e.g. an older table location) from being wrongly deleted. AWARNwith the per-content count of absolute references is logged.Deliberately not in scope: adding locations declared via table properties (
write.data.path,write.metadata.path) as swept base locations. Such locations can be shared by multiple tables (e.g. an object-storage-layout data path at the bucket root, as in the issue report), and sweeping them with a single content's live set would delete other contents' live files.Testing
TestFileReference— relaxed invariant (relative path unchanged, absolute path accepted, relative base still rejected).TestIcebergContentToFiles.filesOutsideBaseLocation— table metadata whoselocationdoes not cover the actual files; extraction yields absolute-path references through real metadata/manifest parsing instead of throwing.TestExpireAbsolutePaths— end-to-end expire: a live file referenced absolutely that is physically under the swept base location is not deleted, an unreferenced file still is, and the outside location is not added as a base location. This test fails without the sweep-side absolute-URI match.ITContentToFilesCrossCheck,ITSparkIcebergNessieS3,ITSparkIcebergNessieCLI) pass unchanged.The same change (applied onto 0.107.5) ships in an internally patched image for the production deployment that hit this failure.