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

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/dev/pr-12973-016f2c153f25d1e9ce20a7250b44ab9e626a3335
in repository https://gitbox.apache.org/repos/asf/seatunnel.git

commit 570eb976113567efc3cbaa8160dbf8c1cab3c2ac
Author: Doyeon Kim <[email protected]>
AuthorDate: Sun Oct 25 16:21:48 2026 +0000

    [Fix][Zeta] Keep IMap entry expiration times across restarts in the file 
map store (#12973)
---
 docs/en/engines/zeta/state-storage-and-recovery.md |   3 +-
 docs/zh/engines/zeta/state-storage-and-recovery.md |   3 +-
 .../engine/e2e/ClusterFaultToleranceIT.java        | 106 +++++++++++++++++++
 .../engine/server/persistence/FileMapStore.java    |  52 +++++++--
 .../server/persistence/FileMapStoreFactory.java    |   6 +-
 .../server/persistence/FileMapStoreTest.java       |  76 ++++++++++++-
 .../engine/imap/storage/api/ExpirableValue.java    |  70 ++++++++++++
 .../engine/imap/storage/api/IMapStorage.java       |  47 +++++++++
 .../engine/imap/storage/file/IMapFileStorage.java  |  36 ++++++-
 .../imap/storage/file/bean/IMapFileData.java       |   9 ++
 .../engine/imap/storage/file/common/WALReader.java |  51 ++++++++-
 .../imap/storage/file/IMapFileStorageTest.java     |  25 +++++
 .../file/common/WALReaderAndWriterTest.java        | 117 +++++++++++++++++++++
 13 files changed, 578 insertions(+), 23 deletions(-)

diff --git a/docs/en/engines/zeta/state-storage-and-recovery.md 
b/docs/en/engines/zeta/state-storage-and-recovery.md
index b0a7460cb1..c60b69539f 100644
--- a/docs/en/engines/zeta/state-storage-and-recovery.md
+++ b/docs/en/engines/zeta/state-storage-and-recovery.md
@@ -240,7 +240,8 @@ seatunnel:
 |---|---|
 | Remove from `finished-job-state` IMap | Yes |
 | Remove from `finished-job-metrics` IMap | Yes |
-| Delete MapStore persistence files for that job | Yes (after IMap eviction) |
+| Keep the job removed after a cluster restart (IMap persistence enabled) | 
Yes (the expiration time is persisted with the entry, so an expired job is not 
loaded again and a job that has not expired yet keeps its remaining time) |
+| Delete MapStore persistence files for that job | **No** (the expired records 
stay in the IMap storage files) |
 | Delete checkpoint storage directories | **No** |
 | Delete savepoint directories | **No** |
 | Delete WAL files | **No** (only indirectly via compaction) |
diff --git a/docs/zh/engines/zeta/state-storage-and-recovery.md 
b/docs/zh/engines/zeta/state-storage-and-recovery.md
index 840dd109f0..98af1592d8 100644
--- a/docs/zh/engines/zeta/state-storage-and-recovery.md
+++ b/docs/zh/engines/zeta/state-storage-and-recovery.md
@@ -231,7 +231,8 @@ seatunnel:
 |---|---|
 | 从 `finished-job-state` IMap 中移除 | 是 |
 | 从 `finished-job-metrics` IMap 中移除 | 是 |
-| 删除该作业的 MapStore 持久化文件 | 是(IMap 驱逐后) |
+| 集群重启后作业仍保持已移除(启用 IMap 持久化时) | 是(过期时间随条目一起持久化,已过期的作业不会被重新加载,尚未过期的作业保留剩余时间) |
+| 删除该作业的 MapStore 持久化文件 | **否**(过期记录仍保留在 IMap 存储文件中) |
 | 删除 Checkpoint 存储目录 | **否** |
 | 删除 Savepoint 目录 | **否** |
 | 删除 WAL 文件 | **否**(仅通过压缩间接处理)|
diff --git 
a/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/ClusterFaultToleranceIT.java
 
b/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/ClusterFaultToleranceIT.java
index 675d89279e..f99cca0950 100644
--- 
a/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/ClusterFaultToleranceIT.java
+++ 
b/seatunnel-e2e/seatunnel-engine-e2e/connector-seatunnel-e2e-base/src/test/java/org/apache/seatunnel/engine/e2e/ClusterFaultToleranceIT.java
@@ -25,6 +25,7 @@ import org.apache.seatunnel.common.utils.FileUtils;
 import org.apache.seatunnel.engine.client.SeaTunnelClient;
 import org.apache.seatunnel.engine.client.job.ClientJobExecutionEnvironment;
 import org.apache.seatunnel.engine.client.job.ClientJobProxy;
+import org.apache.seatunnel.engine.common.Constant;
 import org.apache.seatunnel.engine.common.config.ConfigProvider;
 import org.apache.seatunnel.engine.common.config.JobConfig;
 import org.apache.seatunnel.engine.common.config.SeaTunnelConfig;
@@ -39,6 +40,7 @@ import 
org.testcontainers.shaded.org.apache.commons.lang3.tuple.ImmutablePair;
 
 import com.hazelcast.client.config.ClientConfig;
 import com.hazelcast.config.Config;
+import com.hazelcast.core.EntryView;
 import com.hazelcast.instance.impl.HazelcastInstanceImpl;
 import lombok.NonNull;
 import lombok.extern.slf4j.Slf4j;
@@ -951,6 +953,110 @@ public class ClusterFaultToleranceIT {
         }
     }
 
