lawofcycles commented on code in PR #3320:
URL: https://github.com/apache/iceberg-python/pull/3320#discussion_r3609739789


##########
pyiceberg/table/__init__.py:
##########
@@ -1043,17 +1084,82 @@ def commit_transaction(self) -> Table:
             The table with the updates applied.
         """
         if len(self._updates) > 0:
-            self._requirements += 
(AssertTableUUID(uuid=self.table_metadata.table_uuid),)
-            self._table._do_commit(  # pylint: disable=W0212
-                updates=self._updates,
-                requirements=self._requirements,
+            properties = self._table.metadata.properties
+            num_retries: int = property_as_int(  # type: ignore  # The default 
is set with non-None value.
+                properties, TableProperties.COMMIT_NUM_RETRIES, 
TableProperties.COMMIT_NUM_RETRIES_DEFAULT
             )
+            min_wait_ms: int = property_as_int(  # type: ignore  # The default 
is set with non-None value.
+                properties, TableProperties.COMMIT_MIN_RETRY_WAIT_MS, 
TableProperties.COMMIT_MIN_RETRY_WAIT_MS_DEFAULT
+            )
+            max_wait_ms: int = property_as_int(  # type: ignore  # The default 
is set with non-None value.
+                properties, TableProperties.COMMIT_MAX_RETRY_WAIT_MS, 
TableProperties.COMMIT_MAX_RETRY_WAIT_MS_DEFAULT
+            )
+            total_timeout_ms: int = property_as_int(  # type: ignore  # The 
default is set with non-None value.
+                properties, TableProperties.COMMIT_TOTAL_RETRY_TIME_MS, 
TableProperties.COMMIT_TOTAL_RETRY_TIME_MS_DEFAULT
+            )
+            start_time = time.monotonic()
+            self._requirements += 
(AssertTableUUID(uuid=self.table_metadata.table_uuid),)
+
+            try:
+                for attempt in range(num_retries + 1):
+                    try:
+                        self._table._do_commit(  # pylint: disable=W0212
+                            updates=self._updates,
+                            requirements=self._requirements,
+                        )
+                        self._cleanup_uncommitted_manifests()
+                        break
+                    except CommitFailedException:
+                        elapsed_ms = (time.monotonic() - start_time) * 1000
+                        if attempt == num_retries or not 
self._snapshot_producers or elapsed_ms >= total_timeout_ms:
+                            raise
+
+                        wait = min(min_wait_ms * (2**attempt), max_wait_ms)
+                        jitter = random.uniform(0, 0.1 * wait)
+                        logger.warning(
+                            "Commit failed due to a concurrent update, 
retrying (%s/%s) in %s ms",
+                            attempt + 1,
+                            num_retries,
+                            round(wait + jitter),
+                        )
+                        time.sleep((wait + jitter) / 1000.0)
+
+                        self._table.refresh()
+                        self._rebuild_snapshot_updates()
+            except Exception:
+                for producer in self._snapshot_producers:
+                    producer._clean_all_uncommitted()
+                raise
 
         self._updates = ()

Review Comment:
   Good catch on both. After a successful commit the transaction now clears its 
registered producers, so a later commit no longer replays the first batch. A 
failed commit marks the transaction as failed, so reusing it raises instead of 
committing updates that point at deleted files. Added both of your scenarios as 
regression tests.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to