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


##########
table/table.go:
##########
@@ -630,15 +632,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)...)
+                       }
+               }
                for _, path := range orphanedManifests {
                        if removeErr := wfs.Remove(path); removeErr != nil {
                                log.Printf("Warning: failed to delete orphaned 
manifest list %s: %v", path, removeErr)
                        }
                }
+               // Resubmitting staged updates after cleanup would reference 
deleted files.
+               if !committed && retErr != nil && len(orphanedManifests) > 0 {

Review Comment:
   This is the cleanup-on-early-return semantics I flagged last round, and the 
new `TestTransactionCommit_UnusableAfterTransientFailureFollowingRebuild` reads 
like you've settled on it being intended, which is fine, I just want it to be 
on purpose. A transient refresh error or a ctx cancel after a rebuilt attempt 
now deletes a rewrite producer's staged manifests and marks the transaction 
unusable, where before it stayed re-committable. If that's the contract, the 
wording should say so plainly: the `Commit` comment and `concurrent-writes.md` 
only mention "cleanup removed files" and "requirement failed after refresh", 
but this also fires on an ordinary transient error once a rebuild has happened.
   
   One thing nagging at me about the signal: the wrap keys on 
`len(orphanedManifests) > 0`, which is a side effect of whether a 
producer/rebuild ran, not whether staged updates actually lost files. It's 
correct today, but an attempt-0 failure with no rebuild wraps nothing while a 
rewrite producer in the same spot wraps, so a future change to what counts as 
an orphan would silently flip reusability. An explicit "staged files were 
deleted" bool set at the delete site would make the invariant hold regardless. 
wdyt?



##########
table/table.go:
##########
@@ -50,14 +50,18 @@ 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:
   Small one while we're in here: the opening still says `ErrCommitFailed` lets 
callers "detect retryable commit conflicts", but four lines down the same 
comment says an error matching both sentinels can't succeed on retry. I'd 
soften the opener to something like "retryable unless it also matches 
`ErrTransactionUnusable`" so the two halves don't read against each other.



##########
table/table.go:
##########
@@ -690,11 +704,9 @@ func (t Table) doCommit(ctx context.Context, updates 
[]Update, reqs []Requiremen
                        current = fresh.metadata
                        reqs = rewriteRefSnapshotRequirements(reqs, co.branch, 
current, co.pinnedRefs)
 
-                       // A pinned assertion the fresh catalog state violates 
can
-                       // never succeed — fail now instead of burning the 
remaining
-                       // retries on it.
-                       if err := validatePinnedRefRequirements(reqs, 
co.pinnedRefs, current); err != nil {
-                               return nil, fmt.Errorf("%w: explicit ref 
requirement failed: %w", ErrCommitFailed, err)
+                       if err := validateNonRebasedRequirements(reqs, 
co.branch, co.pinnedRefs, current); err != nil {
+                               return nil, fmt.Errorf("%w: requirement no 
longer holds after refresh: %w: %w",

Review Comment:
   This is the conflation thread from last round, and the text is where it 
bites hardest now. `ErrCommitFailed.Error()` is "commit failed, refresh and try 
again", so this composes to "commit failed, refresh and try again: requirement 
no longer holds after refresh: transaction cannot be committed again", where 
the first and last clauses contradict and a log reader is told to retry a 
terminal error. I'd keep both sentinels for `errors.Is` but control the string: 
wrap the cause and the two sentinels with `%w` without `ErrCommitFailed`'s text 
leading, so "cannot be committed again" isn't buried behind "try again".
   
   The defer above also re-wraps `ErrTransactionUnusable` when 
`orphanedManifests` is non-empty on this same path, so we can get the sentinel 
and the "build a new transaction" phrase twice. Skipping the re-wrap when 
`errors.Is(retErr, ErrTransactionUnusable)` already holds cleans that up.



##########
table/transaction.go:
##########
@@ -3361,13 +3380,17 @@ func (t *Transaction) Commit(ctx context.Context) 
(*Table, error) {
                        withCommitPinnedRefs(t.pinnedRefs),
                )
                if err != nil {
-                       // A clean conflict (ErrCommitFailed) committed nothing 
and stays
-                       // retriable. Any other failure leaves the commit state 
unknown
-                       // (the catalog may have accepted it), so mark it 
terminal to
-                       // avoid a double-apply on retry.
+                       // A clean conflict (ErrCommitFailed) stays retriable 
unless the error
+                       // also matches ErrTransactionUnusable: cleanup removed 
files the staged
+                       // updates reference, or a requirement failed after a 
refresh. Any other
+                       // failure leaves the commit state unknown, so mark it 
terminal to avoid
+                       // a double-apply.
                        if !errors.Is(err, ErrCommitFailed) {
                                t.committed = true
                        }
+                       if errors.Is(err, ErrTransactionUnusable) {

Review Comment:
   Still the conflation thread: this only flips `unusable` when the error 
already carries `ErrTransactionUnusable`, and two paths exit without it. With 
`num-retries=0` there's no refresh, so `validateNonRebasedRequirements` never 
runs; and on the final attempt the catalog's `CommitTable` rejects the 
non-rebased requirement with a plain `ErrCommitFailed`. Both exit the loop with 
`ErrCommitFailed` and no `ErrTransactionUnusable`, so `t.unusable` stays false 
and the transaction is still re-committable. Re-committing runs the same stale 
requirement against the same state and fails identically, which is exactly the 
external `errors.Is(err, ErrCommitFailed)` loop spinning on a hopeless commit. 
This PR newly routes SQL/Glue/Hive requirement conflicts through 
`ErrCommitFailed`, so that spin is now reachable on the local catalogs, not 
just REST.
   
   I'd check the failed requirement against freshly loaded state on the last 
attempt too, or mark the transaction unusable when a non-rebasable requirement 
is what failed, so a re-commit short-circuits instead of replaying. 
`TestStaleAppendRetriesRequirementFailure` already asserts the 
plain-`ErrCommitFailed` outcome for `num-retries=0`, so it'd be worth extending 
it to assert the second `Commit` doesn't just repeat.



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