+    /**
+     * A finished job is kept in the history maps with a TTL of {@code 
history-job-expire-minutes}.
+     * Hazelcast does not delete expired entries from the map store, so the 
job must keep its
+     * expiration time after the cluster restarts from the IMap file storage 
instead of being loaded
+     * back without one and never expiring.
+     */
+    @Test
+    public void testFinishedJobKeepsExpirationTimeAfterAllNodeDown() throws 
Exception {
+        String testCaseName = 
"testFinishedJobKeepsExpirationTimeAfterAllNodeDown";
+        String testClusterName =
+                
"ClusterFaultToleranceIT_testFinishedJobKeepsExpirationTimeAfterAllNodeDown_"
+                        + System.currentTimeMillis();
+        HazelcastInstanceImpl node = null;
+        SeaTunnelClient engineClient = null;
+
+        try {
+            String yaml =
+                    "hazelcast:\n"
+                            + "  network:\n"
+                            + "    join:\n"
+                            + "      tcp-ip:\n"
+                            + "        enabled: true\n"
+                            + "        member-list:\n"
+                            + "          - localhost\n"
+                            + "    port:\n"
+                            + "      auto-increment: true\n"
+                            + "      port-count: 100\n"
+                            + "      port: 5801\n"
+                            + "  map:\n"
+                            + "    engine*:\n"
+                            + "      map-store:\n"
+                            + "        enabled: true\n"
+                            + "        initial-mode: EAGER\n"
+                            + "        factory-class-name: 
org.apache.seatunnel.engine.server.persistence.FileMapStoreFactory\n"
+                            + "        properties:\n"
+                            + "          type: hdfs\n"
+                            + "          namespace: /tmp/seatunnel/imap\n"
+                            + "          clusterName: "
+                            + TestUtils.getClusterName(testClusterName)
+                            + "\n"
+                            + "          fs.defaultFS: file:///\n";
+            Config hazelcastConfig = Config.loadFromString(yaml);
+            
hazelcastConfig.setClusterName(TestUtils.getClusterName(testClusterName));
+            SeaTunnelConfig seaTunnelConfig = 
ConfigProvider.locateAndGetSeaTunnelConfig();
+            seaTunnelConfig.setHazelcastConfig(hazelcastConfig);
+            
seaTunnelConfig.getEngineConfig().getHttpConfig().setEnabled(false);
+            node = 
SeaTunnelServerStarter.createHazelcastInstance(seaTunnelConfig);
+
+            Common.setDeployMode(DeployMode.CLIENT);
+            ImmutablePair<String, String> testResources =
+                    createTestResources(testCaseName, JobMode.BATCH, 100, 1);
+            JobConfig jobConfig = new JobConfig();
+            jobConfig.setName(testCaseName);
+
+            ClientConfig clientConfig = 
ConfigProvider.locateAndGetClientConfig();
+            
clientConfig.setClusterName(TestUtils.getClusterName(testClusterName));
+            engineClient = new SeaTunnelClient(clientConfig);
+            ClientJobProxy clientJobProxy =
+                    engineClient
+                            .createExecutionContext(
+                                    testResources.getRight(), jobConfig, 
seaTunnelConfig)
+                            .execute();
+            Assertions.assertEquals(JobStatus.FINISHED, 
clientJobProxy.waitForJobComplete());
+            Long jobId = clientJobProxy.getJobId();
+
+            HazelcastInstanceImpl finalNode = node;
+            Awaitility.await()
+                    .atMost(60, TimeUnit.SECONDS)
+                    .untilAsserted(
+                            () ->
+                                    Assertions.assertNotNull(
+                                            finalNode
+                                                    
.getMap(Constant.IMAP_FINISHED_JOB_STATE)
+                                                    .getEntryView(jobId)));
+            long expirationTime =
+                    node.getMap(Constant.IMAP_FINISHED_JOB_STATE)
+                            .getEntryView(jobId)
+                            .getExpirationTime();
+            Assertions.assertNotEquals(Long.MAX_VALUE, expirationTime);
+
+            engineClient.close();
+            engineClient = null;
+            node.shutdown();
+            node = 
SeaTunnelServerStarter.createHazelcastInstance(seaTunnelConfig);
+
+            EntryView<Object, Object> restored =
+                    
node.getMap(Constant.IMAP_FINISHED_JOB_STATE).getEntryView(jobId);
+            Assertions.assertNotNull(restored);
+            // Hazelcast keeps record times at second granularity.
+            long drift = Math.abs(restored.getExpirationTime() - 
expirationTime);
+            Assertions.assertTrue(
+                    drift <= TimeUnit.SECONDS.toMillis(2),
+                    "expiration time moved by " + drift + " ms across the 
restart");
+        } finally {
+            if (engineClient != null) {
+                engineClient.close();
+            }
+
+            if (node != null) {
+                node.shutdown();
+            }
+        }
+    }
+
     @Test
     @Disabled
     public void testStreamJobRestoreFromOssInAllNodeDown() throws Exception {
diff --git 
a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/persistence/FileMapStore.java
 
b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/persistence/FileMapStore.java
index 6230ff80c4..16c20e9330 100644
--- 
a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/persistence/FileMapStore.java
+++ 
b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/persistence/FileMapStore.java
@@ -20,13 +20,15 @@ package org.apache.seatunnel.engine.server.persistence;
 import org.apache.seatunnel.shade.com.google.common.collect.Maps;
 
 import org.apache.seatunnel.engine.common.utils.FactoryUtil;
+import org.apache.seatunnel.engine.imap.storage.api.ExpirableValue;
 import org.apache.seatunnel.engine.imap.storage.api.IMapStorage;
 import org.apache.seatunnel.engine.imap.storage.api.IMapStorageFactory;
 import 
org.apache.seatunnel.engine.server.common.statestore.EngineStateStoreNames;
 
 import com.hazelcast.core.HazelcastInstance;
+import com.hazelcast.map.EntryLoader.MetadataAwareValue;
+import com.hazelcast.map.EntryStore;
 import com.hazelcast.map.MapLoaderLifecycleSupport;
-import com.hazelcast.map.MapStore;
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
 
@@ -36,8 +38,16 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.Properties;
 
+/**
+ * Persists engine IMaps to {@link IMapStorage}.
+ *
+ * <p>It is an {@link EntryStore} so that Hazelcast passes the expiration time 
of every stored
+ * entry. Hazelcast does not delete an entry from the map store when it 
expires, so the expiration
+ * time is stored with the entry and handed back on load; otherwise an entry 
put with a TTL would be
+ * loaded again after a restart and never expire.
+ */
 @Slf4j
-public class FileMapStore implements MapStore<Object, Object>, 
MapLoaderLifecycleSupport {
+public class FileMapStore implements EntryStore<Object, Object>, 
MapLoaderLifecycleSupport {
 
     private IMapStorage mapStorage;
 
@@ -78,13 +88,19 @@ public class FileMapStore implements MapStore<Object, 
Object>, MapLoaderLifecycl
     }
 
     @Override
-    public void store(Object key, Object value) {
-        mapStorage.store(key, value);
+    public void store(Object key, MetadataAwareValue<Object> value) {
+        mapStorage.store(key, value.getValue(), value.getExpirationTime());
     }
 
     @Override
-    public void storeAll(Map<Object, Object> map) {
-        mapStorage.storeAll(map);
+    public void storeAll(Map<Object, MetadataAwareValue<Object>> map) {
+        Map<Object, ExpirableValue> values = new HashMap<>(map.size());
+        map.forEach(
+                (key, value) ->
+                        values.put(
+                                key,
+                                new ExpirableValue(value.getValue(), 
value.getExpirationTime())));
+        mapStorage.storeAllWithExpirationTime(values);
     }
 
     @Override
@@ -99,16 +115,30 @@ public class FileMapStore implements MapStore<Object, 
Object>, MapLoaderLifecycl
 
     @SneakyThrows
     @Override
-    public Object load(Object key) {
+    public MetadataAwareValue<Object> load(Object key) {
         return null;
     }
 
+    /**
+     * Loads the requested keys with the expiration times they were stored 
with, so entries put with
+     * a TTL keep expiring after a restart. Keys that are missing or already 
expired are left out,
+     * and Hazelcast does not load them.
+     */
     @SneakyThrows
     @Override
-    public Map<Object, Object> loadAll(Collection<Object> keys) {
-        Map<Object, Object> allMap = mapStorage.loadAll();
-        Map<Object, Object> retMap = new HashMap<>();
-        keys.forEach(key -> retMap.put(key, allMap.get(key)));
+    public Map<Object, MetadataAwareValue<Object>> loadAll(Collection<Object> 
keys) {
+        Map<Object, ExpirableValue> allMap = 
mapStorage.loadAllWithExpirationTime();
+        Map<Object, MetadataAwareValue<Object>> retMap = new HashMap<>();
+        keys.forEach(
+                key -> {
+                    ExpirableValue value = allMap.get(key);
+                    if (value != null) {
+                        retMap.put(
+                                key,
+                                new MetadataAwareValue<>(
+                                        value.getValue(), 
value.getExpirationTime()));
+                    }
+                });
 
         return Collections.unmodifiableMap(retMap);
     }
diff --git 
a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/persistence/FileMapStoreFactory.java
 
b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/persistence/FileMapStoreFactory.java
index 2022e0fcc5..8ae25228ea 100644
--- 
a/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/persistence/FileMapStoreFactory.java
+++ 
b/seatunnel-engine/seatunnel-engine-server/src/main/java/org/apache/seatunnel/engine/server/persistence/FileMapStoreFactory.java
@@ -17,14 +17,16 @@
 
 package org.apache.seatunnel.engine.server.persistence;
 
+import com.hazelcast.map.EntryLoader.MetadataAwareValue;
 import com.hazelcast.map.MapLoader;
 import com.hazelcast.map.MapStoreFactory;
 
 import java.util.Properties;
 
-public class FileMapStoreFactory implements MapStoreFactory<Object, Object> {
+public class FileMapStoreFactory implements MapStoreFactory<Object, 
MetadataAwareValue<Object>> {
     @Override
-    public MapLoader<Object, Object> newMapStore(String mapName, Properties 
properties) {
+    public MapLoader<Object, MetadataAwareValue<Object>> newMapStore(
+            String mapName, Properties properties) {
         properties.setProperty("businessName", mapName);
         return new FileMapStore();
     }
diff --git 
a/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/persistence/FileMapStoreTest.java
 
b/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/persistence/FileMapStoreTest.java
index a1067c4f58..d6d2491fdb 100644
--- 
a/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/persistence/FileMapStoreTest.java
+++ 
b/seatunnel-engine/seatunnel-engine-server/src/test/java/org/apache/seatunnel/engine/server/persistence/FileMapStoreTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.seatunnel.engine.server.persistence;
 
+import org.apache.seatunnel.engine.common.Constant;
 import org.apache.seatunnel.engine.imap.storage.file.common.FileConstants;
 import 
org.apache.seatunnel.engine.server.common.statestore.EngineStateStoreNames;
 
@@ -26,12 +27,22 @@ import org.junit.jupiter.api.condition.EnabledOnOs;
 import org.junit.jupiter.api.condition.OS;
 import org.junit.jupiter.api.io.TempDir;
 
+import com.hazelcast.config.Config;
+import com.hazelcast.config.JoinConfig;
+import com.hazelcast.config.MapConfig;
+import com.hazelcast.config.MapStoreConfig;
+import com.hazelcast.core.EntryView;
+import com.hazelcast.core.Hazelcast;
 import com.hazelcast.core.HazelcastInstance;
+import com.hazelcast.map.EntryLoader.MetadataAwareValue;
+import com.hazelcast.map.IMap;
 
 import java.nio.file.Path;
 import java.util.Collections;
 import java.util.Properties;
+import java.util.concurrent.TimeUnit;
 
+import static org.awaitility.Awaitility.await;
 import static org.mockito.Mockito.mock;
 
 /**
@@ -116,12 +127,75 @@ public class FileMapStoreTest {
                                 props,
                                 EngineStateStoreNames.RUNNING_JOB_METRICS));
         Assertions.assertEquals(Collections.emptySet(), store.loadAllKeys());
-        Assertions.assertDoesNotThrow(() -> store.store("k", "v"));
+        Assertions.assertDoesNotThrow(() -> store.store("k", new 
MetadataAwareValue<>("v")));
         Assertions.assertDoesNotThrow(() -> store.delete("k"));
 
         store.destroy();
     }
 
+    /**
+     * Hazelcast does not delete an entry from the map store when its TTL 
expires. Verifies that
+     * after a restart an expired entry is not loaded again, and an entry put 
with a TTL keeps its
+     * expiration time instead of never expiring.
+     */
+    @Test
+    public void testEntriesKeepExpirationTimeAcrossRestart() {
+        String clusterName = "file-map-store-expiration-" + System.nanoTime();
+        String mapName = Constant.IMAP_FINISHED_JOB_STATE;
+        long expirationTimeBeforeRestart;
+
+        HazelcastInstance writer = 
Hazelcast.newHazelcastInstance(hazelcastConfig(clusterName));
+        try {
+            IMap<Long, String> map = writer.getMap(mapName);
+            map.put(1L, "expired", 1, TimeUnit.SECONDS);
+            map.put(2L, "expiring", 10, TimeUnit.MINUTES);
+            map.put(3L, "never-expiring");
+            expirationTimeBeforeRestart = 
map.getEntryView(2L).getExpirationTime();
+            await().atMost(10, TimeUnit.SECONDS).until(() -> 
!map.containsKey(1L));
+        } finally {
+            writer.shutdown();
+        }
+
+        HazelcastInstance restarted = 
Hazelcast.newHazelcastInstance(hazelcastConfig(clusterName));
+        try {
+            IMap<Long, String> map = restarted.getMap(mapName);
+
+            Assertions.assertNull(map.getEntryView(1L));
+            Assertions.assertEquals(2, map.size());
+
+            EntryView<Long, String> expiring = map.getEntryView(2L);
+            Assertions.assertEquals("expiring", expiring.getValue());
+            // Hazelcast keeps record times at second granularity.
+            long drift = Math.abs(expiring.getExpirationTime() - 
expirationTimeBeforeRestart);
+            Assertions.assertTrue(
+                    drift <= TimeUnit.SECONDS.toMillis(2),
+                    "expiration time moved by " + drift + " ms across the 
restart");
+
+            EntryView<Long, String> neverExpiring = map.getEntryView(3L);
+            Assertions.assertEquals("never-expiring", 
neverExpiring.getValue());
+            Assertions.assertEquals(Long.MAX_VALUE, 
neverExpiring.getExpirationTime());
+        } finally {
+            restarted.shutdown();
+        }
+    }
+
+    private Config hazelcastConfig(String clusterName) {
+        MapStoreConfig mapStoreConfig =
+                new MapStoreConfig()
+                        .setEnabled(true)
+                        
.setInitialLoadMode(MapStoreConfig.InitialLoadMode.EAGER)
+                        
.setFactoryClassName(FileMapStoreFactory.class.getName())
+                        
.setProperties(buildLocalFsProperties(tempDir.toString()));
+
+        Config config = new Config();
+        config.setClusterName(clusterName);
+        JoinConfig join = config.getNetworkConfig().getJoin();
+        join.getMulticastConfig().setEnabled(false);
+        join.getAutoDetectionConfig().setEnabled(false);
+        config.addMapConfig(new 
MapConfig("engine*").setMapStoreConfig(mapStoreConfig));
+        return config;
+    }
+
     private Properties buildLocalFsProperties(String namespace) {
         Properties props = new Properties();
         // "hdfs" is the factoryIdentifier of IMapFileStorageFactory — covers 
local fs via
diff --git 
a/seatunnel-engine/seatunnel-engine-storage/imap-storage-api/src/main/java/org/apache/seatunnel/engine/imap/storage/api/ExpirableValue.java
 
b/seatunnel-engine/seatunnel-engine-storage/imap-storage-api/src/main/java/org/apache/seatunnel/engine/imap/storage/api/ExpirableValue.java
new file mode 100644
index 0000000000..efe12174b9
--- /dev/null
+++ 
b/seatunnel-engine/seatunnel-engine-storage/imap-storage-api/src/main/java/org/apache/seatunnel/engine/imap/storage/api/ExpirableValue.java
@@ -0,0 +1,70 @@
+/*
+ * 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.api;
+
+import java.util.Objects;
+
+/** A stored value together with the time at which its entry expires. */
+public final class ExpirableValue {
+
+    /** Expiration time of an entry that never expires. */
+    public static final long NO_EXPIRATION_TIME = Long.MAX_VALUE;
+
+    private final Object value;
+
+    private final long expirationTime;
+
+    public ExpirableValue(Object value) {
+        this(value, NO_EXPIRATION_TIME);
+    }
+
+    /**
+     * @param value stored value
+     * @param expirationTime epoch milliseconds at which the entry expires, or 
{@link
+     *     #NO_EXPIRATION_TIME}
+     */
+    public ExpirableValue(Object value, long expirationTime) {
+        this.value = Objects.requireNonNull(value, "value");
+        this.expirationTime = expirationTime;
+    }
+
+    public Object getValue() {
+        return value;
+    }
+
+    public long getExpirationTime() {
+        return expirationTime;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof ExpirableValue)) {
+            return false;
+        }
+        ExpirableValue that = (ExpirableValue) o;
+        return expirationTime == that.expirationTime && Objects.equals(value, 
that.value);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(value, expirationTime);
+    }
+}
diff --git 
a/seatunnel-engine/seatunnel-engine-storage/imap-storage-api/src/main/java/org/apache/seatunnel/engine/imap/storage/api/IMapStorage.java
 
b/seatunnel-engine/seatunnel-engine-storage/imap-storage-api/src/main/java/org/apache/seatunnel/engine/imap/storage/api/IMapStorage.java
index f4c6a8f0bb..34c31425fc 100644
--- 
a/seatunnel-engine/seatunnel-engine-storage/imap-storage-api/src/main/java/org/apache/seatunnel/engine/imap/storage/api/IMapStorage.java
+++ 
b/seatunnel-engine/seatunnel-engine-storage/imap-storage-api/src/main/java/org/apache/seatunnel/engine/imap/storage/api/IMapStorage.java
@@ -22,6 +22,7 @@ package org.apache.seatunnel.engine.imap.storage.api;
 
 import java.io.IOException;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
 
@@ -47,6 +48,36 @@ public interface IMapStorage {
      */
     public Set<Object> storeAll(Map<Object, Object> map);
 
+    /**
+     * Store a key-value pair that expires at the given time.
+     *
+     * <p>Storages that do not persist expiration times store the pair without 
it, so the entry
+     * never expires after it is loaded again.
+     *
+     * @param key storage key
+     * @param value storage value
+     * @param expirationTime epoch milliseconds at which the entry expires, or 
{@link
+     *     ExpirableValue#NO_EXPIRATION_TIME}
+     * @return storage status, true is success, false is fail
+     */
+    default boolean store(Object key, Object value, long expirationTime) {
+        return store(key, value);
+    }
+
+    /**
+     * Store key-value pairs together with the times at which they expire.
+     *
+     * @param map storage key to value and expiration time
+     * @return if some key-value pair is not stored, return this keys; if all 
key-value pair is
+     *     stored, return empty set.
+     * @see #store(Object, Object, long)
+     */
+    default Set<Object> storeAllWithExpirationTime(Map<Object, ExpirableValue> 
map) {
+        Map<Object, Object> values = new HashMap<>(map.size());
+        map.forEach((key, value) -> values.put(key, value.getValue()));
+        return storeAll(values);
+    }
+
     /**
      * Delete a key in the map storage.
      *
@@ -66,6 +97,22 @@ public interface IMapStorage {
 
     public Map<Object, Object> loadAll() throws IOException;
 
+    /**
+     * Load all entries together with the times at which they expire. Entries 
that have already
+     * expired are not returned.
+     *
+     * <p>Storages that do not persist expiration times return every entry 
with {@link
+     * ExpirableValue#NO_EXPIRATION_TIME}.
+     *
+     * @return storage key to value and expiration time
+     */
+    default Map<Object, ExpirableValue> loadAllWithExpirationTime() throws 
IOException {
+        Map<Object, Object> values = loadAll();
+        Map<Object, ExpirableValue> result = new HashMap<>(values.size());
+        values.forEach((key, value) -> result.put(key, new 
ExpirableValue(value)));
+        return result;
+    }
+
     public Set<Object> loadAllKeys();
 
     public void destroy(boolean deleteAllFileFlag);
diff --git 
a/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/main/java/org/apache/seatunnel/engine/imap/storage/file/IMapFileStorage.java
 
b/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/main/java/org/apache/seatunnel/engine/imap/storage/file/IMapFileStorage.java
index ab125ba5c8..64b1692397 100644
--- 
a/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/main/java/org/apache/seatunnel/engine/imap/storage/file/IMapFileStorage.java
+++ 
b/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/main/java/org/apache/seatunnel/engine/imap/storage/file/IMapFileStorage.java
@@ -20,6 +20,7 @@
 
 package org.apache.seatunnel.engine.imap.storage.file;
 
+import org.apache.seatunnel.engine.imap.storage.api.ExpirableValue;
 import org.apache.seatunnel.engine.imap.storage.api.IMapStorage;
 import 
org.apache.seatunnel.engine.imap.storage.api.exception.IMapStorageException;
 import org.apache.seatunnel.engine.imap.storage.file.bean.IMapFileData;
@@ -169,9 +170,14 @@ public class IMapFileStorage implements IMapStorage {
 
     @Override
     public boolean store(Object key, Object value) {
+        return store(key, value, ExpirableValue.NO_EXPIRATION_TIME);
+    }
+
+    @Override
+    public boolean store(Object key, Object value, long expirationTime) {
         IMapFileData data;
         try {
-            data = parseToIMapFileData(key, value);
+            data = parseToIMapFileData(key, value, expirationTime);
         } catch (IOException e) {
             log.error("parse to IMapFileData error, key is {}, value is {}", 
key, value, e);
             return false;
@@ -183,12 +189,21 @@ public class IMapFileStorage implements IMapStorage {
 
     @Override
     public Set<Object> storeAll(Map<Object, Object> map) {
+        Map<Object, ExpirableValue> values = new HashMap<>(map.size());
+        map.forEach((key, value) -> values.put(key, new 
ExpirableValue(value)));
+        return storeAllWithExpirationTime(values);
+    }
+
+    @Override
+    public Set<Object> storeAllWithExpirationTime(Map<Object, ExpirableValue> 
map) {
         Map<Long, Object> requestMap = new HashMap<>(map.size());
         Set<Object> failures = new HashSet<>();
         map.forEach(
                 (key, value) -> {
                     try {
-                        IMapFileData data = parseToIMapFileData(key, value);
+                        IMapFileData data =
+                                parseToIMapFileData(
+                                        key, value.getValue(), 
value.getExpirationTime());
                         long requestId = sendToDisruptorQueue(data, 
WALEventType.APPEND);
                         requestMap.put(requestId, key);
                     } catch (IOException e) {
@@ -241,6 +256,17 @@ public class IMapFileStorage implements IMapStorage {
         }
     }
 
+    @Override
+    public Map<Object, ExpirableValue> loadAllWithExpirationTime() {
+        try {
+            WALReader reader = new WALReader(fs, fileConfiguration, 
serializer);
+            return reader.loadAllDataWithExpirationTime(
+                    new Path(businessRootPath), new HashSet<>());
+        } catch (IOException e) {
+            throw new IMapStorageException("load all data error", e);
+        }
+    }
+
     @Override
     public Set<Object> loadAllKeys() {
         try {
@@ -283,7 +309,8 @@ public class IMapFileStorage implements IMapStorage {
         }
     }
 
-    private IMapFileData parseToIMapFileData(Object key, Object value) throws 
IOException {
+    private IMapFileData parseToIMapFileData(Object key, Object value, long 
expirationTime)
+            throws IOException {
         return IMapFileData.builder()
                 .key(serializer.serialize(key))
                 .keyClassName(key.getClass().getName())
@@ -291,6 +318,9 @@ public class IMapFileStorage implements IMapStorage {
                 .valueClassName(value.getClass().getName())
                 .timestamp(System.currentTimeMillis())
                 .deleted(false)
+                // Leave the field unset for entries that never expire, as 
older releases did.
+                .expirationTime(
+                        expirationTime == ExpirableValue.NO_EXPIRATION_TIME ? 
null : expirationTime)
                 .build();
     }
 
diff --git 
a/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/main/java/org/apache/seatunnel/engine/imap/storage/file/bean/IMapFileData.java
 
b/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/main/java/org/apache/seatunnel/engine/imap/storage/file/bean/IMapFileData.java
index e8920f86fb..46e3164e19 100644
--- 
a/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/main/java/org/apache/seatunnel/engine/imap/storage/file/bean/IMapFileData.java
+++ 
b/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/main/java/org/apache/seatunnel/engine/imap/storage/file/bean/IMapFileData.java
@@ -44,6 +44,15 @@ public class IMapFileData implements Serializable, 
Comparable<IMapFileData> {
 
     private long timestamp;
 
+    /**
+     * Epoch milliseconds at which the entry expires, or {@code null} if it 
never expires.
+     *
+     * <p>ProtoStuff numbers fields by declaration order, so this field must 
stay last to keep WAL
+     * files written before it was added readable. Those records read it as 
{@code null}, and
+     * releases that do not know this field skip it.
+     */
+    private Long expirationTime;
+
     @Override
     public int compareTo(IMapFileData o) {
         return IMapDataComparator.compare(
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 f69d0cfb38..8be32aa388 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
@@ -22,6 +22,7 @@ package org.apache.seatunnel.engine.imap.storage.file.common;
 
 import org.apache.seatunnel.shade.org.apache.commons.lang3.ClassUtils;
 
+import org.apache.seatunnel.engine.imap.storage.api.ExpirableValue;
 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.config.FileConfiguration;
@@ -39,6 +40,7 @@ import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.BiFunction;
 
 import static 
org.apache.seatunnel.engine.imap.storage.file.common.LatestMutationAccumulator.SerializedKey;
 
@@ -85,40 +87,81 @@ public class WALReader {
         this.fileReader.initialize(fs, serializer);
     }
 
+    /** Returns the keys whose latest mutation is neither a delete nor 
expired. */
     public Set<Object> loadAllKeys(Path parentPath) throws IOException {
         LatestMutationAccumulator accumulator = 
readLatestMutations(parentPath, null);
+        long now = System.currentTimeMillis();
         Set<Object> result = new HashSet<>(accumulator.size());
         Iterator<IMapFileData> mutations = accumulator.iterator();
         while (mutations.hasNext()) {
             IMapFileData mutation = mutations.next();
             mutations.remove();
-            if (!mutation.isDeleted()) {
+            if (isLive(mutation, now)) {
                 result.add(deserializeData(mutation.getKey(), 
mutation.getKeyClassName()));
             }
         }
         return result;
     }
 
+    /** Returns the latest value of each key that is neither deleted nor 
expired. */
     public Map<Object, Object> loadAllData(Path parentPath, Set<Object> 
searchKeys)
             throws IOException {
+        return loadLiveData(parentPath, searchKeys, (value, mutation) -> 
value);
+    }
+
+    /**
+     * Returns the latest value of each key that is neither deleted nor 
expired, together with the
+     * time at which it expires.
+     */
+    public Map<Object, ExpirableValue> loadAllDataWithExpirationTime(
+            Path parentPath, Set<Object> searchKeys) throws IOException {
+        return loadLiveData(
+                parentPath,
+                searchKeys,
+                (value, mutation) ->
+                        new ExpirableValue(
+                                value,
+                                mutation.getExpirationTime() == null
+                                        ? ExpirableValue.NO_EXPIRATION_TIME
+                                        : mutation.getExpirationTime()));
+    }
+
+    private <V> Map<Object, V> loadLiveData(
+            Path parentPath,
+            Set<Object> searchKeys,
+            BiFunction<Object, IMapFileData, V> valueMapper)
+            throws IOException {
         Set<SerializedKey> serializedSearchKeys = 
serializeSearchKeys(searchKeys);
         LatestMutationAccumulator accumulator =
                 readLatestMutations(parentPath, serializedSearchKeys);
-        Map<Object, Object> result = new HashMap<>(accumulator.size());
+        long now = System.currentTimeMillis();
+        Map<Object, V> result = new HashMap<>(accumulator.size());
         Iterator<IMapFileData> mutations = accumulator.iterator();
         while (mutations.hasNext()) {
             IMapFileData mutation = mutations.next();
             mutations.remove();
-            if (mutation.isDeleted()) {
+            if (!isLive(mutation, now)) {
                 continue;
             }
             Object key = deserializeData(mutation.getKey(), 
mutation.getKeyClassName());
             Object value = deserializeData(mutation.getValue(), 
mutation.getValueClassName());
-            result.put(key, value);
+            result.put(key, valueMapper.apply(value, mutation));
         }
         return result;
     }
 
+    /**
+     * An entry stays in the WAL after it expires because Hazelcast does not 
delete expired entries
+     * from the map store, so its latest put is skipped here instead of being 
loaded again.
+     */
+    private static boolean isLive(IMapFileData mutation, long now) {
+        if (mutation.isDeleted()) {
+            return false;
+        }
+        Long expirationTime = mutation.getExpirationTime();
+        return expirationTime == null || expirationTime > now;
+    }
+
     private LatestMutationAccumulator readLatestMutations(
             Path parentPath, Set<SerializedKey> serializedSearchKeys) throws 
IOException {
         LatestMutationAccumulator accumulator = new 
LatestMutationAccumulator(serializedSearchKeys);
diff --git 
a/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/test/java/org/apache/seatunnel/engine/imap/storage/file/IMapFileStorageTest.java
 
b/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/test/java/org/apache/seatunnel/engine/imap/storage/file/IMapFileStorageTest.java
index a1aebb5e47..73409654b7 100644
--- 
a/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/test/java/org/apache/seatunnel/engine/imap/storage/file/IMapFileStorageTest.java
+++ 
b/seatunnel-engine/seatunnel-engine-storage/imap-storage-plugins/imap-storage-file/src/test/java/org/apache/seatunnel/engine/imap/storage/file/IMapFileStorageTest.java
@@ -20,6 +20,7 @@
 
 package org.apache.seatunnel.engine.imap.storage.file;
 
+import org.apache.seatunnel.engine.imap.storage.api.ExpirableValue;
 import org.apache.seatunnel.engine.imap.storage.file.common.FileConstants;
 
 import org.apache.hadoop.conf.Configuration;
@@ -33,6 +34,7 @@ import org.junit.jupiter.api.condition.EnabledOnOs;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -108,6 +110,29 @@ public class IMapFileStorageTest {
         STORAGE.deleteAll(keys);
     }
 
+    @Test
+    void testStoreWithExpirationTime() {
+        long expirationTime = System.currentTimeMillis() + 
TimeUnit.MINUTES.toMillis(10);
+        STORAGE.store("expiring", 1L, expirationTime);
+        STORAGE.store("expired", 2L, System.currentTimeMillis() - 1);
+        Map<Object, ExpirableValue> batch = new HashMap<>();
+        batch.put("batch-expiring", new ExpirableValue(3L, expirationTime));
+        batch.put("batch-never-expiring", new ExpirableValue(4L));
+        
Assertions.assertTrue(STORAGE.storeAllWithExpirationTime(batch).isEmpty());
+
+        Map<Object, ExpirableValue> loaded = 
STORAGE.loadAllWithExpirationTime();
+        Assertions.assertEquals(new ExpirableValue(1L, expirationTime), 
loaded.get("expiring"));
+        Assertions.assertEquals(
+                new ExpirableValue(3L, expirationTime), 
loaded.get("batch-expiring"));
+        Assertions.assertEquals(new ExpirableValue(4L), 
loaded.get("batch-never-expiring"));
+        Assertions.assertFalse(loaded.containsKey("expired"));
+        Assertions.assertFalse(STORAGE.loadAll().containsKey("expired"));
+        Assertions.assertFalse(STORAGE.loadAllKeys().contains("expired"));
+
+        STORAGE.deleteAll(
+                Arrays.asList("expiring", "expired", "batch-expiring", 
"batch-never-expiring"));
+    }
+
     @Test
     void testStoreArray() {
         Long[] data = new Long[10];
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 2eeb243669..d4c187f181 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
@@ -23,6 +23,7 @@ package org.apache.seatunnel.engine.imap.storage.file.common;
 import org.apache.seatunnel.engine.common.job.JobResult;
 import org.apache.seatunnel.engine.common.job.JobStatus;
 import org.apache.seatunnel.engine.common.job.JobStatusData;
+import org.apache.seatunnel.engine.imap.storage.api.ExpirableValue;
 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.config.FileConfiguration;
@@ -44,9 +45,11 @@ import org.junit.jupiter.api.condition.EnabledOnOs;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 import static org.awaitility.Awaitility.await;
 import static org.junit.jupiter.api.condition.OS.LINUX;
@@ -194,6 +197,95 @@ public class WALReaderAndWriterTest {
                 reader.loadAllData(path, Collections.emptySet()));
     }
 
+    @Test
+    void shouldSkipExpiredEntriesAndReturnExpirationTimes() throws IOException 
{
+        Path path = new Path(PARENT_PATH, "expiration");
+        long now = System.currentTimeMillis();
+        long liveExpirationTime = now + TimeUnit.MINUTES.toMillis(10);
+        IMapFileData expired = mutation("expired", "value-a", 10, false);
+        expired.setExpirationTime(now - 1);
+        IMapFileData live = mutation("live", "value-b", 10, false);
+        live.setExpirationTime(liveExpirationTime);
+        // Records written before the expiration time was stored have none.
+        IMapFileData neverExpiring = mutation("never-expiring", "value-c", 10, 
false);
+        writeWalFile(new Path(path, "region/expiration-wal.txt"), expired, 
live, neverExpiring);
+
+        WALReader reader = new WALReader(FS, FileConfiguration.HDFS, 
SERIALIZER);
+
+        Assertions.assertEquals(
+                new HashSet<>(Arrays.asList("live", "never-expiring")), 
reader.loadAllKeys(path));
+        Map<Object, Object> expectedData = new HashMap<>();
+        expectedData.put("live", "value-b");
+        expectedData.put("never-expiring", "value-c");
+        Assertions.assertEquals(expectedData, reader.loadAllData(path, 
Collections.emptySet()));
+        Map<Object, ExpirableValue> expectedDataWithExpirationTime = new 
HashMap<>();
+        expectedDataWithExpirationTime.put(
+                "live", new ExpirableValue("value-b", liveExpirationTime));
+        expectedDataWithExpirationTime.put("never-expiring", new 
ExpirableValue("value-c"));
+        Assertions.assertEquals(
+                expectedDataWithExpirationTime,
+                reader.loadAllDataWithExpirationTime(path, 
Collections.emptySet()));
+    }
+
+    @Test
+    void shouldNotFallBackToOlderValueWhenLatestValueExpired() throws 
IOException {
+        Path path = new Path(PARENT_PATH, "latest-expired");
+        IMapFileData expired = mutation("key", "new", 20, false);
+        expired.setExpirationTime(System.currentTimeMillis() - 1);
+        writeWalFile(
+                new Path(path, "region/latest-expired-wal.txt"),
+                mutation("key", "old", 10, false),
+                expired);
+
+        WALReader reader = new WALReader(FS, FileConfiguration.HDFS, 
SERIALIZER);
+
+        Assertions.assertEquals(
+                Collections.emptyMap(), reader.loadAllData(path, 
Collections.emptySet()));
+        Assertions.assertEquals(Collections.emptySet(), 
reader.loadAllKeys(path));
+    }
+
+    @Test
+    void shouldLoadRecordsWrittenBeforeExpirationTimeWasStored() throws 
IOException {
+        Path path = new Path(PARENT_PATH, "pre-expiration-time");
+        Path file = new Path(path, "region/pre-expiration-time-wal.txt");
+        FS.mkdirs(file.getParent());
+        try (FSDataOutputStream output = FS.create(file, true)) {
+            output.write(
+                    WALDataUtils.wrapperBytes(
+                            
SERIALIZER.serialize(preExpirationTimeRecord("key", "value", 10))));
+        }
+
+        WALReader reader = new WALReader(FS, FileConfiguration.HDFS, 
SERIALIZER);
+
+        Assertions.assertEquals(
+                Collections.singletonMap("key", new ExpirableValue("value")),
+                reader.loadAllDataWithExpirationTime(path, 
Collections.emptySet()));
+    }
+
+    @Test
+    void shouldWriteRecordsWithoutExpirationTimeInThePreviousFormat() throws 
IOException {
+        Assertions.assertArrayEquals(
+                SERIALIZER.serialize(preExpirationTimeRecord("key", "value", 
10)),
+                SERIALIZER.serialize(mutation("key", "value", 10, false)));
+    }
+
+    @Test
+    void shouldKeepRecordsWithExpirationTimeReadableByPreviousReleases() 
throws IOException {
+        IMapFileData record = mutation("key", "value", 10, false);
+        record.setExpirationTime(1_000L);
+
+        PreExpirationTimeIMapFileData read =
+                SERIALIZER.deserialize(
+                        SERIALIZER.serialize(record), 
PreExpirationTimeIMapFileData.class);
+
+        Assertions.assertFalse(read.deleted);
+        Assertions.assertArrayEquals(record.getKey(), read.key);
+        Assertions.assertEquals(record.getKeyClassName(), read.keyClassName);
+        Assertions.assertArrayEquals(record.getValue(), read.value);
+        Assertions.assertEquals(record.getValueClassName(), 
read.valueClassName);
+        Assertions.assertEquals(record.getTimestamp(), read.timestamp);
+    }
+
     private static IMapFileData mutation(String key, String value, long 
timestamp, boolean deleted)
             throws IOException {
         return IMapFileData.builder()
@@ -219,6 +311,31 @@ public class WALReaderAndWriterTest {
         return WALDataUtils.wrapperBytes(SERIALIZER.serialize(mutation));
     }
 
+    private static PreExpirationTimeIMapFileData preExpirationTimeRecord(
+            String key, String value, long timestamp) throws IOException {
+        PreExpirationTimeIMapFileData record = new 
PreExpirationTimeIMapFileData();
+        record.key = SERIALIZER.serialize(key);
+        record.keyClassName = String.class.getName();
+        record.value = SERIALIZER.serialize(value);
+        record.valueClassName = String.class.getName();
+        record.timestamp = timestamp;
+        return record;
+    }
+
+    /**
+     * Field layout of {@link IMapFileData} before {@code expirationTime} was 
added. ProtoStuff
+     * numbers fields by declaration order, so this class reads and writes the 
same bytes as earlier
+     * releases did.
+     */
+    public static class PreExpirationTimeIMapFileData {
+        private boolean deleted;
+        private byte[] key;
+        private String keyClassName;
+        private byte[] value;
+        private String valueClassName;
+        private long timestamp;
+    }
+
     @Test
     public void testReplayKeepsTombstoneForSameKeyAndTimestamp() throws 
Exception {
         String key = "deleted-key";

Reply via email to