Skip to content

GC: handle Iceberg files outside the table's declared location - #12765

Merged
snazy merged 2 commits into
projectnessie:mainfrom
Beaglefoot:gc-absolute-locations-10817
Jul 21, 2026
Merged

snazy merged 2 commits into
projectnessie:mainfrom
Beaglefoot:gc-absolute-locations-10817

Conversation

@Beaglefoot

Copy link
Copy Markdown
Contributor

Fixes #10817

Problem

Nessie GC fails 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 (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:

Caused by: java.lang.IllegalArgumentException: Path must be relative: s3://bucket/path/to/file.parquet
    at org.projectnessie.gc.files.FileReference.check(FileReference.java:51)
    at org.projectnessie.gc.files.ImmutableFileReference.of(ImmutableFileReference.java:186)
    at org.projectnessie.gc.iceberg.IcebergContentToFiles.lambda$extractFilesRelativize$3(IcebergContentToFiles.java:200)
    ...
    at org.projectnessie.gc.expire.PerContentDeleteExpired.identifyLiveFiles(PerContentDeleteExpired.java:136)

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:

  • FileReference now allows an absolute path() for files outside base() (base() itself must still be absolute). absolutePath() already resolves this case correctly (StorageUri.resolve returns an absolute child unchanged), and the file deleters only ever use absolutePath(). IcebergContentToFiles.extractFilesRelativize needs no code change: StorageUri.relativize already passes non-relativizable URIs through unchanged.
  • PerContentDeleteExpired puts 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. A WARN with the per-content count of absolute references is logged.
  • Files outside all of a content's base locations are never listed, and therefore never deleted — GC completes and expires everything it can reach, instead of aborting.

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 whose location does 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.
  • Existing gc-base/gc-iceberg/gc-tool/gc-repository-jdbc unit and integration suites (incl. 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.

@CLAassistant

CLAassistant commented Jul 17, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

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>
@Beaglefoot
Beaglefoot force-pushed the gc-absolute-locations-10817 branch from 2d0093b to 75192b5 Compare July 17, 2026 08:41

@dimas-b dimas-b left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()))) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 26a7076.

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>
@dimas-b
dimas-b requested a review from snazy July 20, 2026 15:27
@snazy
snazy merged commit 578748d into projectnessie:main Jul 21, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Nessie GC is unable to handle data files outside the location declared in table's metadata

4 participants