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()));
}
}