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

davidzollo pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git


The following commit(s) were added to refs/heads/dev by this push:
     new 6fc3445df7 [Improve][Zeta] Optimize WAL recovery memory usage (#11494)
6fc3445df7 is described below

commit 6fc3445df7b4475b2c82ece7fd80b7e769b920e1
Author: Doyeon Kim <[email protected]>
AuthorDate: Tue Sep 22 23:00:44 2026 +0900

    [Improve][Zeta] Optimize WAL recovery memory usage (#11494)
---
 .../file/common/LatestMutationAccumulator.java     | 101 +++++++++++++++++++++
 .../engine/imap/storage/file/common/WALReader.java |  92 +++++++++----------
 .../storage/file/wal/reader/DefaultReader.java     |  92 +++++++++----------
 .../imap/storage/file/wal/reader/IFileReader.java  |  18 ++++
 .../file/common/LatestMutationAccumulatorTest.java |  64 +++++++++++++
 .../file/common/WALReaderAndWriterTest.java        |  90 ++++++++++++++++++
 6 files changed, 362 insertions(+), 95 deletions(-)

diff --git 
a/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/main/java/org/apache/seatunnel/engine/imap/storage/file/common/LatestMutationAccumulator.java
 
b/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/main/java/org/apache/seatunnel/engine/imap/storage/file/common/LatestMutationAccumulator.java
new file mode 100644
index 0000000000..f8e5a705a4
--- /dev/null
+++ 
b/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/main/java/org/apache/seatunnel/engine/imap/storage/file/common/LatestMutationAccumulator.java
@@ -0,0 +1,101 @@
+/*
+ * 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.seatunnel.engine.imap.storage.file.common;
+
+import org.apache.seatunnel.engine.imap.storage.file.bean.IMapFileData;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/** Retains only the latest WAL mutation for each serialized key. */
+class LatestMutationAccumulator {
+
+    private final Map<SerializedKey, IMapFileData> latestMutations = new 
HashMap<>();
+    private final Set<SerializedKey> searchKeys;
+
+    LatestMutationAccumulator(Set<SerializedKey> searchKeys) {
+        this.searchKeys = searchKeys;
+    }
+
+    void accept(IMapFileData mutation) {
+        SerializedKey serializedKey =
+                SerializedKey.of(mutation.getKey(), 
mutation.getKeyClassName());
+        if (searchKeys != null && !searchKeys.contains(serializedKey)) {
+            return;
+        }
+
+        IMapFileData current = latestMutations.get(serializedKey);
+        if (current == null) {
+            latestMutations.put(serializedKey, mutation);
+            return;
+        }
+        if (mutation.compareTo(current) < 0) {
+            latestMutations.put(serializedKey, mutation);
+        }
+    }
+
+    int size() {
+        return latestMutations.size();
+    }
+
+    Collection<IMapFileData> values() {
+        return latestMutations.values();
+    }
+
+    Iterator<IMapFileData> iterator() {
+        return latestMutations.values().iterator();
+    }
+
+    static final class SerializedKey {
+        private final byte[] key;
+        private final String className;
+        private final int hashCode;
+
+        private SerializedKey(byte[] key, String className) {
+            this.key = key;
+            this.className = className;
+            this.hashCode = 31 * Arrays.hashCode(key) + 
Objects.hashCode(className);
+        }
+
+        static SerializedKey of(byte[] key, String className) {
+            return new SerializedKey(key, className);
+        }
+
+        @Override
+        public boolean equals(Object object) {
+            if (this == object) {
+                return true;
+            }
+            if (!(object instanceof SerializedKey)) {
+                return false;
+            }
+            SerializedKey that = (SerializedKey) object;
+            return Arrays.equals(key, that.key) && Objects.equals(className, 
that.className);
+        }
+
+        @Override
+        public int hashCode() {
+            return hashCode;
+        }
+    }
+}
diff --git 
a/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/main/java/org/apache/seatunnel/engine/imap/storage/file/common/WALReader.java
 
b/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/main/java/org/apache/seatunnel/engine/imap/storage/file/common/WALReader.java
index f4284b6418..f026e2f940 100644
--- 
a/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/main/java/org/apache/seatunnel/engine/imap/storage/file/common/WALReader.java
+++ 
b/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/main/java/org/apache/seatunnel/engine/imap/storage/file/common/WALReader.java
@@ -29,21 +29,21 @@ import 
org.apache.seatunnel.engine.imap.storage.file.wal.DiscoveryWalFileFactory
 import org.apache.seatunnel.engine.imap.storage.file.wal.reader.IFileReader;
 import org.apache.seatunnel.engine.serializer.api.Serializer;
 
-import org.apache.commons.collections.CollectionUtils;
 import org.apache.hadoop.fs.FileSystem;
 import org.apache.hadoop.fs.Path;
 
 import java.io.IOException;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.List;
+import java.util.Iterator;
 import java.util.Map;
 import java.util.Set;
 
+import static 
org.apache.seatunnel.engine.imap.storage.file.common.LatestMutationAccumulator.SerializedKey;
+
 public class WALReader {
     private final Serializer serializer;
-    private final IFileReader fileReader;
+    private final IFileReader<IMapFileData> fileReader;
 
     public WALReader(FileSystem fs, FileConfiguration configuration, 
Serializer serializer)
             throws IOException {
@@ -52,66 +52,60 @@ public class WALReader {
         this.fileReader.initialize(fs, serializer);
     }
 
-    private List<IMapFileData> readAllData(Path parentPath) throws IOException 
{
-        return this.fileReader.readAllData(parentPath);
-    }
-
     public Set<Object> loadAllKeys(Path parentPath) throws IOException {
-        List<IMapFileData> allData = readAllData(parentPath);
-        if (CollectionUtils.isEmpty(allData)) {
-            return new HashSet<>();
-        }
-        Collections.sort(allData);
-        Set<Object> result = new HashSet<>(allData.size());
-        Map<Object, Long> deleteMap = new HashMap<>();
-        for (IMapFileData data : allData) {
-            Object key = deserializeData(data.getKey(), 
data.getKeyClassName());
-            if (deleteMap.containsKey(key)) {
-                continue;
-            }
-            if (data.isDeleted()) {
-                deleteMap.put(key, data.getTimestamp());
-                continue;
-            }
-            if (result.contains(key)) {
-                continue;
+        LatestMutationAccumulator accumulator = 
readLatestMutations(parentPath, null);
+        Set<Object> result = new HashSet<>(accumulator.size());
+        Iterator<IMapFileData> mutations = accumulator.iterator();
+        while (mutations.hasNext()) {
+            IMapFileData mutation = mutations.next();
+            mutations.remove();
+            if (!mutation.isDeleted()) {
+                result.add(deserializeData(mutation.getKey(), 
mutation.getKeyClassName()));
             }
-            result.add(key);
         }
         return result;
     }
 
     public Map<Object, Object> loadAllData(Path parentPath, Set<Object> 
searchKeys)
             throws IOException {
-        List<IMapFileData> allData = readAllData(parentPath);
-        if (CollectionUtils.isEmpty(allData)) {
-            return new HashMap<>();
-        }
-        Collections.sort(allData);
-        Map<Object, Object> result = new HashMap<>(allData.size());
-        Map<Object, Long> deleteMap = new HashMap<>();
-        boolean searchByKeys = CollectionUtils.isNotEmpty(searchKeys);
-        for (IMapFileData data : allData) {
-            Object key = deserializeData(data.getKey(), 
data.getKeyClassName());
-            if (searchByKeys && !searchKeys.contains(data.getKey())) {
-                continue;
-            }
-            if (deleteMap.containsKey(key)) {
+        Set<SerializedKey> serializedSearchKeys = 
serializeSearchKeys(searchKeys);
+        LatestMutationAccumulator accumulator =
+                readLatestMutations(parentPath, serializedSearchKeys);
+        Map<Object, Object> result = new HashMap<>(accumulator.size());
+        Iterator<IMapFileData> mutations = accumulator.iterator();
+        while (mutations.hasNext()) {
+            IMapFileData mutation = mutations.next();
+            mutations.remove();
+            if (mutation.isDeleted()) {
                 continue;
             }
-            if (data.isDeleted()) {
-                deleteMap.put(key, data.getTimestamp());
-                continue;
-            }
-            if (result.containsKey(key)) {
-                continue;
-            }
-            Object value = deserializeData(data.getValue(), 
data.getValueClassName());
+            Object key = deserializeData(mutation.getKey(), 
mutation.getKeyClassName());
+            Object value = deserializeData(mutation.getValue(), 
mutation.getValueClassName());
             result.put(key, value);
         }
         return result;
     }
 
+    private LatestMutationAccumulator readLatestMutations(
+            Path parentPath, Set<SerializedKey> serializedSearchKeys) throws 
IOException {
+        LatestMutationAccumulator accumulator = new 
LatestMutationAccumulator(serializedSearchKeys);
+        fileReader.forEachData(parentPath, accumulator::accept);
+        return accumulator;
+    }
+
+    private Set<SerializedKey> serializeSearchKeys(Set<Object> searchKeys) 
throws IOException {
+        if (searchKeys == null || searchKeys.isEmpty()) {
+            return null;
+        }
+
+        Set<SerializedKey> serializedKeys = new HashSet<>(searchKeys.size());
+        for (Object key : searchKeys) {
+            serializedKeys.add(
+                    SerializedKey.of(serializer.serialize(key), 
key.getClass().getName()));
+        }
+        return serializedKeys;
+    }
+
     private Object deserializeData(byte[] data, String className) {
         try {
             Class<?> clazz = ClassUtils.getClass(className);
diff --git 
a/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/main/java/org/apache/seatunnel/engine/imap/storage/file/wal/reader/DefaultReader.java
 
b/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/main/java/org/apache/seatunnel/engine/imap/storage/file/wal/reader/DefaultReader.java
index f8d5e7fadf..4d900bb4d3 100644
--- 
a/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/main/java/org/apache/seatunnel/engine/imap/storage/file/wal/reader/DefaultReader.java
+++ 
b/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/main/java/org/apache/seatunnel/engine/imap/storage/file/wal/reader/DefaultReader.java
@@ -19,18 +19,18 @@
  */
 package org.apache.seatunnel.engine.imap.storage.file.wal.reader;
 
-import 
org.apache.seatunnel.engine.imap.storage.api.exception.IMapStorageException;
 import org.apache.seatunnel.engine.imap.storage.file.bean.IMapFileData;
 import org.apache.seatunnel.engine.imap.storage.file.common.WALDataUtils;
 import org.apache.seatunnel.engine.serializer.api.Serializer;
 
-import org.apache.commons.collections.CollectionUtils;
-import org.apache.hadoop.fs.FSDataInputStream;
 import org.apache.hadoop.fs.FileSystem;
 import org.apache.hadoop.fs.LocatedFileStatus;
 import org.apache.hadoop.fs.Path;
 import org.apache.hadoop.fs.RemoteIterator;
 
+import java.io.BufferedInputStream;
+import java.io.DataInputStream;
+import java.io.EOFException;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
@@ -38,7 +38,6 @@ import java.util.List;
 import static 
org.apache.seatunnel.engine.imap.storage.file.common.WALDataUtils.WAL_DATA_METADATA_LENGTH;
 
 public class DefaultReader implements IFileReader<IMapFileData> {
-    private static final int DEFAULT_QUERY_LIST_SIZE = 1024;
     FileSystem fs;
     Serializer serializer;
 
@@ -55,60 +54,61 @@ public class DefaultReader implements 
IFileReader<IMapFileData> {
 
     @Override
     public List<IMapFileData> readAllData(Path parentPath) throws IOException {
-        List<String> fileNames = getFileNames(parentPath);
-        if (CollectionUtils.isEmpty(fileNames)) {
-            return new ArrayList<>();
-        }
-        List<IMapFileData> result = new ArrayList<>(DEFAULT_QUERY_LIST_SIZE);
-        for (String fileName : fileNames) {
-            result.addAll(readData(new Path(parentPath, fileName)));
-        }
+        List<IMapFileData> result = new ArrayList<>();
+        forEachData(parentPath, result::add);
         return result;
     }
 
-    private List<String> getFileNames(Path parentPath) {
-        try {
-            if (!fs.exists(parentPath)) {
-                return new ArrayList<>();
-            }
-            RemoteIterator<LocatedFileStatus> fileStatusRemoteIterator =
-                    fs.listFiles(parentPath, true);
-            List<String> fileNames = new ArrayList<>();
-            while (fileStatusRemoteIterator.hasNext()) {
-                LocatedFileStatus fileStatus = fileStatusRemoteIterator.next();
-                if (fileStatus.getPath().getName().endsWith("wal.txt")) {
-                    fileNames.add(fileStatus.getPath().toString());
-                }
+    @Override
+    public void forEachData(Path parentPath, RecordConsumer<IMapFileData> 
consumer)
+            throws IOException {
+        if (!fs.exists(parentPath)) {
+            return;
+        }
+
+        RemoteIterator<LocatedFileStatus> files = fs.listFiles(parentPath, 
true);
+        while (files.hasNext()) {
+            LocatedFileStatus file = files.next();
+            if (file.getPath().getName().endsWith("wal.txt")) {
+                readData(file, consumer);
             }
-            return fileNames;
-        } catch (IOException e) {
-            throw new IMapStorageException(e, "get file names error,path is 
s%", parentPath);
         }
     }
 
-    private List<IMapFileData> readData(Path path) throws IOException {
-        List<IMapFileData> result = new ArrayList<>(DEFAULT_QUERY_LIST_SIZE);
-        long length = fs.getFileStatus(path).getLen();
-        try (FSDataInputStream in = fs.open(path)) {
-            byte[] datas = new byte[(int) length];
-            in.readFully(datas);
-            int startIndex = 0;
-            while (startIndex + WAL_DATA_METADATA_LENGTH < datas.length) {
+    private void readData(LocatedFileStatus file, RecordConsumer<IMapFileData> 
consumer)
+            throws IOException {
+        long remainingBytes = file.getLen();
+        byte[] metadata = new byte[WAL_DATA_METADATA_LENGTH];
+        try (DataInputStream input =
+                new DataInputStream(new 
BufferedInputStream(fs.open(file.getPath())))) {
+            while (remainingBytes >= WAL_DATA_METADATA_LENGTH) {
+                if (!readFully(input, metadata)) {
+                    break;
+                }
+                remainingBytes -= WAL_DATA_METADATA_LENGTH;
 
-                byte[] metadata = new byte[WAL_DATA_METADATA_LENGTH];
-                System.arraycopy(datas, startIndex, metadata, 0, 
WAL_DATA_METADATA_LENGTH);
                 int dataLength = WALDataUtils.byteArrayToInt(metadata);
-                startIndex += WAL_DATA_METADATA_LENGTH;
-                if (startIndex + dataLength > datas.length) {
+                if (dataLength > remainingBytes) {
+                    // The writer may have stopped in the middle of its last 
record.
                     break;
                 }
-                byte[] data = new byte[dataLength];
-                System.arraycopy(datas, startIndex, data, 0, data.length);
-                IMapFileData fileData = serializer.deserialize(data, 
IMapFileData.class);
-                result.add(fileData);
-                startIndex += data.length;
+
+                byte[] serializedRecord = new byte[dataLength];
+                if (!readFully(input, serializedRecord)) {
+                    break;
+                }
+                remainingBytes -= dataLength;
+                consumer.accept(serializer.deserialize(serializedRecord, 
IMapFileData.class));
             }
         }
-        return result;
+    }
+
+    private boolean readFully(DataInputStream input, byte[] target) throws 
IOException {
+        try {
+            input.readFully(target);
+            return true;
+        } catch (EOFException ignored) {
+            return false;
+        }
     }
 }
diff --git 
a/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/main/java/org/apache/seatunnel/engine/imap/storage/file/wal/reader/IFileReader.java
 
b/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/main/java/org/apache/seatunnel/engine/imap/storage/file/wal/reader/IFileReader.java
index 1fbc85706e..e382a91b0a 100644
--- 
a/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/main/java/org/apache/seatunnel/engine/imap/storage/file/wal/reader/IFileReader.java
+++ 
b/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/main/java/org/apache/seatunnel/engine/imap/storage/file/wal/reader/IFileReader.java
@@ -33,4 +33,22 @@ public interface IFileReader<R> {
     void initialize(FileSystem fs, Serializer serializer) throws IOException;
 
     List<R> readAllData(Path parentPath) throws IOException;
+
+    /**
+     * Passes records to the consumer.
+     *
+     * <p>Implementations should override this method to stream records 
without retaining the
+     * complete history in memory. The default implementation preserves 
compatibility with readers
+     * that only implement {@link #readAllData(Path)}.
+     */
+    default void forEachData(Path parentPath, RecordConsumer<R> consumer) 
throws IOException {
+        for (R record : readAllData(parentPath)) {
+            consumer.accept(record);
+        }
+    }
+
+    @FunctionalInterface
+    interface RecordConsumer<R> {
+        void accept(R record) throws IOException;
+    }
 }
diff --git 
a/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/test/java/org/apache/seatunnel/engine/imap/storage/file/common/LatestMutationAccumulatorTest.java
 
b/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/test/java/org/apache/seatunnel/engine/imap/storage/file/common/LatestMutationAccumulatorTest.java
new file mode 100644
index 0000000000..fab89287a1
--- /dev/null
+++ 
b/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/test/java/org/apache/seatunnel/engine/imap/storage/file/common/LatestMutationAccumulatorTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.seatunnel.engine.imap.storage.file.common;
+
+import org.apache.seatunnel.engine.imap.storage.file.bean.IMapFileData;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+
+class LatestMutationAccumulatorTest {
+
+    @Test
+    void shouldRetainAtMostOneMutationPerKey() {
+        LatestMutationAccumulator accumulator = new 
LatestMutationAccumulator(null);
+
+        for (int version = 0; version < 100_000; version++) {
+            accumulator.accept(mutation("key-" + (version % 100), version, 
false));
+        }
+
+        Assertions.assertEquals(100, accumulator.size());
+        Assertions.assertTrue(
+                accumulator.values().stream()
+                        .allMatch(mutation -> mutation.getTimestamp() >= 
99_900));
+    }
+
+    @Test
+    void shouldChooseNewerMutationRegardlessOfArrivalOrder() {
+        LatestMutationAccumulator accumulator = new 
LatestMutationAccumulator(null);
+
+        accumulator.accept(mutation("key", 20, false));
+        accumulator.accept(mutation("key", 10, false));
+
+        Assertions.assertEquals(1, accumulator.size());
+        Assertions.assertEquals(20, 
accumulator.values().iterator().next().getTimestamp());
+    }
+
+    private static IMapFileData mutation(String key, long timestamp, boolean 
deleted) {
+        return IMapFileData.builder()
+                .key(key.getBytes(StandardCharsets.UTF_8))
+                .keyClassName(String.class.getName())
+                .value(("value-" + timestamp).getBytes(StandardCharsets.UTF_8))
+                .valueClassName(String.class.getName())
+                .timestamp(timestamp)
+                .deleted(deleted)
+                .build();
+    }
+}
diff --git 
a/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/test/java/org/apache/seatunnel/engine/imap/storage/file/common/WALReaderAndWriterTest.java
 
b/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/test/java/org/apache/seatunnel/engine/imap/storage/file/common/WALReaderAndWriterTest.java
index 917d02cf79..6b1fb4b7ff 100644
--- 
a/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/test/java/org/apache/seatunnel/engine/imap/storage/file/common/WALReaderAndWriterTest.java
+++ 
b/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/test/java/org/apache/seatunnel/engine/imap/storage/file/common/WALReaderAndWriterTest.java
@@ -26,18 +26,23 @@ import 
org.apache.seatunnel.engine.serializer.api.Serializer;
 import org.apache.seatunnel.engine.serializer.protobuf.ProtoStuffSerializer;
 
 import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.FSDataOutputStream;
 import org.apache.hadoop.fs.FileSystem;
 import org.apache.hadoop.fs.Path;
 
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.condition.EnabledOnOs;
 
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 
 import static org.awaitility.Awaitility.await;
 import static org.junit.jupiter.api.condition.OS.LINUX;
@@ -60,6 +65,11 @@ public class WALReaderAndWriterTest {
         FS = FileSystem.getLocal(conf);
     }
 
+    @BeforeEach
+    void cleanWalDirectory() throws IOException {
+        FS.delete(PARENT_PATH, true);
+    }
+
     @Test
     public void testWriterAndReader() throws Exception {
         WALWriter writer = new WALWriter(FS, FileConfiguration.HDFS, 
PARENT_PATH, SERIALIZER);
@@ -116,6 +126,86 @@ public class WALReaderAndWriterTest {
         Assertions.assertNull(result.get("key519"));
     }
 
+    @Test
+    void shouldChooseLatestMutationsAcrossUnsortedFiles() throws IOException {
+        Path path = new Path(PARENT_PATH, "unsorted");
+        writeWalFile(
+                new Path(path, "region-a/older-wal.txt"),
+                mutation("updated", "old", 10, false),
+                mutation("deleted", "old", 10, false));
+        writeWalFile(
+                new Path(path, "region-b/newer-wal.txt"),
+                mutation("updated", "new", 20, false),
+                mutation("deleted", null, 20, true));
+
+        WALReader reader = new WALReader(FS, FileConfiguration.HDFS, 
SERIALIZER);
+        Map<Object, Object> result = reader.loadAllData(path, 
Collections.emptySet());
+        Set<Object> keys = reader.loadAllKeys(path);
+
+        Assertions.assertEquals(Collections.singletonMap("updated", "new"), 
result);
+        Assertions.assertEquals(Collections.singleton("updated"), keys);
+    }
+
+    @Test
+    void shouldRetainOnlyRequestedKeysWhileScanning() throws IOException {
+        Path path = new Path(PARENT_PATH, "selective");
+        writeWalFile(
+                new Path(path, "region/selective-wal.txt"),
+                mutation("key-a", "value-a", 10, false),
+                mutation("key-b", "value-b", 20, false),
+                mutation("key-c", "value-c", 30, false));
+
+        WALReader reader = new WALReader(FS, FileConfiguration.HDFS, 
SERIALIZER);
+        Map<Object, Object> result =
+                reader.loadAllData(path, new HashSet<>(Arrays.asList("key-a", 
"missing")));
+
+        Assertions.assertEquals(Collections.singletonMap("key-a", "value-a"), 
result);
+    }
+
+    @Test
+    void shouldIgnoreIncompleteTrailingRecord() throws IOException {
+        Path path = new Path(PARENT_PATH, "partial");
+        Path file = new Path(path, "region/partial-wal.txt");
+        FS.mkdirs(file.getParent());
+        byte[] complete = wrapped(mutation("complete", "value", 10, false));
+        byte[] incomplete = wrapped(mutation("partial", "ignored", 20, false));
+        try (FSDataOutputStream output = FS.create(file, true)) {
+            output.write(complete);
+            output.write(incomplete, 0, WALDataUtils.WAL_DATA_METADATA_LENGTH 
+ 3);
+        }
+
+        WALReader reader = new WALReader(FS, FileConfiguration.HDFS, 
SERIALIZER);
+
+        Assertions.assertEquals(
+                Collections.singletonMap("complete", "value"),
+                reader.loadAllData(path, Collections.emptySet()));
+    }
+
+    private static IMapFileData mutation(String key, String value, long 
timestamp, boolean deleted)
+            throws IOException {
+        return IMapFileData.builder()
+                .key(SERIALIZER.serialize(key))
+                .keyClassName(String.class.getName())
+                .value(deleted ? null : SERIALIZER.serialize(value))
+                .valueClassName(deleted ? null : String.class.getName())
+                .timestamp(timestamp)
+                .deleted(deleted)
+                .build();
+    }
+
+    private static void writeWalFile(Path file, IMapFileData... mutations) 
throws IOException {
+        FS.mkdirs(file.getParent());
+        try (FSDataOutputStream output = FS.create(file, true)) {
+            for (IMapFileData mutation : mutations) {
+                output.write(wrapped(mutation));
+            }
+        }
+    }
+
+    private static byte[] wrapped(IMapFileData mutation) throws IOException {
+        return WALDataUtils.wrapperBytes(SERIALIZER.serialize(mutation));
+    }
+
     @Test
     public void testReplayKeepsTombstoneForSameKeyAndTimestamp() throws 
Exception {
         String key = "deleted-key";

Reply via email to