This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 702900d458 [core] Improve row ID reassignment under concurrent writes 
(#9439)
702900d458 is described below

commit 702900d458947172887194bc866eeb06ebb1f033
Author: Jingsong Lee <[email protected]>
AuthorDate: Fri Aug 28 11:30:18 2026 +0800

    [core] Improve row ID reassignment under concurrent writes (#9439)
---
 .../DataEvolutionRowIdReassigner.java              | 144 ++++++++++++++-------
 .../paimon/operation/FileStoreCommitImpl.java      |   6 +
 .../DataEvolutionRowIdReassignerTest.java          |  71 ++++++++++
 3 files changed, 175 insertions(+), 46 deletions(-)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/DataEvolutionRowIdReassigner.java
 
b/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/DataEvolutionRowIdReassigner.java
index 361b9a812a..7c49757f07 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/DataEvolutionRowIdReassigner.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/append/dataevolution/DataEvolutionRowIdReassigner.java
@@ -43,6 +43,7 @@ import org.apache.paimon.table.SpecialFields;
 import org.apache.paimon.utils.CloseableIterator;
 import org.apache.paimon.utils.Pair;
 import org.apache.paimon.utils.Range;
+import org.apache.paimon.utils.RetryWaiter;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -68,7 +69,6 @@ public class DataEvolutionRowIdReassigner {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(DataEvolutionRowIdReassigner.class);
     private static final String COMMIT_USER_PREFIX = "reassign-row-id";
-    private static final int MAX_COMMIT_ATTEMPTS = 3;
 
     private final FileStoreTable table;
     private final @Nullable PartitionPredicate partitionPredicate;
@@ -135,52 +135,29 @@ public class DataEvolutionRowIdReassigner {
             return Result.skipped(
                     latest.id(), nextRowId, "no partition requires row-id 
reassignment");
         }
-        AssignmentPlan assignmentPlan = optionalPlan.get();
 
-        for (int attempt = 1; attempt <= MAX_COMMIT_ATTEMPTS; attempt++) {
-            Assignment assignment = assignmentPlan.createAssignment(latest);
-            CommitAssignmentResult commitResult =
-                    commitAssignment(assignment, manifestFile, manifestList, 
commitUser);
-            if (commitResult.success) {
-                LOG.info(
-                        "Reassigned row IDs for table {} from {} to {}, 
partitions={}, files={}, rows={}.",
-                        table.name(),
-                        assignment.firstAssignedRowId,
-                        assignment.nextRowId,
-                        assignment.rowIdMappings.size(),
-                        commitResult.fileCount,
-                        assignment.logicalRowCount());
-                return new Result(
-                        assignment.snapshot.id(),
-                        assignment.snapshot.id() + 1,
-                        commitResult.fileCount,
-                        assignment.logicalRowCount(),
-                        commitResult.indexFileCount,
-                        assignment.firstAssignedRowId,
-                        assignment.nextRowId);
-            }
-
-            if (attempt == MAX_COMMIT_ATTEMPTS) {
-                throw new RuntimeException(
-                        "Failed to reassign row IDs because a newer snapshot 
has been committed.");
-            }
-
-            Snapshot newLatest = table.snapshotManager().latestSnapshot();
-            checkState(newLatest != null, "Latest snapshot disappeared while 
reassigning row IDs.");
-            assignmentPlan =
-                    advanceAssignmentPlan(
-                            assignmentPlan, latest, newLatest, manifestFile, 
manifestList);
-            LOG.info(
-                    "Failed to commit row-id reassignment for table {} based 
on snapshot {} because snapshot {} has been committed. Retrying {}/{} with the 
updated assignment plan.",
-                    table.name(),
-                    latest.id(),
-                    newLatest.id(),
-                    attempt + 1,
-                    MAX_COMMIT_ATTEMPTS);
-            latest = newLatest;
-        }
-
-        throw new IllegalStateException("Unreachable retry state while 
reassigning row IDs.");
+        CommittedAssignment committed =
+                commitAssignmentWithRetry(
+                        optionalPlan.get(), latest, manifestFile, 
manifestList, commitUser);
+        Assignment assignment = committed.assignment;
+        CommitAssignmentResult commitResult = committed.commitResult;
+        LOG.info(
+                "Reassigned row IDs for table {} from {} to {}, partitions={}, 
files={}, rows={}.",
+                table.name(),
+                assignment.firstAssignedRowId,
+                assignment.nextRowId,
+                assignment.rowIdMappings.size(),
+                commitResult.fileCount,
+                assignment.logicalRowCount());
+
+        return new Result(
+                assignment.snapshot.id(),
+                assignment.snapshot.id() + 1,
+                commitResult.fileCount,
+                assignment.logicalRowCount(),
+                commitResult.indexFileCount,
+                assignment.firstAssignedRowId,
+                assignment.nextRowId);
     }
 
     private Optional<AssignmentPlan> planAssignment(List<ManifestFileMeta> 
manifestMetas) {
@@ -358,6 +335,65 @@ public class DataEvolutionRowIdReassigner {
         return partitionPredicate != null;
     }
 
+    private CommittedAssignment commitAssignmentWithRetry(
+            AssignmentPlan initialAssignmentPlan,
+            Snapshot initialSnapshot,
+            ManifestFile manifestFile,
+            ManifestList manifestList,
+            String commitUser) {
+        AssignmentPlan assignmentPlan = initialAssignmentPlan;
+        Snapshot latest = initialSnapshot;
+        int retryCount = 0;
+        long startMillis = System.currentTimeMillis();
+        CoreOptions options = table.coreOptions();
+        RetryWaiter retryWaiter =
+                new RetryWaiter(options.commitMinRetryWait(), 
options.commitMaxRetryWait());
+
+        while (true) {
+            Snapshot observedLatest = table.snapshotManager().latestSnapshot();
+            checkState(
+                    observedLatest != null,
+                    "Latest snapshot disappeared while reassigning row IDs.");
+            if (observedLatest.id() > latest.id()) {
+                assignmentPlan =
+                        advanceAssignmentPlan(
+                                assignmentPlan, latest, observedLatest, 
manifestFile, manifestList);
+                latest = observedLatest;
+            }
+
+            Assignment assignment = assignmentPlan.createAssignment(latest);
+            CommitAssignmentResult commitResult =
+                    commitAssignment(assignment, manifestFile, manifestList, 
commitUser);
+            if (commitResult.success) {
+                return new CommittedAssignment(assignment, commitResult);
+            }
+
+            if (System.currentTimeMillis() - startMillis > 
options.commitTimeout()
+                    || retryCount >= options.commitMaxRetries()) {
+                throw new RuntimeException(
+                        String.format(
+                                "Failed to reassign row IDs after %s millis 
with %s retries because newer snapshots kept being committed.",
+                                System.currentTimeMillis() - startMillis, 
retryCount));
+            }
+
+            Snapshot newLatest = table.snapshotManager().latestSnapshot();
+            checkState(newLatest != null, "Latest snapshot disappeared while 
reassigning row IDs.");
+            assignmentPlan =
+                    advanceAssignmentPlan(
+                            assignmentPlan, latest, newLatest, manifestFile, 
manifestList);
+            LOG.info(
+                    "Failed to commit row-id reassignment for table {} based 
on snapshot {} because snapshot {} has been committed. Retrying {}/{} with the 
updated assignment plan.",
+                    table.name(),
+                    latest.id(),
+                    newLatest.id(),
+                    retryCount + 1,
+                    options.commitMaxRetries());
+            retryWaiter.retryWait(retryCount);
+            retryCount++;
+            latest = newLatest;
+        }
+    }
+
     private CommitAssignmentResult commitAssignment(
             Assignment assignment,
             ManifestFile manifestFile,
@@ -556,15 +592,21 @@ public class DataEvolutionRowIdReassigner {
             Map<String, List<ManifestFileMeta>> rewrittenManifestMetas,
             ManifestList manifestList) {
         List<ManifestFileMeta> baseManifestMetas = new ArrayList<>();
+        Set<String> unmatchedReplacements = new 
HashSet<>(rewrittenManifestMetas.keySet());
         for (ManifestFileMeta manifestMeta : manifestMetas) {
             List<ManifestFileMeta> replacement =
                     rewrittenManifestMetas.get(manifestMeta.fileName());
             if (replacement == null) {
                 baseManifestMetas.add(manifestMeta);
             } else {
+                unmatchedReplacements.remove(manifestMeta.fileName());
                 baseManifestMetas.addAll(replacement);
             }
         }
+        checkState(
+                unmatchedReplacements.isEmpty(),
+                "Cannot replace planned manifests %s because they are not in 
the current manifest list.",
+                unmatchedReplacements);
         return manifestList.write(baseManifestMetas);
     }
 
@@ -860,6 +902,16 @@ public class DataEvolutionRowIdReassigner {
         }
     }
 
+    private static class CommittedAssignment {
+        private final Assignment assignment;
+        private final CommitAssignmentResult commitResult;
+
+        private CommittedAssignment(Assignment assignment, 
CommitAssignmentResult commitResult) {
+            this.assignment = assignment;
+            this.commitResult = commitResult;
+        }
+    }
+
     private static class CommitAssignmentResult {
         private final boolean success;
         private final long fileCount;
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommitImpl.java
 
b/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommitImpl.java
index cd4919b9ce..2809101551 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommitImpl.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/operation/FileStoreCommitImpl.java
@@ -1372,6 +1372,12 @@ public class FileStoreCommitImpl implements 
FileStoreCommit {
                 latest.nextRowId());
     }
 
+    /**
+     * Replaces the manifest lists exactly as supplied, without manifest 
compaction or sorting.
+     *
+     * <p>This is intended for metadata-only operations which have already 
produced their final
+     * manifest layout and must commit it without invoking {@link 
ManifestFileMerger}.
+     */
     public boolean replaceManifestList(
             Snapshot latest,
             long totalRecordCount,
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/append/dataevolution/DataEvolutionRowIdReassignerTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/append/dataevolution/DataEvolutionRowIdReassignerTest.java
index 08b35b7bdf..af0178bb27 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/append/dataevolution/DataEvolutionRowIdReassignerTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/append/dataevolution/DataEvolutionRowIdReassignerTest.java
@@ -976,6 +976,40 @@ public class DataEvolutionRowIdReassignerTest extends 
TableTestBase {
                 .containsEntry("pt=d/", Collections.singletonList(6L));
     }
 
+    @Test
+    public void testReassignUsesConfiguredRetryBudget() throws Exception {
+        FileStoreTable table = createTableWithInterleavedPartitions();
+        Map<String, String> retryOptions = new HashMap<>();
+        retryOptions.put(CoreOptions.COMMIT_MAX_RETRIES.key(), "3");
+        retryOptions.put(CoreOptions.COMMIT_MIN_RETRY_WAIT.key(), "0ms");
+        retryOptions.put(CoreOptions.COMMIT_MAX_RETRY_WAIT.key(), "0ms");
+        FileStoreTable configured = table.copy(retryOptions);
+
+        AtomicInteger beforeCommits = new AtomicInteger();
+        DataEvolutionRowIdReassigner.Result result =
+                new DataEvolutionRowIdReassigner(
+                                configured,
+                                partitionPredicate(configured, "a"),
+                                () -> {
+                                    int attempt = 
beforeCommits.getAndIncrement();
+                                    if (attempt < 3) {
+                                        try {
+                                            writeOneRow(
+                                                    configured, "new-" + 
attempt, 100 + attempt);
+                                        } catch (Exception e) {
+                                            throw new RuntimeException(e);
+                                        }
+                                    }
+                                })
+                        .reassign("test-reassign-configured-retries");
+
+        assertThat(beforeCommits).hasValue(4);
+        assertThat(result.reassigned).isTrue();
+        assertThat(result.fileCount).isEqualTo(3L);
+        assertThat(result.rowCount).isEqualTo(3L);
+        
assertThat(rowIdsByPartition(configured).get("pt=a/")).containsExactly(8L, 9L, 
10L);
+    }
+
     @Test
     public void 
testReassignPartitionFilterAfterConcurrentAppendOutsideFilter() throws 
Exception {
         FileStoreTable table = createTableWithInterleavedPartitions();
@@ -1287,6 +1321,43 @@ public class DataEvolutionRowIdReassignerTest extends 
TableTestBase {
         
assertThat(afterManifestFiles).doesNotContainAnyElementsOf(affectedManifests);
     }
 
+    @Test
+    public void testReassignDoesNotCompactManifests() throws Exception {
+        testReassignSkipsManifestOptimization(false);
+    }
+
+    @Test
+    public void testReassignDoesNotSortManifests() throws Exception {
+        testReassignSkipsManifestOptimization(true);
+    }
+
+    private void testReassignSkipsManifestOptimization(boolean 
manifestSortEnabled)
+            throws Exception {
+        FileStoreTable table = createTableWithPartiallyOverlappedPartitions();
+        Map<String, Set<String>> partitionsByManifest = 
currentPartitionsByManifest(table);
+        List<String> unaffectedManifests = new ArrayList<>();
+        for (Map.Entry<String, Set<String>> entry : 
partitionsByManifest.entrySet()) {
+            if (!entry.getValue().contains("pt=a/")) {
+                unaffectedManifests.add(entry.getKey());
+            }
+        }
+        assertThat(unaffectedManifests).isNotEmpty();
+
+        Map<String, String> options = new HashMap<>();
+        options.put(CoreOptions.MANIFEST_SORT_ENABLED.key(), 
Boolean.toString(manifestSortEnabled));
+        options.put(CoreOptions.MANIFEST_MERGE_MIN_COUNT.key(), "1");
+        options.put(CoreOptions.MANIFEST_FULL_COMPACTION_FILE_SIZE.key(), 
"1B");
+        FileStoreTable configured = table.copy(options);
+
+        new DataEvolutionRowIdReassigner(configured)
+                .reassign(
+                        manifestSortEnabled
+                                ? "test-reassign-with-manifest-sort"
+                                : "test-reassign-with-manifest-compaction");
+
+        
assertThat(dataManifestFileNames(configured)).containsAll(unaffectedManifests);
+    }
+
     @Test
     public void testSkipWhenPartitionRowIdsAreContiguous() throws Exception {
         createTableDefault();

Reply via email to