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

lzljs3620320 pushed a commit to branch release-0.2
in repository https://gitbox.apache.org/repos/asf/flink-table-store.git


The following commit(s) were added to refs/heads/release-0.2 by this push:
     new 9c16283e [FLINK-29367] FileStoreCommitImpl also checks for conflicts 
when committing append changes
9c16283e is described below

commit 9c16283e04cffccfaa42ed854381990448c787d1
Author: tsreaper <[email protected]>
AuthorDate: Thu Sep 22 18:12:13 2022 +0800

    [FLINK-29367] FileStoreCommitImpl also checks for conflicts when committing 
append changes
    
    This closes #301
---
 .../store/file/operation/FileStoreCommitImpl.java  | 143 +++++++++++++++++----
 .../flink/table/store/file/TestFileStore.java      |  11 +-
 .../store/file/operation/FileStoreCommitTest.java  |  50 ++++++-
 3 files changed, 174 insertions(+), 30 deletions(-)

diff --git 
a/flink-table-store-core/src/main/java/org/apache/flink/table/store/file/operation/FileStoreCommitImpl.java
 
b/flink-table-store-core/src/main/java/org/apache/flink/table/store/file/operation/FileStoreCommitImpl.java
index 319d96cf..71003c14 100644
--- 
a/flink-table-store-core/src/main/java/org/apache/flink/table/store/file/operation/FileStoreCommitImpl.java
+++ 
b/flink-table-store-core/src/main/java/org/apache/flink/table/store/file/operation/FileStoreCommitImpl.java
@@ -45,6 +45,7 @@ import org.slf4j.LoggerFactory;
 import javax.annotation.Nullable;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Comparator;
 import java.util.HashMap;
@@ -66,12 +67,16 @@ import java.util.stream.Collectors;
  *   <li>Before calling {@link FileStoreCommitImpl#commit}, if user cannot 
determine if this commit
  *       is done before, user should first call {@link 
FileStoreCommitImpl#filterCommitted}.
  *   <li>Before committing, it will first check for conflicts by checking if 
all files to be removed
- *       currently exists.
+ *       currently exists, and if modified files have overlapping key ranges 
with existing files.
  *   <li>After that it use the external {@link FileStoreCommitImpl#lock} (if 
provided) or the atomic
  *       rename of the file system to ensure atomicity.
  *   <li>If commit fails due to conflicts or exception it tries its best to 
clean up and aborts.
  *   <li>If atomic rename fails it tries again after reading the latest 
snapshot from step 2.
  * </ol>
+ *
+ * <p>NOTE: If you want to modify this class, any exception during commit MUST 
NOT BE IGNORED. They
+ * must be thrown to restart the job. It is recommended to run 
FileStoreCommitTest thousands of
+ * times to make sure that your changes is correct.
  */
 public class FileStoreCommitImpl implements FileStoreCommit {
 
@@ -182,26 +187,60 @@ public class FileStoreCommitImpl implements 
FileStoreCommit {
             LOG.debug("Ready to commit\n" + committable.toString());
         }
 
+        Long safeLatestSnapshotId = null;
+        List<ManifestEntry> baseEntries = new ArrayList<>();
+
         List<ManifestEntry> appendChanges = 
collectChanges(committable.newFiles(), FileKind.ADD);
+        List<ManifestEntry> compactChanges = new ArrayList<>();
+        compactChanges.addAll(collectChanges(committable.compactBefore(), 
FileKind.DELETE));
+        compactChanges.addAll(collectChanges(committable.compactAfter(), 
FileKind.ADD));
+
         if (createEmptyCommit || !appendChanges.isEmpty()) {
+            // Optimization for common path.
+            // Step 1:
+            // Read manifest entries from changed partitions here and check 
for conflicts.
+            // If there are no other jobs committing at the same time,
+            // we can skip conflict checking in tryCommit method.
+            // This optimization is mainly used to decrease the number of 
times we read from files.
+            Long latestSnapshotId = snapshotManager.latestSnapshotId();
+            if (latestSnapshotId != null) {
+                // it is possible that some partitions only have compact 
changes,
+                // so we need to contain all changes
+                baseEntries.addAll(
+                        readAllEntriesFromChangedPartitions(
+                                latestSnapshotId, appendChanges, 
compactChanges));
+                noConflictsOrFail(baseEntries, appendChanges);
+                safeLatestSnapshotId = latestSnapshotId;
+            }
+
             tryCommit(
                     appendChanges,
                     committable.identifier(),
                     committable.logOffsets(),
                     Snapshot.CommitKind.APPEND,
-                    false);
+                    safeLatestSnapshotId);
         }
 
-        List<ManifestEntry> compactChanges = new ArrayList<>();
-        compactChanges.addAll(collectChanges(committable.compactBefore(), 
FileKind.DELETE));
-        compactChanges.addAll(collectChanges(committable.compactAfter(), 
FileKind.ADD));
         if (!compactChanges.isEmpty()) {
+            // Optimization for common path.
+            // Step 2:
+            // Add appendChanges to the manifest entries read above and check 
for conflicts.
+            // If there are no other jobs committing at the same time,
+            // we can skip conflict checking in tryCommit method.
+            // This optimization is mainly used to decrease the number of 
times we read from files.
+            if (safeLatestSnapshotId != null) {
+                baseEntries.addAll(appendChanges);
+                noConflictsOrFail(baseEntries, compactChanges);
+                // assume this compact commit follows just after the append 
commit created above
+                safeLatestSnapshotId += 1;
+            }
+
             tryCommit(
                     compactChanges,
                     committable.identifier(),
                     committable.logOffsets(),
                     Snapshot.CommitKind.COMPACT,
-                    true);
+                    safeLatestSnapshotId);
         }
     }
 
