Skip to content

fix: common mechanism for retries in insert, upsert and delete - #347

Open
konstantinoscs wants to merge 1 commit into
feature/tow-2494-python-compat-matrixfrom
feature/tow-2494-commit-retry-backoff
Open

fix: common mechanism for retries in insert, upsert and delete#347
konstantinoscs wants to merge 1 commit into
feature/tow-2494-python-compat-matrixfrom
feature/tow-2494-commit-retry-backoff

Conversation

@konstantinoscs

@konstantinoscs konstantinoscs commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What this does

This PR makes table writes more resilient when two writers try to commit at the same time.

Insert, upsert, and delete now share one retry mechanism. After a genuine Iceberg commit conflict, the SDK waits for a small randomized delay, refreshes the table metadata, and tries again. The delay grows between attempts and is capped at 30 seconds.

Safety

Only a known CommitFailedException is retried. Authentication, permission, network, validation, not-found, and unknown-commit-state errors still fail immediately, because replaying those operations could duplicate or hide a write.

This does not change catalog routing or credential vending, so S3 Tables and BYO behavior is preserved.

Testing

Tests cover all three write operations, retry timing and ordering, exhausted retries, refresh failures, and errors that must not be retried.

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3ec0bee0-e60c-4d8e-b195-43fd88d536f3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@konstantinoscs
konstantinoscs force-pushed the feature/tow-2494-commit-retry-backoff branch from a0a35b9 to d583fea Compare August 13, 2026 13:52
@konstantinoscs
konstantinoscs force-pushed the feature/tow-2494-commit-retry-backoff branch 2 times, most recently from e576dea to 96e2b74 Compare August 13, 2026 17:39
@konstantinoscs
konstantinoscs force-pushed the feature/tow-2494-commit-retry-backoff branch from 96e2b74 to 9b76e9b Compare August 13, 2026 19:18
@konstantinoscs konstantinoscs changed the title fix: add bounded jitter to commit retries fix: common mechanism for retries in insert, upsert and delete Aug 13, 2026
@konstantinoscs
konstantinoscs marked this pull request as ready for review August 13, 2026 19:19
Comment thread src/tower/_tables.py
Comment on lines +255 to +256
if not math.isfinite(retry_delay_seconds) or retry_delay_seconds < 0:
raise ValueError("retry_delay_seconds must be finite and >= 0")

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.

I would suggest to give an upper bound (maybe 300 seconds / 5 minutes?), rather than only checking if the delay is finite.

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.

It's a bit confusing because the upper bound is anyway decided in retry_ceiling = min(retry_delay_seconds, _MAX_COMMIT_RETRY_DELAY_SECONDS) inside _commit_with-retry_().
I'll add some clarification in the documentation for this

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.

If it's handled somewhere else, I would just be consistent with the above line then - retry_delay_seconds < 0 is probably enough here.

Comment thread src/tower/_tables.py Outdated
self,
operation: Callable[[], TRetryResult],
max_retries: int,
retry_delay_seconds: float,

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.

I would suggest to use an int here, rather than float, unless there's a strong need for sub second delays.

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.

So the default value is itself a float (0.5). Also the randomized backoff produces fractional values.
I think float is the correct thing here

Comment thread src/tower/_tables.py
return operation()
except CommitFailedException:
if attempt == max_retries:
raise

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.

I would raise a new exception, like CommitRetryExhaustionException.

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.

This should remain CommitFailedException because it's actually the PyIceberg exception.
Preserving that exception:

  1. retains the original failure details;
  2. keeps existing except CommitFailedException handlers working;
  3. avoids introducing Tower-specific behavior for S3 Tables and BYO callers.

A new exception would be a breaking change unless it subclassed CommitFailedException, and even then it adds little value.

Comment thread src/tower/_tables.py Outdated
if attempt == max_retries:
raise

delay = random.uniform(0.0, retry_ceiling)

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.

Hmm. The way that this is used, compared to what the naming of the variable is, could be confusing (I was not sure why we have this jitter when we're using retry_delay_seconds). I'd suggest to rename retry_delay_seconds to max_retry_delay to make it clear it's the maximum, not the actual delay.

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.

Actually, max_retry_delay is also inaccurate: the supplied value is only the maximum for the first retry; later ceilings grow until the separate 30-second cap we discussed above.

The biggest problem though is that It is also an existing public keyword across insert, upsert, and delete, so renaming it breaks callers on every catalog type.
I’d keep retry_delay_seconds, improve its documentation, and use initial_retry_ceiling_seconds internally

@konstantinoscs
konstantinoscs force-pushed the feature/tow-2494-commit-retry-backoff branch 2 times, most recently from 3055dd3 to 8e793a7 Compare August 14, 2026 14:26

@socksy socksy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ah nice

@konstantinoscs
konstantinoscs force-pushed the feature/tow-2494-commit-retry-backoff branch from 8e793a7 to 26243f8 Compare August 14, 2026 15:34
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.

3 participants