Skip to content

Commit cadafef

Browse files
committed
Replan the manifest rewrite on commit retry and align rewrites_needed with it
1 parent 1a7ab16 commit cadafef

4 files changed

Lines changed: 85 additions & 6 deletions

File tree

‎mkdocs/docs/api.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1535,7 +1535,7 @@ Rewrite the current snapshot's data manifests without changing any data. Live en
15351535
table.maintenance.rewrite_manifests().commit()
15361536
```
15371537

1538-
Nothing is committed when there is only one data manifest, so the call is safe to schedule periodically. To find out in advance whether a rewrite would do anything:
1538+
Nothing is committed when no manifests would be merged, so the call is safe to schedule periodically. To find out in advance whether a rewrite would do anything:
15391539

15401540
```python
15411541
if table.maintenance.rewrite_manifests().rewrites_needed():

‎pyiceberg/table/update/snapshot.py‎

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1452,6 +1452,18 @@ def _existing_manifests(self) -> list[ManifestFile]:
14521452
self._computed_manifests = new_manifests + kept_manifests
14531453
return self._computed_manifests
14541454

1455+
def _refresh_for_retry(self) -> None:
1456+
"""Reset state for a retry attempt, discarding the plan built from the stale branch head."""
1457+
super()._refresh_for_retry()
1458+
# The plan and its counters depend on the branch head, which changes on retry. Keeping them
1459+
# would rebuild the replacement snapshot from the manifests of the superseded snapshot,
1460+
# dropping whatever was committed concurrently.
1461+
self._computed_manifests = None
1462+
self._rewritten_count = 0
1463+
self._created_count = 0
1464+
self._kept_count = 0
1465+
self._entries_processed = 0
1466+
14551467
def _commit(self) -> UpdatesAndRequirements:
14561468
self._existing_manifests()
14571469
if self._created_count == 0:
@@ -1460,11 +1472,25 @@ def _commit(self) -> UpdatesAndRequirements:
14601472
return super()._commit()
14611473

14621474
def rewrites_needed(self) -> bool:
1463-
"""Return whether the current snapshot has data manifests to rewrite."""
1475+
"""Return whether committing would rewrite any manifest.
1476+
1477+
This mirrors the grouping that `_existing_manifests` performs, so a snapshot whose data
1478+
manifests all end up kept as-is reports False. A predicate that matches only manifests
1479+
holding no live entries still reports True, because deciding that requires reading them.
1480+
"""
14641481
snapshot = self._transaction.table_metadata.snapshot_by_name(self._target_branch or MAIN_BRANCH)
14651482
if snapshot is None:
14661483
return False
1467-
data_manifests = [m for m in snapshot.manifests(self._io) if m.content == ManifestContent.DATA]
1468-
if self._manifest_predicate is not None:
1469-
return any(self._manifest_predicate(m) for m in data_manifests)
1470-
return len(data_manifests) > 1
1484+
1485+
data_manifests_by_spec: defaultdict[int, list[ManifestFile]] = defaultdict(list)
1486+
for manifest in snapshot.manifests(self._io):
1487+
if manifest.content == ManifestContent.DATA and (
1488+
self._manifest_predicate is None or self._manifest_predicate(manifest)
1489+
):
1490+
data_manifests_by_spec[manifest.partition_spec_id].append(manifest)
1491+
1492+
return any(
1493+
len(group) > 1 or self._manifest_predicate is not None
1494+
for manifests in data_manifests_by_spec.values()
1495+
for group in self._group_by_target_size(manifests)
1496+
)

‎tests/table/test_commit_retry.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,40 @@ def test_delete_files_refresh_clears_compute_deletes_cache(catalog: Catalog) ->
302302
assert "_compute_deletes" not in producer.__dict__
303303

304304

305+
def test_rewrite_manifests_refresh_clears_computed_manifests(catalog: Catalog) -> None:
306+
"""Verify that _refresh_for_retry discards the plan RewriteManifests computed for the old head."""
307+
catalog.create_namespace("default")
308+
schema = _test_schema()
309+
table = catalog.create_table("default.rewrite_cache_test", schema=schema)
310+
311+
import pyarrow as pa
312+
313+
table.append(pa.table({"x": [1, 2, 3]}))
314+
table.append(pa.table({"x": [4, 5, 6]}))
315+
table = catalog.load_table("default.rewrite_cache_test")
316+
317+
from pyiceberg.table.update.snapshot import RewriteManifests
318+
319+
tx = Transaction(table, autocommit=False)
320+
producer = RewriteManifests(transaction=tx, io=table.io)
321+
322+
# Plan against the current head, populating the cache and the counters
323+
planned = producer._existing_manifests()
324+
325+
assert planned
326+
assert producer._created_count == 1
327+
assert producer._rewritten_count == 2
328+
329+
producer._refresh_for_retry()
330+
331+
# Reusing either would rebuild the replacement snapshot from the superseded head
332+
assert producer._computed_manifests is None
333+
assert producer._created_count == 0
334+
assert producer._rewritten_count == 0
335+
assert producer._kept_count == 0
336+
assert producer._entries_processed == 0
337+
338+
305339
def test_concurrent_overwrite_overwrite_raises_validation_exception(catalog: Catalog) -> None:
306340
"""Concurrent overwrites on the same data should fail with ValidationException."""
307341
catalog.create_namespace("default")

‎tests/table/test_rewrite_manifests.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,3 +284,22 @@ def test_rewrite_manifests_merges_live_and_fully_deleted_manifests(catalog: Cata
284284

285285
# Verify table data is fully preserved and matches remaining live records
286286
assert table.scan().to_arrow().sort_by("id") == _arrow_table(offset=3)
287+
288+
289+
def test_rewrites_needed_is_false_when_every_group_is_a_single_manifest(catalog: Catalog) -> None:
290+
table = _create_table_with_appends(catalog, appends=3)
291+
# A tiny target size puts every manifest in its own group, so nothing can be merged
292+
with table.transaction() as tx:
293+
tx.set_properties({"commit.manifest.target-size-bytes": "1"})
294+
295+
table = catalog.load_table("default.test_rewrite")
296+
assert len(_data_manifests(table)) == 3
297+
298+
snapshot_before = table.current_snapshot()
299+
assert table.maintenance.rewrite_manifests().rewrites_needed() is False
300+
301+
table.maintenance.rewrite_manifests().commit()
302+
303+
table = catalog.load_table("default.test_rewrite")
304+
assert table.current_snapshot() == snapshot_before
305+
assert len(_data_manifests(table)) == 3

0 commit comments

Comments
 (0)