@@ -246,7 +285,7 @@ public class FileStoreCommitImpl implements FileStoreCommit 
{
                     committable.identifier(),
                     committable.logOffsets(),
                     Snapshot.CommitKind.COMPACT,
-                    true);
+                    null);
         }
     }
 
@@ -255,11 +294,16 @@ public class FileStoreCommitImpl implements 
FileStoreCommit {
             String hash,
             Map<Integer, Long> logOffsets,
             Snapshot.CommitKind commitKind,
-            boolean checkFileConflicts) {
+            Long safeLatestSnapshotId) {
         while (true) {
             Long latestSnapshotId = snapshotManager.latestSnapshotId();
             if (tryCommitOnce(
-                    changes, hash, logOffsets, commitKind, latestSnapshotId, 
checkFileConflicts)) {
+                    changes,
+                    hash,
+                    logOffsets,
+                    commitKind,
+                    latestSnapshotId,
+                    safeLatestSnapshotId)) {
                 break;
             }
         }
@@ -298,7 +342,7 @@ public class FileStoreCommitImpl implements FileStoreCommit 
{
                     logOffsets,
                     Snapshot.CommitKind.OVERWRITE,
                     latestSnapshotId,
-                    false)) {
+                    null)) {
                 break;
             }
         }
@@ -333,7 +377,7 @@ public class FileStoreCommitImpl implements FileStoreCommit 
{
             Map<Integer, Long> logOffsets,
             Snapshot.CommitKind commitKind,
             Long latestSnapshotId,
-            boolean checkFileConflicts) {
+            Long safeLatestSnapshotId) {
         long newSnapshotId =
                 latestSnapshotId == null ? Snapshot.FIRST_SNAPSHOT_ID : 
latestSnapshotId + 1;
         Path newSnapshotPath = snapshotManager.snapshotPath(newSnapshotId);
@@ -347,7 +391,9 @@ public class FileStoreCommitImpl implements FileStoreCommit 
{
 
         Snapshot latestSnapshot = null;
         if (latestSnapshotId != null) {
-            if (checkFileConflicts) {
+            if (!latestSnapshotId.equals(safeLatestSnapshotId)) {
+                // latestSnapshotId is different from the snapshot id we've 
checked for conflicts,
+                // so we have to check again
                 noConflictsOrFail(latestSnapshotId, changes);
             }
             latestSnapshot = snapshotManager.snapshot(latestSnapshotId);
@@ -479,23 +525,31 @@ public class FileStoreCommitImpl implements 
FileStoreCommit {
         return false;
     }
 
-    private void noConflictsOrFail(long snapshotId, List<ManifestEntry> 
changes) {
+    @SafeVarargs
+    private final List<ManifestEntry> readAllEntriesFromChangedPartitions(
+            long snapshotId, List<ManifestEntry>... changes) {
         List<BinaryRowData> changedPartitions =
-                changes.stream()
+                Arrays.stream(changes)
+                        .flatMap(Collection::stream)
                         .map(ManifestEntry::partition)
                         .distinct()
                         .collect(Collectors.toList());
-        List<ManifestEntry> allEntries;
         try {
-            allEntries =
-                    new ArrayList<>(
-                            scan.withSnapshot(snapshotId)
-                                    .withPartitionFilter(changedPartitions)
-                                    .plan()
-                                    .files());
+            return scan.withSnapshot(snapshotId)
+                    .withPartitionFilter(changedPartitions)
+                    .plan()
+                    .files();
         } catch (Throwable e) {
-            throw new RuntimeException("Cannot determine if conflicts exist.", 
e);
+            throw new RuntimeException("Cannot read manifest entries from 
changed partitions.", e);
         }
+    }
+
+    private void noConflictsOrFail(long snapshotId, List<ManifestEntry> 
changes) {
+        noConflictsOrFail(readAllEntriesFromChangedPartitions(snapshotId, 
changes), changes);
+    }
+
+    private void noConflictsOrFail(List<ManifestEntry> baseEntries, 
List<ManifestEntry> changes) {
+        List<ManifestEntry> allEntries = new ArrayList<>(baseEntries);
         allEntries.addAll(changes);
 
         Collection<ManifestEntry> mergedEntries;
@@ -503,8 +557,9 @@ public class FileStoreCommitImpl implements FileStoreCommit 
{
             // merge manifest entries and also check if the files we want to 
delete are still there
             mergedEntries = ManifestEntry.mergeManifestEntries(allEntries);
         } catch (Throwable e) {
-            throw new RuntimeException(
-                    "File deletion conflicts detected! Give up committing 
compact changes.", e);
+            LOG.warn("File deletion conflicts detected! Give up committing.", 
e);
+            throw createConflictException(
+                    "File deletion conflicts detected! Give up committing.", 
baseEntries, changes);
         }
 
         // fast exit for file store without keys
@@ -531,16 +586,50 @@ public class FileStoreCommitImpl implements 
FileStoreCommit {
                 ManifestEntry a = entries.get(i);
                 ManifestEntry b = entries.get(i + 1);
                 if (keyComparator.compare(a.file().maxKey(), 
b.file().minKey()) >= 0) {
-                    throw new RuntimeException(
-                            "LSM conflicts detected! Give up committing 
compact changes. Conflict files are:\n"
+                    throw createConflictException(
+                            "LSM conflicts detected! Give up committing. 
Conflict files are:\n"
                                     + a.identifier().toString(pathFactory)
                                     + "\n"
-                                    + b.identifier().toString(pathFactory));
+                                    + b.identifier().toString(pathFactory),
+                            baseEntries,
+                            changes);
                 }
             }
         }
     }
 
+    private RuntimeException createConflictException(
+            String message, List<ManifestEntry> baseEntries, 
List<ManifestEntry> changes) {
+        String possibleCauses =
+                String.join(
+                        "\n",
+                        "Conflicts during commits are normal and this failure 
is intended to resolve the conflicts.",
+                        "Conflicts are mainly caused by the following 
scenarios:",
+                        "1. Multiple jobs are writing into the same partition 
at the same time.",
+                        "2. You're recovering from an old savepoint, or you're 
creating multiple jobs from a savepoint.",
+                        "   The job will fail continuously in this scenario to 
protect metadata from corruption.",
+                        "   You can either recover from the latest savepoint, "
+                                + "or you can revert the table to the snapshot 
corresponding to the old savepoint.");
+        String baseEntriesString =
+                "Base entries are:\n"
+                        + baseEntries.stream()
+                                .map(ManifestEntry::toString)
+                                .collect(Collectors.joining("\n"));
+        String changesString =
+                "Changes are:\n"
+                        + changes.stream()
+                                .map(ManifestEntry::toString)
+                                .collect(Collectors.joining("\n"));
+        return new RuntimeException(
+                message
+                        + "\n\n"
+                        + possibleCauses
+                        + "\n\n"
+                        + baseEntriesString
+                        + "\n\n"
+                        + changesString);
+    }
+
     private void cleanUpTmpManifests(
             String previousChangesListName,
             String newChangesListName,
diff --git 
a/flink-table-store-core/src/test/java/org/apache/flink/table/store/file/TestFileStore.java
 
b/flink-table-store-core/src/test/java/org/apache/flink/table/store/file/TestFileStore.java
index 1b347791..76f6f8bc 100644
--- 
a/flink-table-store-core/src/test/java/org/apache/flink/table/store/file/TestFileStore.java
+++ 
b/flink-table-store-core/src/test/java/org/apache/flink/table/store/file/TestFileStore.java
@@ -34,6 +34,7 @@ import org.apache.flink.table.store.file.memory.MemoryOwner;
 import org.apache.flink.table.store.file.mergetree.Increment;
 import org.apache.flink.table.store.file.mergetree.compact.MergeFunction;
 import org.apache.flink.table.store.file.operation.FileStoreCommit;
+import org.apache.flink.table.store.file.operation.FileStoreCommitImpl;
 import org.apache.flink.table.store.file.operation.FileStoreExpireImpl;
 import org.apache.flink.table.store.file.operation.FileStoreRead;
 import org.apache.flink.table.store.file.operation.FileStoreScan;
@@ -133,6 +134,10 @@ public class TestFileStore extends KeyValueFileStore {
         this.user = UUID.randomUUID().toString();
     }
 
+    public FileStoreCommitImpl newCommit() {
+        return super.newCommit(user);
+    }
+
     public FileStoreExpireImpl newExpire(
             int numRetainedMin, int numRetainedMax, long millisRetained) {
         return new FileStoreExpireImpl(
@@ -164,6 +169,7 @@ public class TestFileStore extends KeyValueFileStore {
                 partitionCalculator,
                 bucketCalculator,
                 false,
+                null,
                 (commit, committable) -> {
                     logOffsets.forEach(committable::addLogOffset);
                     commit.commit(committable, Collections.emptyMap());
@@ -181,6 +187,7 @@ public class TestFileStore extends KeyValueFileStore {
                 partitionCalculator,
                 bucketCalculator,
                 true,
+                null,
                 (commit, committable) ->
                         commit.overwrite(partition, committable, 
Collections.emptyMap()));
     }
@@ -190,6 +197,7 @@ public class TestFileStore extends KeyValueFileStore {
             Function<KeyValue, BinaryRowData> partitionCalculator,
             Function<KeyValue, Integer> bucketCalculator,
             boolean emptyWriter,
+            String identifier,
             BiConsumer<FileStoreCommit, ManifestCommittable> commitFunction)
             throws Exception {
         FileStoreWrite<KeyValue> write = newWrite();
@@ -224,7 +232,8 @@ public class TestFileStore extends KeyValueFileStore {
 
         FileStoreCommit commit = newCommit(user);
         ManifestCommittable committable =
-                new ManifestCommittable(String.valueOf(new 
Random().nextLong()));
+                new ManifestCommittable(
+                        identifier == null ? String.valueOf(new 
Random().nextLong()) : identifier);
         for (Map.Entry<BinaryRowData, Map<Integer, RecordWriter<KeyValue>>> 
entryWithPartition :
                 writers.entrySet()) {
             for (Map.Entry<Integer, RecordWriter<KeyValue>> entryWithBucket :
diff --git 
a/flink-table-store-core/src/test/java/org/apache/flink/table/store/file/operation/FileStoreCommitTest.java
 
b/flink-table-store-core/src/test/java/org/apache/flink/table/store/file/operation/FileStoreCommitTest.java
index 13accde3..8d29b364 100644
--- 
a/flink-table-store-core/src/test/java/org/apache/flink/table/store/file/operation/FileStoreCommitTest.java
+++ 
b/flink-table-store-core/src/test/java/org/apache/flink/table/store/file/operation/FileStoreCommitTest.java
@@ -26,6 +26,8 @@ import org.apache.flink.table.store.file.TestFileStore;
 import org.apache.flink.table.store.file.TestKeyValueGenerator;
 import org.apache.flink.table.store.file.manifest.ManifestCommittable;
 import 
org.apache.flink.table.store.file.mergetree.compact.DeduplicateMergeFunction;
+import org.apache.flink.table.store.file.schema.SchemaManager;
+import org.apache.flink.table.store.file.schema.UpdateSchema;
 import org.apache.flink.table.store.file.utils.FailingAtomicRenameFileSystem;
 import org.apache.flink.table.store.file.utils.FileUtils;
 import org.apache.flink.table.store.file.utils.SnapshotManager;
@@ -52,6 +54,7 @@ import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 /** Tests for {@link FileStoreCommitImpl}. */
 public class FileStoreCommitTest {
@@ -323,6 +326,7 @@ public class FileStoreCommitTest {
                 gen::getPartition,
                 kv -> 0,
                 false,
+                null,
                 (commit, committable) -> commit.commit(committable, 
Collections.emptyMap()));
         
assertThat(store.snapshotManager().findLatest()).isEqualTo(snapshot.id());
 
@@ -332,6 +336,7 @@ public class FileStoreCommitTest {
                 gen::getPartition,
                 kv -> 0,
                 false,
+                null,
                 (commit, committable) -> {
                     commit.withCreateEmptyCommit(true);
                     commit.commit(committable, Collections.emptyMap());
@@ -339,16 +344,57 @@ public class FileStoreCommitTest {
         
assertThat(store.snapshotManager().findLatest()).isEqualTo(snapshot.id() + 1);
     }
 
-    private TestFileStore createStore(boolean failing) {
+    @Test
+    public void testCommitOldSnapshotAgain() throws Exception {
+        TestFileStore store = createStore(false, 2);
+        List<ManifestCommittable> committables = new ArrayList<>();
+
+        // commit 3 snapshots
+        for (int i = 0; i < 3; i++) {
+            store.commitDataImpl(
+                    generateDataList(10),
+                    gen::getPartition,
+                    kv -> 0,
+                    false,
+                    String.valueOf(i),
+                    (commit, committable) -> {
+                        commit.commit(committable, Collections.emptyMap());
+                        committables.add(committable);
+                    });
+        }
+
+        // commit the first snapshot again, should throw exception due to 
conflicts
+        for (int i = 0; i < 3; i++) {
+            RuntimeException e =
+                    assertThrows(
+                            RuntimeException.class,
+                            () ->
+                                    store.newCommit()
+                                            .commit(committables.get(0), 
Collections.emptyMap()),
+                            "Expecting RuntimeException, but nothing is 
thrown.");
+            assertThat(e).hasMessageContaining("Give up committing.");
+        }
+    }
+
+    private TestFileStore createStore(boolean failing) throws Exception {
         return createStore(failing, 1);
     }
 
-    private TestFileStore createStore(boolean failing, int numBucket) {
+    private TestFileStore createStore(boolean failing, int numBucket) throws 
Exception {
         String root =
                 failing
                         ? FailingAtomicRenameFileSystem.getFailingPath(
                                 failingName, tempDir.toString())
                         : TestAtomicRenameFileSystem.SCHEME + "://" + 
tempDir.toString();
+        SchemaManager schemaManager = new SchemaManager(new 
Path(tempDir.toUri()));
+        schemaManager.commitNewVersion(
+                new UpdateSchema(
+                        TestKeyValueGenerator.DEFAULT_ROW_TYPE,
+                        
TestKeyValueGenerator.DEFAULT_PART_TYPE.getFieldNames(),
+                        TestKeyValueGenerator.getPrimaryKeys(
+                                
TestKeyValueGenerator.GeneratorMode.MULTI_PARTITIONED),
+                        Collections.emptyMap(),
+                        null));
         return TestFileStore.create(
                 "avro",
                 root,

Reply via email to