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

apolovtsev pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new bb3e38a128e IGNITE-27980 Save empty index file meta for missing groups 
(#7708)
bb3e38a128e is described below

commit bb3e38a128e83d6de4ce8872b5377ef3a293aefd
Author: Alexander Polovtcev <[email protected]>
AuthorDate: Thu Mar 5 16:23:51 2026 +0200

    IGNITE-27980 Save empty index file meta for missing groups (#7708)
---
 .../raft/storage/segstore/GroupIndexMeta.java      | 14 ++++++
 .../raft/storage/segstore/IndexFileManager.java    | 28 +++++++++++
 .../raft/storage/segstore/IndexFileMeta.java       | 16 +++----
 .../storage/segstore/IndexFileManagerTest.java     | 54 ++++++++++++++++++----
 4 files changed, 94 insertions(+), 18 deletions(-)

diff --git 
a/modules/raft/src/main/java/org/apache/ignite/internal/raft/storage/segstore/GroupIndexMeta.java
 
b/modules/raft/src/main/java/org/apache/ignite/internal/raft/storage/segstore/GroupIndexMeta.java
index edf3c50c54b..c7fce3222a8 100644
--- 
a/modules/raft/src/main/java/org/apache/ignite/internal/raft/storage/segstore/GroupIndexMeta.java
+++ 
b/modules/raft/src/main/java/org/apache/ignite/internal/raft/storage/segstore/GroupIndexMeta.java
@@ -54,6 +54,10 @@ class GroupIndexMeta {
             return fileMetas.lastLogIndexExclusive();
         }
 
+        FileProperties lastFileProperties() {
+            return fileMetas.get(fileMetas.size() - 1).indexFileProperties();
+        }
+
         void addIndexMeta(IndexFileMeta indexFileMeta) {
             while (true) {
                 IndexFileMetaArray fileMetas = this.fileMetas;
@@ -131,6 +135,16 @@ class GroupIndexMeta {
                         curLastLogIndex, newFirstLogIndex
                 );
 
+        int lastFileOrdinal = curFileMetas.lastFileProperties().ordinal();
+
+        int newFileOrdinal = indexFileMeta.indexFileProperties().ordinal();
+
+        assert newFileOrdinal == lastFileOrdinal + 1 :
+                String.format(
+                        "Expected consecutive index file ordinals. Last file 
ordinal: %d, new file ordinal: %d",
+                        lastFileOrdinal, newFileOrdinal
+                );
+
         // Merge consecutive index metas into a single meta block. If there's 
an overlap (e.g. due to log truncation), start a new block,
         // which will override the previous one during search.
         if (curLastLogIndex == newFirstLogIndex) {
diff --git 
a/modules/raft/src/main/java/org/apache/ignite/internal/raft/storage/segstore/IndexFileManager.java
 
b/modules/raft/src/main/java/org/apache/ignite/internal/raft/storage/segstore/IndexFileManager.java
index 468b51530a4..7ebd05cc953 100644
--- 
a/modules/raft/src/main/java/org/apache/ignite/internal/raft/storage/segstore/IndexFileManager.java
+++ 
b/modules/raft/src/main/java/org/apache/ignite/internal/raft/storage/segstore/IndexFileManager.java
@@ -23,6 +23,8 @@ import static java.nio.file.StandardOpenOption.WRITE;
 import static org.apache.ignite.internal.util.IgniteUtils.atomicMoveFile;
 import static org.apache.ignite.internal.util.IgniteUtils.fsyncFile;
 
+import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
+import it.unimi.dsi.fastutil.longs.LongSet;
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
 import java.io.EOFException;
@@ -185,6 +187,12 @@ class IndexFileManager {
 
         metaSpecs.forEach(this::putIndexFileMeta);
 
+        var groupIdsInFile = new LongOpenHashSet(metaSpecs.size());
+
+        metaSpecs.forEach(metaSpec -> groupIdsInFile.add((long) 
metaSpec.groupId()));
+
+        putIndexFileMetasForMissingGroups(groupIdsInFile, newFileProperties);
+
         return indexFilePath;
     }
 
@@ -441,6 +449,20 @@ class IndexFileManager {
         }
     }
 
+    /**
+     * Adds empty index file metas for Raft groups that are not present in a 
particular index file (but exist in one of the previous files).
+     * This is needed because we rely on the invariant that IndexFileMeta 
array for every group has continuous file ordinals.
+     */
+    private void putIndexFileMetasForMissingGroups(LongSet groupIdsInFile, 
FileProperties indexFileProperties) {
+        groupIndexMetas.forEach((groupId, groupIndexMeta) -> {
+            if (!groupIdsInFile.contains((long) groupId)) {
+                var emptyIndexMeta = 
IndexFileMeta.empty(groupIndexMeta.lastLogIndexExclusive(), 
indexFileProperties);
+
+                groupIndexMeta.addIndexMeta(emptyIndexMeta);
+            }
+        });
+    }
+
     private static Path syncAndRename(Path from, Path to) throws IOException {
         fsyncFile(from);
 
@@ -505,6 +527,8 @@ class IndexFileManager {
                 ));
             }
 
+            var groupIdsInFile = new LongOpenHashSet(numGroups);
+
             for (int i = 0; i < numGroups; i++) {
                 ByteBuffer groupMetaBuffer = readBytes(is, GROUP_META_SIZE, 
indexFilePath);
 
@@ -519,10 +543,14 @@ class IndexFileManager {
                         firstLogIndexInclusive, lastLogIndexExclusive, 
firstIndexKept, payloadOffset, fileProperties
                 );
 
+                groupIdsInFile.add(groupId);
+
                 var metaSpec = new IndexMetaSpec(groupId, indexFileMeta, 
firstIndexKept);
 
                 putIndexFileMeta(metaSpec);
             }
+
+            putIndexFileMetasForMissingGroups(groupIdsInFile, fileProperties);
         }
     }
 
diff --git 
a/modules/raft/src/main/java/org/apache/ignite/internal/raft/storage/segstore/IndexFileMeta.java
 
b/modules/raft/src/main/java/org/apache/ignite/internal/raft/storage/segstore/IndexFileMeta.java
index 10bd62f0d61..b89a8cc7bfa 100644
--- 
a/modules/raft/src/main/java/org/apache/ignite/internal/raft/storage/segstore/IndexFileMeta.java
+++ 
b/modules/raft/src/main/java/org/apache/ignite/internal/raft/storage/segstore/IndexFileMeta.java
@@ -25,6 +25,8 @@ import org.apache.ignite.internal.tostring.S;
  * @see IndexFileManager
  */
 class IndexFileMeta {
+    private static final int NO_PAYLOAD_OFFSET = -1;
+
     private final long firstLogIndexInclusive;
 
     private final long lastLogIndexExclusive;
@@ -47,6 +49,10 @@ class IndexFileMeta {
         this.indexFileProperties = indexFileProperties;
     }
 
+    static IndexFileMeta empty(long logIndex, FileProperties 
indexFileProperties) {
+        return new IndexFileMeta(logIndex, logIndex, NO_PAYLOAD_OFFSET, 
indexFileProperties);
+    }
+
     /**
      * Returns the inclusive lower bound of log indices stored in the index 
file for the Raft Group.
      */
@@ -65,6 +71,8 @@ class IndexFileMeta {
      * Returns the offset of the payload for the Raft Group in the index file.
      */
     int indexFilePayloadOffset() {
+        assert indexFilePayloadOffset != NO_PAYLOAD_OFFSET : "Must not be 
called for empty metas.";
+
         return indexFilePayloadOffset;
     }
 
@@ -72,14 +80,6 @@ class IndexFileMeta {
         return indexFileProperties;
     }
 
-    /**
-     * Returns {@code true} if the index meta is empty. This happens if some 
data was inserted but then the log suffix got truncated,
-     * completely wiping it out.
-     */
-    boolean isEmpty() {
-        return firstLogIndexInclusive == lastLogIndexExclusive;
-    }
-
     @Override
     public boolean equals(Object o) {
         if (o == null || getClass() != o.getClass()) {
diff --git 
a/modules/raft/src/test/java/org/apache/ignite/internal/raft/storage/segstore/IndexFileManagerTest.java
 
b/modules/raft/src/test/java/org/apache/ignite/internal/raft/storage/segstore/IndexFileManagerTest.java
index 6da3b7067a8..78453d5f3c6 100644
--- 
a/modules/raft/src/test/java/org/apache/ignite/internal/raft/storage/segstore/IndexFileManagerTest.java
+++ 
b/modules/raft/src/test/java/org/apache/ignite/internal/raft/storage/segstore/IndexFileManagerTest.java
@@ -29,7 +29,6 @@ import java.util.concurrent.ThreadLocalRandom;
 import java.util.stream.IntStream;
 import org.apache.ignite.internal.testframework.IgniteAbstractTest;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 
 class IndexFileManagerTest extends IgniteAbstractTest {
@@ -559,7 +558,6 @@ class IndexFileManagerTest extends IgniteAbstractTest {
         assertThat(indexFileManager.lastLogIndexExclusive(0), is(3L));
     }
 
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-27980";)
     @Test
     void testCompactionWithMissingGroups() throws IOException {
         var memtable = new SingleThreadMemTable();
@@ -600,14 +598,50 @@ class IndexFileManagerTest extends IgniteAbstractTest {
 
         indexFileManager.onIndexFileCompacted(compactedMemtable, new 
FileProperties(2, 0), new FileProperties(2, 1));
 
-        assertThat(
-                indexFileManager.getSegmentFilePointer(0, 3),
-                is(nullValue())
-        );
+        assertThat(indexFileManager.getSegmentFilePointer(0, 3), 
is(nullValue()));
+        assertThat(indexFileManager.getSegmentFilePointer(1, 2), is(new 
SegmentFilePointer(new FileProperties(2, 1), 2)));
+    }
 
-        assertThat(
-                indexFileManager.getSegmentFilePointer(1, 2),
-                is(new SegmentFilePointer(new FileProperties(2, 1), 2))
-        );
+    @Test
+    void testRecoveryWithMissingGroups() throws IOException {
+        var memtable = new SingleThreadMemTable();
+        memtable.appendSegmentFileOffset(0, 1, 10);
+        memtable.appendSegmentFileOffset(1, 1, 20);
+        indexFileManager.saveNewIndexMemtable(memtable);
+
+        memtable = new SingleThreadMemTable();
+        // Group 1 is absent from this file — it will receive an empty 
placeholder meta.
+        memtable.appendSegmentFileOffset(0, 2, 30);
+        indexFileManager.saveNewIndexMemtable(memtable);
+
+        memtable = new SingleThreadMemTable();
+        memtable.appendSegmentFileOffset(0, 3, 40);
+        memtable.appendSegmentFileOffset(1, 2, 50);
+        indexFileManager.saveNewIndexMemtable(memtable);
+
+        // Restart — recoverIndexFileMetas must rebuild the ordinal chain for 
both groups, including empty placeholders.
+        indexFileManager = new IndexFileManager(workDir);
+        indexFileManager.start();
+
+        assertThat(indexFileManager.getSegmentFilePointer(0, 1), is(new 
SegmentFilePointer(new FileProperties(0), 10)));
+        assertThat(indexFileManager.getSegmentFilePointer(0, 2), is(new 
SegmentFilePointer(new FileProperties(1), 30)));
+        assertThat(indexFileManager.getSegmentFilePointer(0, 3), is(new 
SegmentFilePointer(new FileProperties(2), 40)));
+        assertThat(indexFileManager.getSegmentFilePointer(1, 1), is(new 
SegmentFilePointer(new FileProperties(0), 20)));
+        assertThat(indexFileManager.getSegmentFilePointer(1, 2), is(new 
SegmentFilePointer(new FileProperties(2), 50)));
+    }
+
+    @Test
+    void testGetSegmentFilePointerReturnsNullForEmptyMetaRange() throws 
IOException {
+        var memtable = new SingleThreadMemTable();
+        memtable.appendSegmentFileOffset(0, 1, 10);
+        memtable.appendSegmentFileOffset(1, 1, 20);
+        indexFileManager.saveNewIndexMemtable(memtable);
+
+        memtable = new SingleThreadMemTable();
+        // Group 1 absent — receives an empty placeholder meta covering [2, 2).
+        memtable.appendSegmentFileOffset(0, 2, 30);
+        indexFileManager.saveNewIndexMemtable(memtable);
+
+        assertThat(indexFileManager.getSegmentFilePointer(1, 2), 
is(nullValue()));
     }
 }

Reply via email to