laskoviymishka commented on code in PR #2100:
URL: https://github.com/apache/iceberg-go/pull/2100#discussion_r4208261469


##########
table/table.go:
##########
@@ -630,15 +631,27 @@ func (t Table) doCommit(ctx context.Context, updates 
[]Update, reqs []Requiremen
        // CommitTable, where the catalog may have silently accepted the commit 
and one
        // of the "orphaned" files may actually be the live snapshot.
        cleanupOrphans := true
+       committed := false
        defer func() {
-               if !cleanupOrphans || len(orphanedManifests) == 0 {
+               if !cleanupOrphans {
                        return
                }
+               // The final attempt's manifests are orphaned only if the 
commit failed.
+               for _, u := range updates {
+                       if su, ok := u.(*addSnapshotUpdate); ok && 
su.supersededSource != nil {
+                               orphanedManifests = append(orphanedManifests, 
su.supersededSource.supersededManifests(committed)...)

Review Comment:
   This fold used to run only after the retry loop. Moving it into the defer 
means it now fires on every non-committed exit: a refresh I/O blip, a cancelled 
context, a validator rejection on attempt 0. For a rewrite-manifests producer 
those paths now delete the staged manifests and wrap `ErrTransactionUnusable`, 
so a transient error destroys work the caller could previously re-commit. 
That's a behavior change outside this PR's stated scope, and nothing tests 
those early-return paths.
   
   I'd settle this deliberately: either keep the fold on the 
exhausted-`ErrCommitFailed` and terminal exits only, or add tests pinning the 
chosen semantics for a refresh error and a ctx cancel after a rebuild. 
`TestCommitAfterFailFastWithoutCleanupStaysRetriable` only covers the 
SetProperties case today, which has no superseded source to delete.



##########
table/transaction.go:
##########
@@ -781,14 +796,12 @@ func (t *Transaction) ExpireSnapshots(opts 
...ExpireSnapshotsOpt) error {
 
        retainedRefs := make(map[string]SnapshotRef, len(meta.refs))
        for refName, ref := range meta.refs {
-               // Assert the ref's base snapshot id so we don't accidentally
-               // expire snapshots that are now referenced by concurrently
-               // updated refs. Refs the transaction itself staged (absent on
-               // the base) get no assertion here: the update that created
-               // them carries its own base-state requirement.
+               // Pin refs to avoid expiring snapshots retained by a 
concurrent ref update.
+               // Refs staged by this transaction already have absence 
assertions.
                if id := t.baseRefSnapshotID(refName); id != nil {
                        reqs = append(reqs, AssertRefSnapshotID(refName, id))
                }
+               pinned = append(pinned, refName)

Review Comment:
   Pin a ref only when the expire actually removes something. Appending every 
ref to `pinned` here unconditionally is the round-2 regression, still present: 
on a no-op expire `applyPinned` publishes a pin on every ref including the 
commit branch, so an `Append` plus a non-expiring `ExpireSnapshots` in one 
transaction loses refresh-and-replay. A peer's non-conflicting append then 
fails the first refresh with `ErrCommitFailed` and the retry budget never gets 
used. Standalone it's harmless since `Commit` short-circuits on empty updates, 
but it bites as soon as the transaction carries other work.
   
   I'd pass `pinned` to `applyPinned` only when `len(snapsToDelete) > 0` (or a 
`RemoveSnapshotRef` was staged), and `nil` otherwise, then add a test: append 
on `main`, an expire that removes nothing, a peer append in between, asserting 
the commit still succeeds on retry. If pinning every ref is intended, let's say 
so in the comment.



##########
table/table.go:
##########
@@ -50,14 +50,17 @@ import (
 // commit fails due to a concurrent modification (e.g. HTTP 409 Conflict
 // from the REST catalog). Catalog implementations should wrap this
 // error so that callers using errors.Is(err, table.ErrCommitFailed)
-// can detect retryable commit conflicts.
-//
-// Currently only catalog/rest wraps this sentinel; Glue, SQL, and Hive
-// catalogs return their conflict errors raw and will not trigger
-// retries until follow-up work wires them through (tracked under
-// issue #830).
+// can detect retryable commit conflicts. Failed requirements also wrap it.

Review Comment:
   `ErrCommitFailed` now spans two cases a caller has to treat differently: a 
retry-safe head conflict, and a non-rebasable failure (stale 
schema/spec/sort-order, or a staging-time `Validate` in `applyLocked`) where 
re-committing the same `Transaction` only reproduces the identical error. The 
sentinel text still reads "refresh and try again" and `Commit` leaves the 
transaction re-committable on any `ErrCommitFailed`, so an external retry loop 
keyed on `errors.Is(err, ErrCommitFailed)` will spin on the non-rebasable case.
   
   The godoc here does call it out, which helps. But I'd give callers something 
to branch on without string-matching: either a distinct sentinel for "rebuild 
required," or mark the transaction non-re-committable after a non-rebased 
failure the same way the cleanup path already does with 
`ErrTransactionUnusable`. Not blocking the way the expire one is, but this is 
the public retry contract, so worth settling now.



-- 
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