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

szetszwo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ratis.git


The following commit(s) were added to refs/heads/master by this push:
     new 96dc9980e RATIS-244. Skip snapshot file if corresponding MD5 file is 
missing (#1320)
96dc9980e is described below

commit 96dc9980e8bec02492272482117342fd68efbca1
Author: Abhishek Pal <[email protected]>
AuthorDate: Sat Mar 28 09:54:20 2026 +0530

    RATIS-244. Skip snapshot file if corresponding MD5 file is missing (#1320)
---
 .../impl/SimpleStateMachineStorage.java            |  76 +++---
 .../statemachine/impl/SingleFileSnapshotInfo.java  |   5 +
 .../ratis/server/storage/TestRaftStorage.java      | 303 ++++++++++++++++++++-
 3 files changed, 340 insertions(+), 44 deletions(-)

diff --git 
a/ratis-server/src/main/java/org/apache/ratis/statemachine/impl/SimpleStateMachineStorage.java
 
b/ratis-server/src/main/java/org/apache/ratis/statemachine/impl/SimpleStateMachineStorage.java
index 0ca6734a0..532277899 100644
--- 
a/ratis-server/src/main/java/org/apache/ratis/statemachine/impl/SimpleStateMachineStorage.java
+++ 
b/ratis-server/src/main/java/org/apache/ratis/statemachine/impl/SimpleStateMachineStorage.java
@@ -39,7 +39,6 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Comparator;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
@@ -82,6 +81,9 @@ public class SimpleStateMachineStorage implements 
StateMachineStorage {
     // TODO
   }
 
+  /**
+   * Fetch all the snapshot files irrespective of whether they have an MD5 
file or not
+   */
   static List<SingleFileSnapshotInfo> getSingleFileSnapshotInfos(Path dir) 
throws IOException {
     final List<SingleFileSnapshotInfo> infos = new ArrayList<>();
     try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
@@ -92,7 +94,8 @@ public class SimpleStateMachineStorage implements 
StateMachineStorage {
           if (matcher.matches()) {
             final long term = Long.parseLong(matcher.group(1));
             final long index = Long.parseLong(matcher.group(2));
-            final FileInfo fileInfo = new FileInfo(path, null); //No 
FileDigest here.
+            final MD5Hash md5 = 
MD5FileUtil.readStoredMd5ForFile(path.toFile());
+            final FileInfo fileInfo = new FileInfo(path, md5);
             infos.add(new SingleFileSnapshotInfo(fileInfo, term, index));
           }
         }
@@ -115,10 +118,25 @@ public class SimpleStateMachineStorage implements 
StateMachineStorage {
     }
 
     final List<SingleFileSnapshotInfo> allSnapshotFiles = 
getSingleFileSnapshotInfos(stateMachineDir.toPath());
+    
allSnapshotFiles.sort(Comparator.comparing(SingleFileSnapshotInfo::getIndex).reversed());
+    int numSnapshotsWithMd5 = 0;
+    int deleteIdx = -1;
 
-    if (allSnapshotFiles.size() > numSnapshotsRetained) {
-      
allSnapshotFiles.sort(Comparator.comparing(SingleFileSnapshotInfo::getIndex).reversed());
-      allSnapshotFiles.subList(numSnapshotsRetained, allSnapshotFiles.size())
+    for (int i = 0; i < allSnapshotFiles.size(); i++) {
+      final SingleFileSnapshotInfo snapshot = allSnapshotFiles.get(i);
+      if (snapshot.hasMd5()) {
+        if (++numSnapshotsWithMd5 == numSnapshotsRetained) {
+          // We have found the last snapshot with an MD5 file that needs to be 
retained
+          deleteIdx = i + 1;
+          break;
+        }
+      } else {
+        LOG.warn("Snapshot file {} has missing MD5 file.", snapshot);
+      }
+    }
+
+    if (deleteIdx > 0) {
+      allSnapshotFiles.subList(deleteIdx, allSnapshotFiles.size())
           .stream()
           .map(SingleFileSnapshotInfo::getFile)
           .map(FileInfo::getPath)
@@ -126,20 +144,21 @@ public class SimpleStateMachineStorage implements 
StateMachineStorage {
             LOG.info("Deleting old snapshot at {}", 
snapshotPath.toAbsolutePath());
             FileUtils.deletePathQuietly(snapshotPath);
           });
-      // clean up the md5 files if the corresponding snapshot file does not 
exist
-      try (DirectoryStream<Path> stream = 
Files.newDirectoryStream(stateMachineDir.toPath(),
-          SNAPSHOT_MD5_FILTER)) {
-        for (Path md5path : stream) {
-          Path md5FileNamePath = md5path.getFileName();
-          if (md5FileNamePath == null) {
-            continue;
-          }
-          final String md5FileName = md5FileNamePath.toString();
-          final File snapshotFile = new File(stateMachineDir,
-              md5FileName.substring(0, md5FileName.length() - 
MD5_SUFFIX.length()));
-          if (!snapshotFile.exists()) {
-            FileUtils.deletePathQuietly(md5path);
-          }
+    }
+
+    // clean up the md5 files if the corresponding snapshot file does not exist
+    try (DirectoryStream<Path> stream = 
Files.newDirectoryStream(stateMachineDir.toPath(),
+      SNAPSHOT_MD5_FILTER)) {
+      for (Path md5path : stream) {
+        Path md5FileNamePath = md5path.getFileName();
+        if (md5FileNamePath == null) {
+          continue;
+        }
+        final String md5FileName = md5FileNamePath.toString();
+        final File snapshotFile = new File(stateMachineDir,
+            md5FileName.substring(0, md5FileName.length() - 
MD5_SUFFIX.length()));
+        if (!snapshotFile.exists()) {
+          FileUtils.deletePathQuietly(md5path);
         }
       }
     }
@@ -182,24 +201,19 @@ public class SimpleStateMachineStorage implements 
StateMachineStorage {
   }
 
   static SingleFileSnapshotInfo findLatestSnapshot(Path dir) throws 
IOException {
-    final Iterator<SingleFileSnapshotInfo> i = 
getSingleFileSnapshotInfos(dir).iterator();
-    if (!i.hasNext()) {
+    final List<SingleFileSnapshotInfo> infos = getSingleFileSnapshotInfos(dir);
+    if (infos.isEmpty()) {
       return null;
     }
+    
infos.sort(Comparator.comparing(SingleFileSnapshotInfo::getIndex).reversed());
 
-    SingleFileSnapshotInfo latest = i.next();
-    for(; i.hasNext(); ) {
-      final SingleFileSnapshotInfo info = i.next();
-      if (info.getIndex() > latest.getIndex()) {
-        latest = info;
+    for (SingleFileSnapshotInfo latest : infos) {
+      if (latest.hasMd5()) {
+        return latest;
       }
     }
 
-    // read md5
-    final Path path = latest.getFile().getPath();
-    final MD5Hash md5 = MD5FileUtil.readStoredMd5ForFile(path.toFile());
-    final FileInfo info = new FileInfo(path, md5);
-    return new SingleFileSnapshotInfo(info, latest.getTerm(), 
latest.getIndex());
+    return infos.get(0); // all snapshots do not have MD5
   }
 
   public SingleFileSnapshotInfo updateLatestSnapshot(SingleFileSnapshotInfo 
info) {
diff --git 
a/ratis-server/src/main/java/org/apache/ratis/statemachine/impl/SingleFileSnapshotInfo.java
 
b/ratis-server/src/main/java/org/apache/ratis/statemachine/impl/SingleFileSnapshotInfo.java
index 14d501a4a..5ecc59a10 100644
--- 
a/ratis-server/src/main/java/org/apache/ratis/statemachine/impl/SingleFileSnapshotInfo.java
+++ 
b/ratis-server/src/main/java/org/apache/ratis/statemachine/impl/SingleFileSnapshotInfo.java
@@ -36,6 +36,11 @@ public class SingleFileSnapshotInfo extends 
FileListSnapshotInfo {
     this(fileInfo, TermIndex.valueOf(term, endIndex));
   }
 
+  /** @return true iff the MD5 exists. */
+  public boolean hasMd5() {
+    return getFile().getFileDigest() != null;
+  }
+
   /** @return the file associated with the snapshot. */
   public FileInfo getFile() {
     return getFiles().get(0);
diff --git 
a/ratis-test/src/test/java/org/apache/ratis/server/storage/TestRaftStorage.java 
b/ratis-test/src/test/java/org/apache/ratis/server/storage/TestRaftStorage.java
index 12cd77131..093e9add7 100644
--- 
a/ratis-test/src/test/java/org/apache/ratis/server/storage/TestRaftStorage.java
+++ 
b/ratis-test/src/test/java/org/apache/ratis/server/storage/TestRaftStorage.java
@@ -18,6 +18,7 @@
 package org.apache.ratis.server.storage;
 
 import static java.util.stream.Collectors.toList;
+import static 
org.apache.ratis.statemachine.impl.SimpleStateMachineStorage.SNAPSHOT_MD5_REGEX;
 import static 
org.apache.ratis.statemachine.impl.SimpleStateMachineStorage.SNAPSHOT_REGEX;
 import static org.apache.ratis.util.MD5FileUtil.MD5_SUFFIX;
 
@@ -29,7 +30,9 @@ import org.apache.ratis.server.protocol.TermIndex;
 import org.apache.ratis.server.storage.RaftStorageDirectoryImpl.StorageState;
 import org.apache.ratis.statemachine.impl.SimpleStateMachineStorage;
 import org.apache.ratis.statemachine.SnapshotRetentionPolicy;
+import org.apache.ratis.statemachine.impl.SingleFileSnapshotInfo;
 import org.apache.ratis.util.FileUtils;
+import org.apache.ratis.util.MD5FileUtil;
 import org.apache.ratis.util.Preconditions;
 import org.apache.ratis.util.SizeInBytes;
 import org.junit.jupiter.api.AfterEach;
@@ -40,10 +43,13 @@ import org.mockito.Mockito;
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.atomic.AtomicReference;
@@ -228,7 +234,7 @@ public class TestRaftStorage extends BaseTest {
     SnapshotRetentionPolicy snapshotRetentionPolicy = new 
SnapshotRetentionPolicy() {
       @Override
       public int getNumSnapshotsRetained() {
-        return 3;
+        return 2;
       }
     };
 
@@ -239,15 +245,24 @@ public class TestRaftStorage extends BaseTest {
 
     Set<TermIndex> termIndexSet = new HashSet<>();
 
-    //Create 5 snapshot files in storage dir.
-    while (termIndexSet.size() < 5) {
+    //Create 3 snapshot files in storage dir.
+    while (termIndexSet.size() < 3) {
       final long term = ThreadLocalRandom.current().nextLong(1, 10L);
-      final long index = ThreadLocalRandom.current().nextLong(100, 1000L);
+      final long index = ThreadLocalRandom.current().nextLong(100, 500L);
       if (termIndexSet.add(TermIndex.valueOf(term, index))) {
-        File file = simpleStateMachineStorage.getSnapshotFile(term, index);
-        Assertions.assertTrue(file.createNewFile());
+        createSnapshot(simpleStateMachineStorage, term, index, true);
+      }
+    }
+
+    // Create 2 more snapshot files in storage dir without MD5 files
+    while (termIndexSet.size() < 5) {
+      final long term = ThreadLocalRandom.current().nextLong(11, 20L);
+      final long index = ThreadLocalRandom.current().nextLong(501, 1000L);
+      if (termIndexSet.add(TermIndex.valueOf(term, index))) {
+        createSnapshot(simpleStateMachineStorage, term, index, false);
       }
     }
+
     // create MD5 files that will not be deleted in older version
     while (termIndexSet.size() < 7) {
       final long term = 1;
@@ -260,16 +275,18 @@ public class TestRaftStorage extends BaseTest {
     }
 
     File stateMachineDir = storage.getStorageDir().getStateMachineDir();
-    assertFileCount(stateMachineDir, 7);
+    assertFileCount(stateMachineDir, 10);
 
     simpleStateMachineStorage.cleanupOldSnapshots(snapshotRetentionPolicy);
 
-    File[] remainingFiles = assertFileCount(stateMachineDir, 3);
+    // Since the MD5 files are not matching the snapshot files they are 
cleaned up.
+    // So we still have 6 files - 4 snapshots and 2 MD5 files.
+    File[] remainingFiles = assertFileCount(stateMachineDir, 6);
 
     List<Long> remainingIndices = termIndexSet.stream()
         .map(TermIndex::getIndex)
         .sorted(Collections.reverseOrder())
-        .limit(3)
+        .limit(4)
         .collect(toList());
     for (File file : remainingFiles) {
       System.out.println(file.getName());
@@ -281,21 +298,262 @@ public class TestRaftStorage extends BaseTest {
 
     // Attempt to clean up again should not delete any more files.
     simpleStateMachineStorage.cleanupOldSnapshots(snapshotRetentionPolicy);
-    assertFileCount(stateMachineDir, 3);
+    assertFileCount(stateMachineDir, 6);
 
     //Test with Retention disabled.
     //Create 2 snapshot files in storage dir.
     for (int i = 0; i < 2; i++) {
-      final long term = ThreadLocalRandom.current().nextLong(1, 10L);
+      final long term = ThreadLocalRandom.current().nextLong(21, 30L);
       final long index = ThreadLocalRandom.current().nextLong(1000L);
-      File file = simpleStateMachineStorage.getSnapshotFile(term, index);
-      Assertions.assertTrue(file.createNewFile());
+      createSnapshot(simpleStateMachineStorage, term, index, false);
     }
 
     simpleStateMachineStorage.cleanupOldSnapshots(new 
SnapshotRetentionPolicy() { });
+    assertFileCount(stateMachineDir, 8);
+  }
+
+  @Test
+  public void testSnapshotCleanupWithMissingMd5File() throws IOException {
+
+    SnapshotRetentionPolicy snapshotRetentionPolicy = new 
SnapshotRetentionPolicy() {
+      @Override
+      public int getNumSnapshotsRetained() {
+        return 2;
+      }
+    };
+
+
+    SimpleStateMachineStorage simpleStateMachineStorage = new 
SimpleStateMachineStorage();
+    final RaftStorage storage = newRaftStorage(storageDir);
+    simpleStateMachineStorage.init(storage);
+
+    Set<TermIndex> termIndexSet = new HashSet<>();
+
+    // Create one snapshot file without MD5 file
+    if (termIndexSet.add(TermIndex.valueOf(1, 100))) {
+      createSnapshot(simpleStateMachineStorage, 1, 100, false);
+    }
+
+    //Create 4 snapshot files in storage dir
+    while (termIndexSet.size() < 5) {
+      final long term = ThreadLocalRandom.current().nextLong(2, 10L);
+      final long index = ThreadLocalRandom.current().nextLong(100, 1000L);
+      if (termIndexSet.add(TermIndex.valueOf(term, index))) {
+        createSnapshot(simpleStateMachineStorage, term, index, true);
+      }
+    }
+
+    // 1 snapshot file without MD5 hash, 4 snapshots + 4 md5 hash files = 9 
files
+    File stateMachineDir = storage.getStorageDir().getStateMachineDir();
+    assertFileCount(stateMachineDir, 9);
+
+    simpleStateMachineStorage.cleanupOldSnapshots(snapshotRetentionPolicy);
+
+    // We should have 4 files remaining, and 2 snapshots with MD5 hash
+    assertFileCount(stateMachineDir, 4);
+  }
+
+  @Test
+  public void testSnapshotCleanupWithLatestSnapshotMissingMd5File() throws 
IOException {
+
+    SnapshotRetentionPolicy snapshotRetentionPolicy = new 
SnapshotRetentionPolicy() {
+      @Override
+      public int getNumSnapshotsRetained() {
+        return 2;
+      }
+    };
+
+
+    SimpleStateMachineStorage simpleStateMachineStorage = new 
SimpleStateMachineStorage();
+    final RaftStorage storage = newRaftStorage(storageDir);
+    simpleStateMachineStorage.init(storage);
+
+    Set<TermIndex> termIndexSet = new HashSet<>();
+
+    //Create 4 snapshot files in storage dir
+    while (termIndexSet.size() < 4) {
+      final long term = ThreadLocalRandom.current().nextLong(1, 10L);
+      final long index = ThreadLocalRandom.current().nextLong(100, 1000L);
+      if (termIndexSet.add(TermIndex.valueOf(term, index))) {
+        createSnapshot(simpleStateMachineStorage, term, index, true);
+      }
+    }
+
+    // Create a snapshot file with a missing MD5 file and having the highest 
term index
+    if (termIndexSet.add(TermIndex.valueOf(99, 1001))) {
+      createSnapshot(simpleStateMachineStorage, 99, 1001, false);
+    }
+
+    // 1 snapshot file without MD5 hash, 4 snapshots + 4 md5 hash files = 9 
files
+    File stateMachineDir = storage.getStorageDir().getStateMachineDir();
+    assertFileCount(stateMachineDir, 9);
+
+    simpleStateMachineStorage.cleanupOldSnapshots(snapshotRetentionPolicy);
+
+    // We should have 5 files remaining, and 2 snapshots with MD5 hash and 1 
snapshot file without MD5 hash
     assertFileCount(stateMachineDir, 5);
   }
 
+  @Test
+  public void testCleanupOldSnapshotsDeletesOlderSnapshotsWithMd5() throws 
Exception {
+    SnapshotRetentionPolicy snapshotRetentionPolicy = new 
SnapshotRetentionPolicy() {
+      @Override
+      public int getNumSnapshotsRetained() {
+        return 2;
+      }
+    };
+
+    SimpleStateMachineStorage simpleStateMachineStorage = new 
SimpleStateMachineStorage();
+    final RaftStorage storage = newRaftStorage(storageDir);
+    simpleStateMachineStorage.init(storage);
+    try {
+      createSnapshot(simpleStateMachineStorage, 1, 100, true);
+      createSnapshot(simpleStateMachineStorage, 1, 200, true);
+      createSnapshot(simpleStateMachineStorage, 1, 300, true);
+      createSnapshot(simpleStateMachineStorage, 1, 400, true);
+
+      File stateMachineDir = storage.getStorageDir().getStateMachineDir();
+      simpleStateMachineStorage.cleanupOldSnapshots(snapshotRetentionPolicy);
+
+      List<String> snapshotNames = listMatchingFileNames(stateMachineDir, 
SNAPSHOT_REGEX);
+      Assertions.assertEquals(2, snapshotNames.size());
+      
Assertions.assertTrue(snapshotNames.contains(SimpleStateMachineStorage.getSnapshotFileName(1,
 400)));
+      
Assertions.assertTrue(snapshotNames.contains(SimpleStateMachineStorage.getSnapshotFileName(1,
 300)));
+      
Assertions.assertFalse(snapshotNames.contains(SimpleStateMachineStorage.getSnapshotFileName(1,
 200)));
+      
Assertions.assertFalse(snapshotNames.contains(SimpleStateMachineStorage.getSnapshotFileName(1,
 100)));
+
+      List<String> md5Names = listMatchingFileNames(stateMachineDir, 
SNAPSHOT_MD5_REGEX);
+      Assertions.assertEquals(2, md5Names.size());
+      
Assertions.assertTrue(md5Names.contains(SimpleStateMachineStorage.getSnapshotFileName(1,
 400) + MD5_SUFFIX));
+      
Assertions.assertTrue(md5Names.contains(SimpleStateMachineStorage.getSnapshotFileName(1,
 300) + MD5_SUFFIX));
+      
Assertions.assertFalse(md5Names.contains(SimpleStateMachineStorage.getSnapshotFileName(1,
 200) + MD5_SUFFIX));
+      
Assertions.assertFalse(md5Names.contains(SimpleStateMachineStorage.getSnapshotFileName(1,
 100) + MD5_SUFFIX));
+    } finally {
+      storage.close();
+    }
+  }
+
+  @Test
+  public void testCleanupOldSnapshotsWithoutAnyMd5() throws Exception {
+    SnapshotRetentionPolicy snapshotRetentionPolicy = new 
SnapshotRetentionPolicy() {
+      @Override
+      public int getNumSnapshotsRetained() {
+        return 2;
+      }
+    };
+
+    SimpleStateMachineStorage simpleStateMachineStorage = new 
SimpleStateMachineStorage();
+    final RaftStorage storage = newRaftStorage(storageDir);
+    simpleStateMachineStorage.init(storage);
+    try {
+      createSnapshot(simpleStateMachineStorage, 1, 100, false);
+      createSnapshot(simpleStateMachineStorage, 1, 200, false);
+      createSnapshot(simpleStateMachineStorage, 1, 300, false);
+
+      File stateMachineDir = storage.getStorageDir().getStateMachineDir();
+      simpleStateMachineStorage.cleanupOldSnapshots(snapshotRetentionPolicy);
+
+      List<String> snapshotNames = listMatchingFileNames(stateMachineDir, 
SNAPSHOT_REGEX);
+      Assertions.assertEquals(3, snapshotNames.size());
+      Assertions.assertTrue(listMatchingFileNames(stateMachineDir, 
SNAPSHOT_MD5_REGEX).isEmpty());
+    } finally {
+      storage.close();
+    }
+  }
+
+  @Test
+  public void testGetLatestSnapshotReturnsNewest() throws Exception {
+    SimpleStateMachineStorage simpleStateMachineStorage = new 
SimpleStateMachineStorage();
+    final RaftStorage storage = newRaftStorage(storageDir);
+    simpleStateMachineStorage.init(storage);
+    try {
+      Assertions.assertNull(simpleStateMachineStorage.getLatestSnapshot());
+
+      createSnapshot(simpleStateMachineStorage, 1, 100, true);
+      simpleStateMachineStorage.loadLatestSnapshot();
+      SingleFileSnapshotInfo first = 
simpleStateMachineStorage.getLatestSnapshot();
+      Assertions.assertNotNull(first);
+      Assertions.assertEquals(1, first.getTerm());
+      Assertions.assertEquals(100, first.getIndex());
+      Assertions.assertNotNull(first.getFile().getFileDigest());
+
+      createSnapshot(simpleStateMachineStorage, 1, 200, true);
+      simpleStateMachineStorage.loadLatestSnapshot();
+      SingleFileSnapshotInfo second = 
simpleStateMachineStorage.getLatestSnapshot();
+      Assertions.assertNotNull(second);
+      Assertions.assertEquals(1, second.getTerm());
+      Assertions.assertEquals(200, second.getIndex());
+      Assertions.assertNotNull(second.getFile().getFileDigest());
+    } finally {
+      storage.close();
+    }
+  }
+
+  @Test
+  public void testGetLatestSnapshotIgnoresSnapshotsWithoutMd5() throws 
Exception {
+    SimpleStateMachineStorage simpleStateMachineStorage = new 
SimpleStateMachineStorage();
+    final RaftStorage storage = newRaftStorage(storageDir);
+    simpleStateMachineStorage.init(storage);
+    try {
+      createSnapshot(simpleStateMachineStorage, 1, 100, true);
+      simpleStateMachineStorage.loadLatestSnapshot();
+
+      createSnapshot(simpleStateMachineStorage, 1, 200, false);
+      simpleStateMachineStorage.loadLatestSnapshot();
+
+      SingleFileSnapshotInfo latest = 
simpleStateMachineStorage.getLatestSnapshot();
+      Assertions.assertNotNull(latest);
+      Assertions.assertEquals(100, latest.getIndex());
+      Assertions.assertEquals(1, latest.getTerm());
+    } finally {
+      storage.close();
+    }
+  }
+
+  @Test
+  public void testGetLatestSnapshotFallsBackToSnapshotWithoutMd5() throws 
Exception {
+    SimpleStateMachineStorage simpleStateMachineStorage = new 
SimpleStateMachineStorage();
+    final RaftStorage storage = newRaftStorage(storageDir);
+    simpleStateMachineStorage.init(storage);
+    try {
+      createSnapshot(simpleStateMachineStorage, 1, 100, false);
+      simpleStateMachineStorage.loadLatestSnapshot();
+
+      SingleFileSnapshotInfo latest = 
simpleStateMachineStorage.getLatestSnapshot();
+      Assertions.assertNotNull(latest);
+      Assertions.assertEquals(100, latest.getIndex());
+      Assertions.assertEquals(1, latest.getTerm());
+      Assertions.assertNull(latest.getFile().getFileDigest());
+    } finally {
+      storage.close();
+    }
+  }
+
+  @Test
+  public void testGetLatestSnapshotFallsBackWhenNewestMd5IsInvalid() throws 
Exception {
+    SimpleStateMachineStorage simpleStateMachineStorage = new 
SimpleStateMachineStorage();
+    final RaftStorage storage = newRaftStorage(storageDir);
+    simpleStateMachineStorage.init(storage);
+    try {
+      createSnapshot(simpleStateMachineStorage, 1, 100, true);
+      simpleStateMachineStorage.loadLatestSnapshot();
+
+      File latestSnapshot = createSnapshot(simpleStateMachineStorage, 1, 200, 
true);
+      final File latestMd5File = 
MD5FileUtil.getDigestFileForFile(latestSnapshot);
+      Files.write(latestMd5File.toPath(), 
"null".getBytes(StandardCharsets.UTF_8));
+
+      simpleStateMachineStorage.loadLatestSnapshot();
+
+      SingleFileSnapshotInfo latest = 
simpleStateMachineStorage.getLatestSnapshot();
+      Assertions.assertNotNull(latest);
+      Assertions.assertEquals(100, latest.getIndex());
+      Assertions.assertEquals(1, latest.getTerm());
+      Assertions.assertNotNull(latest.getFile().getFileDigest());
+    } finally {
+      storage.close();
+    }
+  }
+
   private static File[] assertFileCount(File dir, int expected) {
     File[] files = dir.listFiles();
     Assertions.assertNotNull(files);
@@ -303,6 +561,25 @@ public class TestRaftStorage extends BaseTest {
     return files;
   }
 
+  private File createSnapshot(SimpleStateMachineStorage storage,
+                              long term, long endIndex,
+                              boolean withMd5) throws IOException {
+    File snapshotFile = storage.getSnapshotFile(term, endIndex);
+    Assertions.assertTrue(snapshotFile.createNewFile());
+
+    if (withMd5) {
+      MD5FileUtil.computeAndSaveMd5ForFile(snapshotFile);
+    }
+
+    return snapshotFile;
+  }
+
+  private static List<String> listMatchingFileNames(File dir, 
java.util.regex.Pattern pattern) {
+    return Arrays.stream(Objects.requireNonNull(dir.list()))
+        .filter(name -> pattern.matcher(name).matches())
+        .collect(toList());
+  }
+
   @Test
   public void testNotEnoughSpace() throws IOException {
     File mockStorageDir = Mockito.spy(storageDir);

Reply via email to