This is an automated email from the ASF dual-hosted git repository.
github-actions[bot] pushed a commit to branch gh-pages
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git
The following commit(s) were added to refs/heads/gh-pages by this push:
new 45e8d570e deploy: 94102c5cb24f437a7c33e6bf53e2acb4efacdcc5
45e8d570e is described below
commit 45e8d570ee61cee0625d6b37657def42d5c3d74e
Author: laskoviymishka <[email protected]>
AuthorDate: Sun Nov 8 21:17:17 2026 +0000
deploy: 94102c5cb24f437a7c33e6bf53e2acb4efacdcc5
---
concurrent-writes.html | 42 ++++++++++++++++++++++++++++++++++--------
print.html | 45 +++++++++++++++++++++++++++++++++++++--------
releases.html | 3 +++
searchindex.js | 2 +-
searchindex.json | 2 +-
5 files changed, 76 insertions(+), 18 deletions(-)
diff --git a/concurrent-writes.html b/concurrent-writes.html
index ba91b2d3f..bf7a8e53e 100644
--- a/concurrent-writes.html
+++ b/concurrent-writes.html
@@ -289,11 +289,11 @@ rejects when concurrent deletes touch the same files.</p>
<p>iceberg-go can refresh the table and replay the operation against the latest
snapshot between attempts. On each retry the table metadata is reloaded, the
producer's validators re-run against the fresh snapshot, and the commit is
-re-submitted only if it is still valid. <code>RollbackToSnapshot</code>,
<code>ExpireSnapshots</code>
-and <code>AssertRefSnapshotID</code> pin the refs they guard, so a retry does
not rebase
-those assertions. If a guarded ref has moved, the commit fails with
-<code>ErrCommitFailed</code> instead of retrying. An
<code>ExpireSnapshots</code> that removes nothing
-adds no guards.</p>
+re-submitted only if it is still valid. <code>RollbackToSnapshot</code> and
+<code>AssertRefSnapshotID</code> pin their refs, so retries do not rebase
those assertions.
+If a pinned ref moved, the commit fails with <code>ErrCommitFailed</code>
instead of
+retrying. <code>ExpireSnapshots</code> does not pin refs. Retries replan it as
described in
+<a href="#snapshot-expiration">Snapshot expiration</a>.</p>
<p>Retry is <strong>off by default</strong>
(<code>commit.retry.num-retries</code> is <code>0</code>). Opt in by
setting the retry properties — see <a
href="./configuration.html#manifest-and-commit">Manifest and commit</a>
in the configuration reference for the full list:</p>
@@ -328,9 +328,35 @@ retries disabled always has. An abandoned manifest rewrite
also leaves the
manifests that later attempts merged. <code>TableCommit</code> refuses a
transaction whose
commit failed, since the multi-table path does not refresh. Call
<code>Commit</code> to
resume it, or build a new transaction.</p>
-<p>Because an <code>ExpireSnapshots</code> that removes something pins the
commit branch, a
-transaction that also appends fails instead of rebasing onto a peer's append.
-This is deliberate, since the expiry was computed from the old head.</p>
+<h3 id="snapshot-expiration"><a class="header"
href="#snapshot-expiration">Snapshot expiration</a></h3>
+<p>A retry replays staged changes in order against the refreshed table. It
replans
+each <code>ExpireSnapshots</code> call at its original position, with the
original options
+and cutoff time. The plan retains snapshots that a concurrent commit made
+reachable, such as rollback targets and new branch heads. It can expire
+snapshots that a concurrent append moved outside the retention window. A
+transaction that appends and expires snapshots rebases onto a concurrent append
+like any other append.</p>
+<p>Each attempt requires the refs read by expiries that remove snapshots or
refs
+to be unchanged since loading or refreshing the table. An expiry that removes
+nothing adds no such requirement. If a concurrent commit moves one of those
+refs before the attempt succeeds, it causes another conflict. The next attempt
+replans the expiry. If a concurrent commit creates a ref that the plan did not
+read, that ref does not cause a conflict. Expiring its snapshot also removes
+the ref.</p>
+<ul>
+<li>Retries replan an expiry even if staging or an earlier attempt produced no
+expiry updates.</li>
+<li>If no updates remain, for example because a concurrent commit already
expired
+the same snapshots, <code>Commit</code> returns the refreshed table without
committing.</li>
+<li>A transaction containing only expiries with no updates makes no commit
attempt
+and never refreshes or replans.</li>
+<li>Post-commit cleanup deletes files for snapshots expired by the successful
+attempt, if cleanup is enabled.</li>
+</ul>
+<p>A retry fails with <code>ErrTransactionUnusable</code> if GC is disabled
after refresh, a
+staged change no longer applies, or replanning changes whether a non-main
+commit branch is removed. <code>ErrTransactionUnusable</code> and
<code>ErrCommitFailed</code> list
+these cases and which of them a new transaction can retry.</p>
</main>
diff --git a/print.html b/print.html
index 5a807e57e..07df51b55 100644
--- a/print.html
+++ b/print.html
@@ -2106,11 +2106,11 @@ rejects when concurrent deletes touch the same
files.</p>
<p>iceberg-go can refresh the table and replay the operation against the latest
snapshot between attempts. On each retry the table metadata is reloaded, the
producer's validators re-run against the fresh snapshot, and the commit is
-re-submitted only if it is still valid. <code>RollbackToSnapshot</code>,
<code>ExpireSnapshots</code>
-and <code>AssertRefSnapshotID</code> pin the refs they guard, so a retry does
not rebase
-those assertions. If a guarded ref has moved, the commit fails with
-<code>ErrCommitFailed</code> instead of retrying. An
<code>ExpireSnapshots</code> that removes nothing
-adds no guards.</p>
+re-submitted only if it is still valid. <code>RollbackToSnapshot</code> and
+<code>AssertRefSnapshotID</code> pin their refs, so retries do not rebase
those assertions.
+If a pinned ref moved, the commit fails with <code>ErrCommitFailed</code>
instead of
+retrying. <code>ExpireSnapshots</code> does not pin refs. Retries replan it as
described in
+<a href="concurrent-writes.html#snapshot-expiration">Snapshot
expiration</a>.</p>
<p>Retry is <strong>off by default</strong>
(<code>commit.retry.num-retries</code> is <code>0</code>). Opt in by
setting the retry properties — see <a
href="./configuration.html#manifest-and-commit">Manifest and commit</a>
in the configuration reference for the full list:</p>
@@ -2145,9 +2145,35 @@ retries disabled always has. An abandoned manifest
rewrite also leaves the
manifests that later attempts merged. <code>TableCommit</code> refuses a
transaction whose
commit failed, since the multi-table path does not refresh. Call
<code>Commit</code> to
resume it, or build a new transaction.</p>
-<p>Because an <code>ExpireSnapshots</code> that removes something pins the
commit branch, a
-transaction that also appends fails instead of rebasing onto a peer's append.
-This is deliberate, since the expiry was computed from the old head.</p>
+<h3 id="snapshot-expiration"><a class="header"
href="#snapshot-expiration">Snapshot expiration</a></h3>
+<p>A retry replays staged changes in order against the refreshed table. It
replans
+each <code>ExpireSnapshots</code> call at its original position, with the
original options
+and cutoff time. The plan retains snapshots that a concurrent commit made
+reachable, such as rollback targets and new branch heads. It can expire
+snapshots that a concurrent append moved outside the retention window. A
+transaction that appends and expires snapshots rebases onto a concurrent append
+like any other append.</p>
+<p>Each attempt requires the refs read by expiries that remove snapshots or
refs
+to be unchanged since loading or refreshing the table. An expiry that removes
+nothing adds no such requirement. If a concurrent commit moves one of those
+refs before the attempt succeeds, it causes another conflict. The next attempt
+replans the expiry. If a concurrent commit creates a ref that the plan did not
+read, that ref does not cause a conflict. Expiring its snapshot also removes
+the ref.</p>
+<ul>
+<li>Retries replan an expiry even if staging or an earlier attempt produced no
+expiry updates.</li>
+<li>If no updates remain, for example because a concurrent commit already
expired
+the same snapshots, <code>Commit</code> returns the refreshed table without
committing.</li>
+<li>A transaction containing only expiries with no updates makes no commit
attempt
+and never refreshes or replans.</li>
+<li>Post-commit cleanup deletes files for snapshots expired by the successful
+attempt, if cleanup is enabled.</li>
+</ul>
+<p>A retry fails with <code>ErrTransactionUnusable</code> if GC is disabled
after refresh, a
+staged change no longer applies, or replanning changes whether a non-main
+commit branch is removed. <code>ErrTransactionUnusable</code> and
<code>ErrCommitFailed</code> list
+these cases and which of them a new transaction can retry.</p>
<div style="break-before: page; page-break-before: always;"></div><!--
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
@@ -2777,6 +2803,9 @@ make integration-external-rest
<p><strong>Branch transactions return initialization errors.</strong>
<code>Table.NewTransactionOnBranch(branch)</code> now returns
<code>(*Transaction, error)</code>, and
<code>Table.NewTransactionOnBranchWithError</code> is removed. Use
<code>NewTransactionOnBranch</code> and handle its error.</p>
</li>
<li>
+<p><strong>Snapshot expiration now tolerates concurrent ref changes.</strong>
If a concurrent commit moves a ref that the expiry read, a retry or later
<code>Commit</code> replans instead of failing with
<code>table.ErrCommitFailed</code>. The committed expiry can differ from
<code>Transaction.StagedTable</code>. See <a
href="./concurrent-writes.html#snapshot-expiration">Snapshot expiration</a> for
cases that now fail with <code>table.ErrTransactionUnusable</code>.</p>
+</li>
+<li>
<p><strong>Schema JSON constructor uses the Go initialism.</strong> Replace
<code>iceberg.NewSchemaFromJsonFields</code> with
<code>iceberg.NewSchemaFromJSONFields</code>.</p>
</li>
<li>
diff --git a/releases.html b/releases.html
index ea990ba85..64eb83a45 100644
--- a/releases.html
+++ b/releases.html
@@ -309,6 +309,9 @@
<p><strong>Branch transactions return initialization errors.</strong>
<code>Table.NewTransactionOnBranch(branch)</code> now returns
<code>(*Transaction, error)</code>, and
<code>Table.NewTransactionOnBranchWithError</code> is removed. Use
<code>NewTransactionOnBranch</code> and handle its error.</p>
</li>
<li>
+<p><strong>Snapshot expiration now tolerates concurrent ref changes.</strong>
If a concurrent commit moves a ref that the expiry read, a retry or later
<code>Commit</code> replans instead of failing with
<code>table.ErrCommitFailed</code>. The committed expiry can differ from
<code>Transaction.StagedTable</code>. See <a
href="./concurrent-writes.html#snapshot-expiration">Snapshot expiration</a> for
cases that now fail with <code>table.ErrTransactionUnusable</code>.</p>
+</li>
+<li>
<p><strong>Schema JSON constructor uses the Go initialism.</strong> Replace
<code>iceberg.NewSchemaFromJsonFields</code> with
<code>iceberg.NewSchemaFromJSONFields</code>.</p>
</li>
<li>
diff --git a/searchindex.js b/searchindex.js
index 8db8f1c5e..8506c01c2 100644
--- a/searchindex.js
+++ b/searchindex.js
@@ -1 +1 @@
-Object.assign(window.search,
{"doc_urls":["introduction.html#iceberg-go","introduction.html#where-to-start","introduction.html#what-works-today","introduction.html#beyond-go","introduction.html#help-and-contribution","install.html#install","install.html#requirements","install.html#installation","getting-started.html#getting-started","getting-started.html#1-install","getting-started.html#2-open-a-local-catalog","getting-started.html#3-create-a-namespace-and-a-table","getting-started.html#
[...]
\ No newline at end of file
+Object.assign(window.search,
{"doc_urls":["introduction.html#iceberg-go","introduction.html#where-to-start","introduction.html#what-works-today","introduction.html#beyond-go","introduction.html#help-and-contribution","install.html#install","install.html#requirements","install.html#installation","getting-started.html#getting-started","getting-started.html#1-install","getting-started.html#2-open-a-local-catalog","getting-started.html#3-create-a-namespace-and-a-table","getting-started.html#
[...]
\ No newline at end of file
diff --git a/searchindex.json b/searchindex.json
index abc44bf7f..fe9cd28e3 100644
--- a/searchindex.json
+++ b/searchindex.json
@@ -1 +1 @@
-{"doc_urls":["introduction.html#iceberg-go","introduction.html#where-to-start","introduction.html#what-works-today","introduction.html#beyond-go","introduction.html#help-and-contribution","install.html#install","install.html#requirements","install.html#installation","getting-started.html#getting-started","getting-started.html#1-install","getting-started.html#2-open-a-local-catalog","getting-started.html#3-create-a-namespace-and-a-table","getting-started.html#4-write-some-arrow-data","get
[...]
\ No newline at end of file
+{"doc_urls":["introduction.html#iceberg-go","introduction.html#where-to-start","introduction.html#what-works-today","introduction.html#beyond-go","introduction.html#help-and-contribution","install.html#install","install.html#requirements","install.html#installation","getting-started.html#getting-started","getting-started.html#1-install","getting-started.html#2-open-a-local-catalog","getting-started.html#3-create-a-namespace-and-a-table","getting-started.html#4-write-some-arrow-data","get
[...]
\ No newline at end of file