Repository navigation
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds support for expiring stale snapshot refs (branches/tags) based on max-ref-age-ms, bringing PyIceberg’s snapshot expiration behavior closer to Iceberg’s retention policy semantics and preventing refs from pinning snapshots indefinitely.
Changes:
- Add
ExpireSnapshots.remove_expired_refs()to drop stale refs based on per-refmax-ref-age-mswith fallback tohistory.expire.max-ref-age-ms. - Make
older_than()resolve at commit time so chaining order withremove_expired_refs()does not affect which snapshots become eligible for expiration. - Add unit tests and API documentation for expiring branches/tags and the new table property.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| tests/table/test_expire_snapshots.py | Adds coverage for ref expiry semantics (expired/unexpired refs, opt-in behavior, order independence, table-property fallback, main exemption). |
| pyiceberg/table/update/snapshot.py | Implements ref expiry staging and defers older_than() evaluation to commit to make builder chaining order-independent. |
| pyiceberg/table/init.py | Introduces TableProperties.MAX_REF_AGE_MS and its default. |
| mkdocs/docs/api.md | Documents how to expire branches/tags and reclaim pinned snapshots. |
Suppressed comments (2)
tests/table/test_expire_snapshots.py:409
- This test creates a fixed namespace name in a session-scoped warehouse; if another test already created it, create_namespace will raise NamespaceAlreadyExistsError. Use create_namespace_if_not_exists (or randomize the namespace) to keep the suite order-independent.
catalog_with_warehouse.create_namespace("expire_refs")
tests/table/test_expire_snapshots.py:433
- This test creates a fixed namespace name in a session-scoped warehouse; if another test already created it, create_namespace will raise NamespaceAlreadyExistsError. Use create_namespace_if_not_exists (or randomize the namespace) to keep the suite order-independent.
catalog_with_warehouse.create_namespace("expire_refs")
| MAX_REF_AGE_MS = "history.expire.max-ref-age-ms" | ||
| MAX_REF_AGE_MS_DEFAULT = sys.maxsize |
There was a problem hiding this comment.
Fixed in ca43097. sys.maxsize is 2**31-1 on a 32-bit build, so the default would have been ~25 days rather than unbounded — pinned to 2**63-1 to match Java's Long.MAX_VALUE.
|
|
||
| Returns the reloaded table and the snapshot id the branch pins. | ||
| """ | ||
| catalog_with_warehouse.create_namespace("expire_refs") |
There was a problem hiding this comment.
Fixed in ca43097 — each test now uses its own namespace. The collision does not actually occur today, since catalog_with_warehouse calls destroy_tables() on teardown, but the tests should not depend on that for isolation.
7a86150 to
ca43097
Compare
create_branch() and create_tag() accept max_ref_age_ms and write it to table metadata, but nothing in pyiceberg acts on it. Since a live ref protects its snapshot from expiry, a stale ref pins that snapshot and its ancestors indefinitely. Add ExpireSnapshots.remove_expired_refs(), implementing step 2 of the spec's snapshot retention policy: drop refs other than main whose referenced snapshot is older than max-ref-age-ms, falling back to the new history.expire.max-ref-age-ms table property. Refs pointing at a snapshot that no longer exists are removed too. older_than() resolves its snapshot set at commit time so that snapshots released by remove_expired_refs() are reclaimed in the same commit regardless of the order the two are chained in. Signed-off-by: 1fanwang <1fannnw@gmail.com>
ca43097 to
5254245
Compare
|
This pull request has been marked as stale due to 30 days of inactivity. It will be closed in 1 week if no further activity occurs. If you think that's incorrect or this pull request requires a review, please simply write any comment. If closed, you can revive the PR at any time and @mention a reviewer or discuss it on the dev@iceberg.apache.org list. Thank you for your contributions. |
|
@Fokko could you review when you have time? |
rambleraptor
left a comment
There was a problem hiding this comment.
One quick note after looking at the Java implementation.
| # Remove any protected snapshot IDs from the set to expire, just in case | ||
| self._snapshot_ids_to_expire -= protected_ids | ||
| for ref_name in self._ref_names_to_remove: | ||
| self._updates += (RemoveSnapshotRefUpdate(ref_name=ref_name),) |
There was a problem hiding this comment.
I think we need to also add a AssertRefSnapshotId requirement. This helps ensure that if a ref is expired, it's never checked against what's in the catalog at commit time.
There was a problem hiding this comment.
Done in b1bd6ed. Each removed ref now asserts the snapshot it pointed at when it was judged expired, so a branch that advanced meanwhile fails the commit.
A ref judged expired can advance before the commit lands, and removing it then drops a live branch. Each removal now asserts the snapshot id the ref had when it was selected, the same check ManageSnapshots uses for remove_branch and remove_tag. Signed-off-by: 1fanwang <1fannnw@gmail.com>
Signed-off-by: 1fanwang <1fannnw@gmail.com>
Main now requires Python 3.11, so ruff flags timezone.utc as UP017 in the lines this change adds. Signed-off-by: 1fanwang <1fannnw@gmail.com>
Rationale for this change
Age-based snapshot expiration leaves stale branches and tags holding snapshots. This adds an opt-in step to expire those refs so their snapshots can expire too.
Ref age uses the snapshot's timestamp and its retention limit or the table-property fallback. Main never expires; dangling refs are removed. Each removal asserts the snapshot its ref had when it was judged expired, so a branch that advanced meanwhile fails the commit instead of being dropped.
Builds on #3246.
Are these changes tested?
Python 3.12, PyArrow 25.0.1. The probe builds an expired branch in SQLite with the test helper and commits the expiration. With RACE set, a second writer commits to that branch after expiry was planned. It skips ref expiry on code without that API. In the PR checkout, run uv sync --group dev --extra pyarrow --extra sql-sqlite first.
Reproducer source: probe.py
REST and Hive cases were not rerun.
Are there any user-facing changes?
Yes: opt-in ref expiry and history.expire.max-ref-age-ms, documented in the API guide.
AI disclosure: GitHub Copilot CLI (GPT-6 Astra, Claude Sonnet 5.5) wrote this change and its tests, and an automated coordinator reviewed the diff and test output before submission.