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

luoyuxia pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fluss.git


The following commit(s) were added to refs/heads/main by this push:
     new 03d24f121 [flink] Preserve log-only source restore mode (#3355)
03d24f121 is described below

commit 03d24f12165a9e5451c727e2de4ed9d434b637c5
Author: yuxia Luo <[email protected]>
AuthorDate: Thu May 28 11:42:02 2026 +0800

    [flink] Preserve log-only source restore mode (#3355)
---
 .../org/apache/fluss/flink/source/FlinkSource.java |  13 ++-
 .../source/enumerator/FlinkSourceEnumerator.java   |  40 +++++---
 .../FlussSourceEnumeratorStateSerializer.java      |   5 +-
 .../enumerator/FlinkSourceEnumeratorTest.java      | 101 ++++++++++++++++++++-
 .../state/SourceEnumeratorStateSerializerTest.java |  22 +++++
 5 files changed, 163 insertions(+), 18 deletions(-)

diff --git 
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/source/FlinkSource.java
 
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/source/FlinkSource.java
index 8535a881a..2ecfca09d 100644
--- 
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/source/FlinkSource.java
+++ 
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/source/FlinkSource.java
@@ -52,6 +52,9 @@ import org.apache.flink.core.io.SimpleVersionedSerializer;
 
 import javax.annotation.Nullable;
 
+import java.util.Collections;
+import java.util.List;
+
 import static org.apache.fluss.config.ConfigOptions.CLIENT_SCANNER_IO_TMP_DIR;
 import static 
org.apache.fluss.flink.utils.FlinkConnectorOptionsUtils.getClientScannerIoTmpDir;
 
@@ -236,6 +239,14 @@ public class FlinkSource<OUT>
     public SplitEnumerator<SourceSplitBase, SourceEnumeratorState> 
restoreEnumerator(
             SplitEnumeratorContext<SourceSplitBase> splitEnumeratorContext,
             SourceEnumeratorState sourceEnumeratorState) {
+        List<SourceSplitBase> remainingHybridLakeFlussSplits =
+                sourceEnumeratorState.getRemainingHybridLakeFlussSplits();
+        // A fresh null means lake splits are not initialized yet. When 
restoring, null means
+        // nothing is pending, so normalize it here to avoid generating lake 
splits later.
+        if (remainingHybridLakeFlussSplits == null) {
+            remainingHybridLakeFlussSplits = Collections.emptyList();
+        }
+
         return new FlinkSourceEnumerator(
                 tablePath,
                 flussConf,
@@ -244,7 +255,7 @@ public class FlinkSource<OUT>
                 splitEnumeratorContext,
                 sourceEnumeratorState.getAssignedBuckets(),
                 sourceEnumeratorState.getAssignedPartitions(),
-                sourceEnumeratorState.getRemainingHybridLakeFlussSplits(),
+                remainingHybridLakeFlussSplits,
                 offsetsInitializer,
                 scanPartitionDiscoveryIntervalMs,
                 splitPerAssignmentBatchSize,
diff --git 
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/source/enumerator/FlinkSourceEnumerator.java
 
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/source/enumerator/FlinkSourceEnumerator.java
index f58c4adfa..d107712ff 100644
--- 
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/source/enumerator/FlinkSourceEnumerator.java
+++ 
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/source/enumerator/FlinkSourceEnumerator.java
@@ -133,6 +133,20 @@ public class FlinkSourceEnumerator
     /** Buckets that have been assigned to readers. */
     private final Set<TableBucket> assignedTableBuckets;
 
+    /**
+     * Remaining lake snapshot and hybrid lake/Fluss splits to assign.
+     *
+     * <p>The field has three states:
+     *
+     * <ul>
+     *   <li>{@code null}: lake split initialization has not run yet, or the 
source has no lake
+     *       (non-lake table) so initialization will never run.
+     *   <li>empty list: lake split initialization has run, or this enumerator 
was started in
+     *       Fluss-only (non-lake) mode and must not initialize lake splits 
after restore.
+     *   <li>non-empty list: lake split initialization has run and these 
splits still need to be
+     *       assigned.
+     * </ul>
+     */
     @Nullable private List<SourceSplitBase> pendingHybridLakeFlussSplits;
 
     private final long scanPartitionDiscoveryIntervalMs;
@@ -513,18 +527,15 @@ public class FlinkSourceEnumerator
 
     private void startInStreamModeForNonPartitionedTable() {
         if (lakeSource != null) {
-            context.callAsync(
-                    () -> {
-                        // firstly, try to generate hybrid lake splits,
-                        List<SourceSplitBase> splits = 
generateHybridLakeFlussSplits();
-                        // splits is null,
-                        // we'll fall back to normal fluss splits generation 
logic
-                        if (splits == null) {
-                            splits = this.initNonPartitionedSplits();
-                        }
-                        return splits;
-                    },
-                    this::handleSplitsAdd);
+            // Generate lake splits synchronously so that they are available 
before the
+            // first checkpoint. This is consistent with the partitioned-table 
path in
+            // start().
+            List<SourceSplitBase> splits = generateHybridLakeFlussSplits();
+            if (splits == null) {
+                // no lake snapshot, fall back to normal Fluss splits
+                splits = this.initNonPartitionedSplits();
+            }
+            handleSplitsAdd(splits, null);
         } else {
             // init bucket splits and assign
             context.callAsync(this::initNonPartitionedSplits, 
this::handleSplitsAdd);
@@ -872,9 +883,8 @@ public class FlinkSourceEnumerator
     /** Return the hybrid lake and fluss splits. Return null if no lake 
snapshot. */
     @Nullable
     private List<SourceSplitBase> generateHybridLakeFlussSplits() {
-        // still have pending lake fluss splits,
-        // should be restored from checkpoint, shouldn't
-        // list splits again
+        // Restored from checkpoint with pending lake splits — return them 
directly
+        // without re-generating.
         if (pendingHybridLakeFlussSplits != null) {
             LOG.info("Still have pending lake fluss splits, shouldn't list 
splits again.");
             return pendingHybridLakeFlussSplits;
diff --git 
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/source/state/FlussSourceEnumeratorStateSerializer.java
 
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/source/state/FlussSourceEnumeratorStateSerializer.java
index 121041df1..996313e60 100644
--- 
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/source/state/FlussSourceEnumeratorStateSerializer.java
+++ 
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/source/state/FlussSourceEnumeratorStateSerializer.java
@@ -270,12 +270,15 @@ public class FlussSourceEnumeratorStateSerializer
         if (in.readBoolean()) {
             int numSplits = in.readInt();
             List<SourceSplitBase> splits = new ArrayList<>(numSplits);
+            int version = in.readInt();
+            if (numSplits == 0) {
+                return splits;
+            }
             SourceSplitSerializer sourceSplitSerializer =
                     new SourceSplitSerializer(
                             checkNotNull(
                                     lakeSource,
                                     "lake source must not be null when there 
are hybrid lake splits."));
-            int version = in.readInt();
             for (int i = 0; i < numSplits; i++) {
                 int splitSizeInBytes = in.readInt();
                 byte[] splitBytes = new byte[splitSizeInBytes];
diff --git 
a/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/source/enumerator/FlinkSourceEnumeratorTest.java
 
b/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/source/enumerator/FlinkSourceEnumeratorTest.java
index 6d239f965..184e1b6e7 100644
--- 
a/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/source/enumerator/FlinkSourceEnumeratorTest.java
+++ 
b/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/source/enumerator/FlinkSourceEnumeratorTest.java
@@ -25,6 +25,8 @@ import org.apache.fluss.config.Configuration;
 import org.apache.fluss.flink.FlinkConnectorOptions;
 import org.apache.fluss.flink.lake.split.LakeSnapshotAndFlussLogSplit;
 import org.apache.fluss.flink.lake.split.LakeSnapshotSplit;
+import org.apache.fluss.flink.source.FlinkSource;
+import org.apache.fluss.flink.source.deserializer.RowDataDeserializationSchema;
 import org.apache.fluss.flink.source.event.PartitionBucketsUnsubscribedEvent;
 import org.apache.fluss.flink.source.event.PartitionsRemovedEvent;
 import org.apache.fluss.flink.source.reader.LeaseContext;
@@ -32,6 +34,7 @@ import 
org.apache.fluss.flink.source.split.HybridSnapshotLogSplit;
 import org.apache.fluss.flink.source.split.LogSplit;
 import org.apache.fluss.flink.source.split.SnapshotSplit;
 import org.apache.fluss.flink.source.split.SourceSplitBase;
+import org.apache.fluss.flink.source.state.SourceEnumeratorState;
 import org.apache.fluss.flink.utils.FlinkTestBase;
 import org.apache.fluss.lake.source.LakeSource;
 import org.apache.fluss.lake.source.LakeSplit;
@@ -53,8 +56,10 @@ import org.apache.fluss.types.DataTypes;
 
 import org.apache.flink.api.connector.source.ReaderInfo;
 import org.apache.flink.api.connector.source.SourceEvent;
+import org.apache.flink.api.connector.source.SplitEnumerator;
 import org.apache.flink.api.connector.source.SplitsAssignment;
 import org.apache.flink.api.connector.source.mocks.MockSplitEnumeratorContext;
+import org.apache.flink.table.data.RowData;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
@@ -210,6 +215,100 @@ class FlinkSourceEnumeratorTest extends FlinkTestBase {
         }
     }
 
+    @Test
+    void 
testRestoreFlussOnlySourceWithLakeSourceDoesNotGenerateLakeSplits(@TempDir Path 
tempDir)
+            throws Throwable {
+        long tableId =
+                createTable(DEFAULT_TABLE_PATH, 
DEFAULT_AUTO_PARTITIONED_LOG_TABLE_DESCRIPTOR);
+        ZooKeeperClient zooKeeperClient = 
FLUSS_CLUSTER_EXTENSION.getZooKeeperClient();
+        Map<Long, String> partitionNameByIds =
+                waitUntilPartitions(zooKeeperClient, DEFAULT_TABLE_PATH);
+        Long partitionId = 
partitionNameByIds.keySet().stream().sorted().findFirst().get();
+        String partitionName = partitionNameByIds.get(partitionId);
+
+        LakeTableSnapshot lakeTableSnapshot =
+                new LakeTableSnapshot(
+                        0,
+                        ImmutableMap.of(
+                                new TableBucket(tableId, partitionId, 0), 50L,
+                                new TableBucket(tableId, partitionId, 1), 50L,
+                                new TableBucket(tableId, partitionId, 2), 
50L));
+        LakeTableHelper lakeTableHelper = new LakeTableHelper(zooKeeperClient, 
tempDir.toString());
+        lakeTableHelper.registerLakeTableSnapshotV1(tableId, 
lakeTableSnapshot);
+
+        ResolvedPartitionSpec partitionSpec =
+                ResolvedPartitionSpec.fromPartitionName(
+                        Collections.singletonList("name"), partitionName);
+        LakeSource<LakeSplit> lakeSource =
+                new TestingLakeSource(
+                        DEFAULT_BUCKET_NUM,
+                        Collections.singletonList(
+                                new PartitionInfo(
+                                        partitionId, partitionSpec, 
DEFAULT_REMOTE_DATA_DIR)));
+
+        SourceEnumeratorState checkpointState;
+        try (MockSplitEnumeratorContext<SourceSplitBase> context =
+                        new MockSplitEnumeratorContext<>(1);
+                SplitEnumerator<SourceSplitBase, SourceEnumeratorState> 
enumerator =
+                        new FlinkSource<RowData>(
+                                        flussConf,
+                                        DEFAULT_TABLE_PATH,
+                                        false,
+                                        true,
+                                        DEFAULT_LOG_TABLE_SCHEMA.getRowType(),
+                                        null,
+                                        null,
+                                        OffsetsInitializer.timestamp(1000L),
+                                        0L,
+                                        new RowDataDeserializationSchema(),
+                                        streaming,
+                                        null,
+                                        LeaseContext.DEFAULT)
+                                .createEnumerator(context)) {
+            checkpointState = enumerator.snapshotState(1L);
+            
assertThat(checkpointState.getRemainingHybridLakeFlussSplits()).isNull();
+        }
+
+        try (MockSplitEnumeratorContext<SourceSplitBase> context =
+                        new MockSplitEnumeratorContext<>(DEFAULT_BUCKET_NUM);
+                SplitEnumerator<SourceSplitBase, SourceEnumeratorState> 
restoredEnumerator =
+                        new FlinkSource<RowData>(
+                                        flussConf,
+                                        DEFAULT_TABLE_PATH,
+                                        false,
+                                        true,
+                                        DEFAULT_LOG_TABLE_SCHEMA.getRowType(),
+                                        null,
+                                        null,
+                                        OffsetsInitializer.full(),
+                                        0L,
+                                        new RowDataDeserializationSchema(),
+                                        streaming,
+                                        null,
+                                        lakeSource,
+                                        LeaseContext.DEFAULT)
+                                .restoreEnumerator(context, checkpointState)) {
+            
assertThat(restoredEnumerator.snapshotState(1L).getRemainingHybridLakeFlussSplits())
+                    .isEmpty();
+
+            restoredEnumerator.start();
+            context.runNextOneTimeCallable();
+            context.runNextOneTimeCallable();
+
+            for (int i = 0; i < DEFAULT_BUCKET_NUM; i++) {
+                registerReader(context, restoredEnumerator, i);
+            }
+
+            List<SourceSplitBase> assignedSplits =
+                    getReadersAssignments(context).values().stream()
+                            .flatMap(List::stream)
+                            .collect(Collectors.toList());
+            assertThat(assignedSplits).isNotEmpty();
+            assertThat(assignedSplits).allMatch(split -> split instanceof 
LogSplit);
+            assertThat(assignedSplits).noneMatch(split -> split instanceof 
LakeSnapshotSplit);
+        }
+    }
+
     @Test
     void testPkTableWithSnapshotSplits() throws Throwable {
         long tableId = createTable(DEFAULT_TABLE_PATH, 
DEFAULT_PK_TABLE_DESCRIPTOR);
@@ -852,7 +951,7 @@ class FlinkSourceEnumeratorTest extends FlinkTestBase {
     // ---------------------
     private void registerReader(
             MockSplitEnumeratorContext<SourceSplitBase> context,
-            FlinkSourceEnumerator enumerator,
+            SplitEnumerator<SourceSplitBase, SourceEnumeratorState> enumerator,
             int readerId) {
         context.registerReader(new ReaderInfo(readerId, "location " + 
readerId));
         enumerator.addReader(readerId);
diff --git 
a/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/source/state/SourceEnumeratorStateSerializerTest.java
 
b/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/source/state/SourceEnumeratorStateSerializerTest.java
index 5273d12d1..fa90eda96 100644
--- 
a/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/source/state/SourceEnumeratorStateSerializerTest.java
+++ 
b/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/source/state/SourceEnumeratorStateSerializerTest.java
@@ -169,4 +169,26 @@ class SourceEnumeratorStateSerializerTest {
                 serializer.deserialize(serializer.getVersion(), serialized);
         
assertThat(deserializedSourceEnumeratorState).isEqualTo(sourceEnumeratorState);
     }
+
+    @Test
+    void testEmptyPendingSplitsCheckpointSerdeWithoutLakeSource() throws 
Exception {
+        FlussSourceEnumeratorStateSerializer serializer =
+                new FlussSourceEnumeratorStateSerializer(null);
+
+        SourceEnumeratorState sourceEnumeratorState =
+                new SourceEnumeratorState(
+                        Collections.emptySet(),
+                        Collections.emptyMap(),
+                        Collections.emptyList(),
+                        LeaseContext.DEFAULT.getKvSnapshotLeaseId());
+
+        byte[] serialized = serializer.serialize(sourceEnumeratorState);
+        SourceEnumeratorState deserializedSourceEnumeratorState =
+                serializer.deserialize(serializer.getVersion(), serialized);
+
+        
assertThat(deserializedSourceEnumeratorState).isEqualTo(sourceEnumeratorState);
+        
assertThat(deserializedSourceEnumeratorState.getRemainingHybridLakeFlussSplits())
+                .isNotNull()
+                .isEmpty();
+    }
 }

Reply via email to