Repository navigation
Support deleting unreferenced files after snapshot expiration - #4201
Open
dragonknightk wants to merge 1 commit into
Open
dragonknightk wants to merge 1 commit into
dragonknightk wants to merge 1 commit into
Conversation
Closes issue 2604 # Rationale for this change `expire_snapshots` only rewrites the table metadata, so files that only expired snapshots referenced stay on the file system. Trino and Spark both clean those up. This adds an opt-in `clean_expired_files()` step, following the approach agreed in the issue: after the expiration has committed, compare the expired snapshots against the snapshots left in the table and delete the difference, without listing the object store. ʕ•ᴥ•ʔ ### How files are chosen Similar to Iceberg Java's `ReachableFileCleanup`, but more conservative: 1. **Candidates:** entries (including DELETED entries) of manifests used by the expired snapshots and by no snapshot left in the table. 2. **Subtract:** if there are candidates, read the live entries of every manifest left in the table and remove their paths. 3. **Abort on read failure:** if any manifest list or manifest cannot be read, nothing is deleted. Read cost: - When snapshots are selected, the manifest lists of the expired and the remaining snapshots are read. - A table written only with fast appends reads no manifests, because no manifest goes away. - Reads run in parallel on the shared executor, and only file paths are kept in memory. ### Concurrent ref changes Without a check, the catalog would drop a branch or tag that another writer created on a snapshot being expired, and the cleanup would delete files that ref still needs. So `commit()` reloads the latest table metadata first: | Change by another writer | Result | |---|---| | Before the reload: a branch or tag points at a selected snapshot | `ValueError`, nothing is expired | | After the reload: a branch or tag other than `main` is moved or removed | commit fails (`AssertRefSnapshotId`), nothing is deleted | | After the reload: an append to `main` | expiration succeeds (`main` is not asserted) | | After the reload: a new branch or tag on a selected snapshot, or `main` reset to one | **not detected**: the catalog drops that ref with the snapshot, and its files are deleted | ### `gc.enabled` differs from Java Java rejects any expiration when `gc.enabled` is false, because its expiration deletes files by default. Here, expiring without `clean_expired_files()` only changes the metadata, so it stays allowed; only `clean_expired_files()` raises a `ValueError`. This matches the consensus in the issue that deleting files is opt-in. The value is read from the latest table metadata. ## Are these changes tested? Yes, in `tests/table/test_expire_snapshots.py`: - **Default:** no call, or `enabled=False` after enabling, deletes nothing. - **Deleted vs kept:** see the table under user-facing changes, including files still used by retained snapshots, tags, or snapshots another writer committed. - **Read cost:** a shared manifest is read once and only for live entries; a fast-append history reads no manifests. - **Concurrent refs:** every row of the table above except the undetected one, including a table object refreshed after the selection. - **Failures:** commit failure deletes nothing; a failed delete is skipped and logged; a corrupt retained manifest stops the cleanup while the expiration stays committed; autocommit and `gc.enabled` checks. ## Are there any user-facing changes? Yes. `ExpireSnapshots` gets a new `clean_expired_files(enabled: bool = True)` method: ```python table.maintenance.expire_snapshots().clean_expired_files().older_than(cutoff).commit() ``` After the expiration has committed: | Deleted | Kept | |---|---| | Data and delete files of the expired snapshots that no remaining snapshot references as live | Files any remaining snapshot references as live | | Manifests and manifest lists of the expired snapshots that no remaining snapshot uses | Previous metadata files | | Statistics and partition statistics files of the expired snapshots | Statistics and partition statistics files the metadata after the commit still references | | | Orphan files no snapshot ever referenced | On failure: - A file whose deletion raises an `OSError` is skipped and a warning is logged. - A manifest list or manifest that cannot be read: no files are deleted. - Any other exception: files not deleted yet are kept. - In both of the last two cases, a warning with the traceback is logged and the expiration stays committed. The expired snapshots are gone from the metadata, so the cleanup cannot be rerun; leftover files can only be removed by an orphan file cleanup in another engine, such as Spark's `remove_orphan_files` procedure. Requirements (otherwise `commit()` raises a `ValueError` before anything is expired): - an autocommit transaction - `gc.enabled = true` in the latest table metadata - no branch or tag in the latest table metadata pointing at a selected snapshot Without `clean_expired_files()`, or with `enabled=False`, `expire_snapshots` behaves as before (๑•̀ㅂ•́)و✧ The expire-snapshots section of `mkdocs/docs/api.md` documents the new option. ## AI Disclosure Human-led contribution. Grok Bot, working for dragonknightk, assisted with this change. All submitted content was reviewed by a human before submission.
dragonknightk
force-pushed
the
fix/2604-expire-snapshots-delete-files
branch
from
October 30, 2026 08:54
2712a5e to
b152031
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2604
Rationale for this change
expire_snapshotsonly rewrites the table metadata, so files that only expired snapshots referenced stay on the file system. Trino and Spark both clean those up.This adds an opt-in
clean_expired_files()step, following the approach agreed in the issue: after the expiration has committed, compare the expired snapshots against the snapshots left in the table and delete the difference, without listing the object store. ʕ•ᴥ•ʔHow files are chosen
Similar to Iceberg Java's
ReachableFileCleanup, but more conservative:Read cost:
Concurrent ref changes
Without a check, the catalog would drop a branch or tag that another writer created on a snapshot being expired, and the cleanup would delete files that ref still needs. So
commit()reloads the latest table metadata first:ValueError, nothing is expiredmainis moved or removedAssertRefSnapshotId), nothing is deletedmainmainis not asserted)mainreset to onegc.enableddiffers from JavaJava rejects any expiration when
gc.enabledis false, because its expiration deletes files by default. Here, expiring withoutclean_expired_files()only changes the metadata, so it stays allowed; onlyclean_expired_files()raises aValueError. This matches the consensus in the issue that deleting files is opt-in. The value is read from the latest table metadata.Are these changes tested?
Yes, in
tests/table/test_expire_snapshots.py:enabled=Falseafter enabling, deletes nothing.gc.enabledchecks.Are there any user-facing changes?
Yes.
ExpireSnapshotsgets a newclean_expired_files(enabled: bool = True)method:After the expiration has committed:
On failure:
OSErroris skipped and a warning is logged.remove_orphan_filesprocedure.Requirements (otherwise
commit()raises aValueErrorbefore anything is expired):gc.enabled = truein the latest table metadataWithout
clean_expired_files(), or withenabled=False,expire_snapshotsbehaves as before (๑•̀ㅂ•́)و✧ The expire-snapshots section ofmkdocs/docs/api.mddocuments the new option.AI Disclosure
Human-led contribution. Grok Bot, working for dragonknightk, assisted with this change. All submitted content was reviewed by a human before submission.