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 30131ba675 [flink] Fix asynchronous partition refresh generation race
(#9318)
30131ba675 is described below
commit 30131ba675d999903a61d2a51bf235422aa4cfe7
Author: QuakeWang <[email protected]>
AuthorDate: Fri Aug 21 18:04:27 2026 +0800
[flink] Fix asynchronous partition refresh generation race (#9318)
---
.../flink/lookup/FileStoreLookupFunction.java | 3 +-
.../paimon/flink/lookup/PartitionRefresher.java | 79 ++++++++----
.../flink/lookup/PartitionRefresherTest.java | 133 +++++++++++++++++++++
3 files changed, 189 insertions(+), 26 deletions(-)
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lookup/FileStoreLookupFunction.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lookup/FileStoreLookupFunction.java
index 1545a092c4..aa647f167c 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lookup/FileStoreLookupFunction.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lookup/FileStoreLookupFunction.java
@@ -399,8 +399,7 @@ public class FileStoreLookupFunction implements
Serializable, Closeable {
// 2. check if async partition refresh has completed, and switch if so
if (partitionRefresher != null &&
partitionRefresher.isPartitionRefreshAsync()) {
- LookupTable newLookupTable =
-
partitionRefresher.getNewLookupTable(partitionLoader.partitions());
+ LookupTable newLookupTable =
partitionRefresher.getNewLookupTable();
if (newLookupTable != null) {
lookupTable.close();
lookupTable = newLookupTable;
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lookup/PartitionRefresher.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lookup/PartitionRefresher.java
index a9caa0f6fb..b10ad23780 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lookup/PartitionRefresher.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/lookup/PartitionRefresher.java
@@ -18,6 +18,7 @@
package org.apache.paimon.flink.lookup;
+import org.apache.paimon.annotation.VisibleForTesting;
import org.apache.paimon.data.BinaryRow;
import org.apache.paimon.data.InternalRow;
import org.apache.paimon.options.Options;
@@ -54,7 +55,7 @@ public class PartitionRefresher implements Closeable {
private final String tmpDirectory;
private volatile File path;
private ExecutorService partitionRefreshExecutor;
- private AtomicReference<LookupTable> pendingLookupTable;
+ private AtomicReference<RefreshResult> pendingRefresh;
private AtomicReference<Exception> partitionRefreshException;
/** Current partitions being used for lookup. Updated when partition
refresh completes. */
@@ -72,7 +73,7 @@ public class PartitionRefresher implements Closeable {
if (!partitionRefreshAsync) {
return;
}
- this.pendingLookupTable = new AtomicReference<>(null);
+ this.pendingRefresh = new AtomicReference<>(null);
this.partitionRefreshException = new AtomicReference<>(null);
this.partitionRefreshExecutor =
Executors.newSingleThreadExecutor(
@@ -139,14 +140,15 @@ public class PartitionRefresher implements Closeable {
partitionRefreshExecutor.submit(
() -> {
+ File newPath = new File(tmpDirectory, "lookup-" +
UUID.randomUUID());
+ FullCacheLookupTable newTable;
try {
- this.path = new File(tmpDirectory, "lookup-" +
UUID.randomUUID());
- if (!path.mkdirs()) {
- throw new RuntimeException("Failed to create dir:
" + path);
+ if (!newPath.mkdirs()) {
+ throw new RuntimeException("Failed to create dir:
" + newPath);
}
- FullCacheLookupTable.Context newContext =
context.copy(path);
+ FullCacheLookupTable.Context newContext =
context.copy(newPath);
Options options =
Options.fromMap(context.table.options());
- FullCacheLookupTable newTable =
+ newTable =
FullCacheLookupTable.create(
newContext,
options.get(LOOKUP_CACHE_ROWS));
if (cacheRowFilter != null) {
@@ -154,28 +156,43 @@ public class PartitionRefresher implements Closeable {
}
newTable.specifyPartitions(newPartitions,
partitionFilter);
newTable.open();
-
- pendingLookupTable.set(newTable);
- LOG.info("Async partition refresh completed for table
{}.", tableName);
} catch (Exception e) {
LOG.error("Async partition refresh failed for table
{}.", tableName, e);
partitionRefreshException.set(e);
- if (path != null) {
- FileIOUtils.deleteDirectoryQuietly(path);
- }
+ FileIOUtils.deleteDirectoryQuietly(newPath);
+ return;
+ }
+
+ try {
+ publishRefreshResult(newTable, newPartitions, newPath);
+ LOG.info("Async partition refresh completed for table
{}.", tableName);
+ } catch (IOException e) {
+ LOG.error(
+ "Failed to close superseded async partition
refresh for table {}.",
+ tableName,
+ e);
+ partitionRefreshException.set(e);
}
});
}
+ @VisibleForTesting
+ void publishRefreshResult(LookupTable lookupTable, List<BinaryRow>
partitions, File refreshPath)
+ throws IOException {
+ RefreshResult previous =
+ pendingRefresh.getAndSet(new RefreshResult(lookupTable,
partitions, refreshPath));
+ if (previous != null) {
+ previous.lookupTable.close();
+ }
+ }
+
/**
* Check if an async partition refresh has completed.
*
- * @param newPartitions the new partitions to update after refresh
completes
- * @return a Pair containing the new lookup table and its temp path if
ready, or null if no
- * switch is needed
+ * @return the new lookup table if ready, or null if no switch is needed
*/
@Nullable
- public LookupTable getNewLookupTable(List<BinaryRow> newPartitions) throws
Exception {
+ public LookupTable getNewLookupTable() throws Exception {
if (!partitionRefreshAsync) {
return null;
}
@@ -189,14 +206,15 @@ public class PartitionRefresher implements Closeable {
throw asyncException;
}
- LookupTable newTable = pendingLookupTable.getAndSet(null);
- if (newTable == null) {
+ RefreshResult refreshResult = pendingRefresh.getAndSet(null);
+ if (refreshResult == null) {
return null;
}
- this.currentPartitions = newPartitions;
+ this.currentPartitions = refreshResult.partitions;
+ this.path = refreshResult.path;
LOG.info("Switched to new lookup table for table {} with new
partitions.", tableName);
- return newTable;
+ return refreshResult.lookupTable;
}
/** Close partition refresh resources. */
@@ -205,10 +223,10 @@ public class PartitionRefresher implements Closeable {
if (partitionRefreshExecutor != null) {
ExecutorUtils.gracefulShutdown(1L, TimeUnit.MINUTES,
partitionRefreshExecutor);
}
- if (pendingLookupTable != null) {
- LookupTable pending = pendingLookupTable.getAndSet(null);
+ if (pendingRefresh != null) {
+ RefreshResult pending = pendingRefresh.getAndSet(null);
if (pending != null) {
- pending.close();
+ pending.lookupTable.close();
}
}
}
@@ -220,4 +238,17 @@ public class PartitionRefresher implements Closeable {
public File path() {
return path;
}
+
+ private static final class RefreshResult {
+
+ private final LookupTable lookupTable;
+ private final List<BinaryRow> partitions;
+ private final File path;
+
+ private RefreshResult(LookupTable lookupTable, List<BinaryRow>
partitions, File path) {
+ this.lookupTable = lookupTable;
+ this.partitions = partitions;
+ this.path = path;
+ }
+ }
}
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/lookup/PartitionRefresherTest.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/lookup/PartitionRefresherTest.java
new file mode 100644
index 0000000000..21b3e7dbde
--- /dev/null
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/lookup/PartitionRefresherTest.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.flink.lookup;
+
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.data.BinaryRowWriter;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.predicate.Predicate;
+import org.apache.paimon.utils.Filter;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for {@link PartitionRefresher}. */
+class PartitionRefresherTest {
+
+ @TempDir private Path tempDir;
+
+ @Test
+ void testRefreshResultKeepsGenerationConsistent() throws Exception {
+ List<BinaryRow> initialPartitions = partitions(1);
+ List<BinaryRow> refreshedPartitions = partitions(2);
+ File refreshPath = tempDir.resolve("refresh-2").toFile();
+ TestingLookupTable refreshedTable = new TestingLookupTable();
+ PartitionRefresher refresher = createRefresher(initialPartitions);
+
+ try {
+ refresher.publishRefreshResult(refreshedTable,
refreshedPartitions, refreshPath);
+
+ assertThat(refresher.getNewLookupTable()).isSameAs(refreshedTable);
+
assertThat(refresher.currentPartitions()).isSameAs(refreshedPartitions);
+ assertThat(refresher.path()).isEqualTo(refreshPath);
+ assertThat(refreshedTable.closed).isFalse();
+ } finally {
+ refresher.close();
+ refreshedTable.close();
+ }
+ }
+
+ @Test
+ void testPublishingNewResultClosesUnconsumedTable() throws Exception {
+ List<BinaryRow> partitionsB = partitions(2);
+ List<BinaryRow> partitionsC = partitions(3);
+ File pathB = tempDir.resolve("refresh-2").toFile();
+ File pathC = tempDir.resolve("refresh-3").toFile();
+ TestingLookupTable tableB = new TestingLookupTable();
+ TestingLookupTable tableC = new TestingLookupTable();
+ PartitionRefresher refresher = createRefresher(partitions(1));
+
+ try {
+ refresher.publishRefreshResult(tableB, partitionsB, pathB);
+ refresher.publishRefreshResult(tableC, partitionsC, pathC);
+
+ assertThat(tableB.closed).isTrue();
+ assertThat(tableC.closed).isFalse();
+ assertThat(refresher.getNewLookupTable()).isSameAs(tableC);
+ assertThat(refresher.currentPartitions()).isSameAs(partitionsC);
+ assertThat(refresher.path()).isEqualTo(pathC);
+ } finally {
+ refresher.close();
+ tableB.close();
+ tableC.close();
+ }
+ }
+
+ private PartitionRefresher createRefresher(List<BinaryRow>
initialPartitions) {
+ return new PartitionRefresher(true, "table", tempDir.toString(),
initialPartitions);
+ }
+
+ private static List<BinaryRow> partitions(int value) {
+ BinaryRow row = new BinaryRow(1);
+ BinaryRowWriter writer = new BinaryRowWriter(row);
+ writer.writeInt(0, value);
+ writer.complete();
+ return Collections.singletonList(row);
+ }
+
+ private static class TestingLookupTable implements LookupTable {
+
+ private boolean closed;
+
+ @Override
+ public void specifyPartitions(List<BinaryRow> scanPartitions,
Predicate partitionFilter) {}
+
+ @Override
+ public void open() {}
+
+ @Override
+ public List<InternalRow> get(InternalRow key) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public void refresh() {}
+
+ @Override
+ public void specifyCacheRowFilter(Filter<InternalRow> filter) {}
+
+ @Override
+ public Long nextSnapshotId() {
+ return null;
+ }
+
+ @Override
+ public void close() throws IOException {
+ closed = true;
+ }
+ }
+}