You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iotdb.apache.org by ji...@apache.org on 2026/10/20 14:25:43 UTC
(iotdb) branch master updated: [Subscription] Fix stalled WAL replay retries across file refresh and rotation (#18919)
This is an automated email from the ASF dual-hosted git repository.
jt2594838 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/master by this push:
new 6844c476885 [Subscription] Fix stalled WAL replay retries across file refresh and rotation (#18919)
6844c476885 is described below
commit 6844c4768855004dae5313aa435b41a5058cdc2d
Author: Caideyipi <87...@users.noreply.github.com>
AuthorDate: Tue Oct 20 22:25:36 2026 +0800
[Subscription] Fix stalled WAL replay retries across file refresh and rotation (#18919)
---
.../apache/iotdb/db/i18n/DataNodePipeMessages.java | 6 +
.../apache/iotdb/db/i18n/DataNodePipeMessages.java | 6 +
.../dataregion/wal/buffer/WALBuffer.java | 11 +
.../storageengine/dataregion/wal/node/WALNode.java | 4 +
.../consensus/ConsensusPrefetchingQueue.java | 4 +-
.../broker/consensus/ProgressWALIterator.java | 211 ++++++++++++++++---
.../wal/buffer/WALBufferRollRecoveryTest.java | 23 +++
.../ConsensusPrefetchingQueueDeliveryTest.java | 2 +-
...onsensusPrefetchingQueueInitializationTest.java | 5 +-
.../consensus/ConsensusPrefetchingQueueTest.java | 5 +-
...nsensusPrefetchingQueueWalBackpressureTest.java | 11 +-
...onsensusPrefetchingQueueWriterOrderingTest.java | 2 +-
.../broker/consensus/ProgressWALIteratorTest.java | 230 ++++++++++++++++++++-
13 files changed, 471 insertions(+), 49 deletions(-)
diff --git a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
index 374a35e1642..bb04beac563 100644
--- a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
+++ b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
@@ -2270,6 +2270,12 @@ public final class DataNodePipeMessages {
MESSAGE_SUBSCRIPTION_WAL_REPLAY_SKIPPED_ARG_UNREADABLE_RETAINED_WAL_FILE_S_IN_QUEUE_ARG_REGION_ARG_HISTORICAL_DATA_MAY_BE_LOST_DETAILS_RECENT_FILES_FULL_DETAILS_IN_SERVER_LOG_ARG_FEDEFF7E =
"Subscription WAL replay skipped %d unreadable retained WAL file(s) in queue %s, region %s. Historical data may be lost. Details (recent files; full details in server log): %s";
public static final String MESSAGE_UNKNOWN_AD921D60 = "unknown";
+ public static final String
+ MESSAGE_WAL_METADATA_SNAPSHOT_FOR_FILE_ARG_VERSIONID_ARG_IS_UNAVAILABLE_AFTER_ROTATION_REPLAY_WILL_RETRY_WITHOUT_SKIPPING_D99C12A0 =
+ "WAL metadata snapshot for file %s (versionId=%d) is unavailable after rotation; replay will retry without skipping";
+ public static final String
+ EXCEPTION_WAL_FILE_ARG_VERSIONID_ARG_IS_MISSING_FROM_TWO_SUCCESSIVE_DIRECTORY_SCANS_AFTER_A_NEWER_WAL_FILE_BECAME_VISIBLE_1E170B4B =
+ "WAL file %s (versionId=%d) is missing from two successive directory scans after a newer WAL file became visible";
public static final String
MESSAGE_WAL_FILE_ARG_VERSIONID_ARG_ENTRYOFFSET_ARG_IS_TEMPORARILY_UNREADABLE_REPLAY_WILL_RETRY_WITHOUT_SKIPPING_ARG_EA11FBDD =
"WAL file %s (versionId=%d, entryOffset=%d) is temporarily unreadable; replay will retry without skipping: %s";
diff --git a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
index d9439de9014..d319d0e3772 100644
--- a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
+++ b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
@@ -2111,6 +2111,12 @@ public final class DataNodePipeMessages {
MESSAGE_SUBSCRIPTION_WAL_REPLAY_SKIPPED_ARG_UNREADABLE_RETAINED_WAL_FILE_S_IN_QUEUE_ARG_REGION_ARG_HISTORICAL_DATA_MAY_BE_LOST_DETAILS_RECENT_FILES_FULL_DETAILS_IN_SERVER_LOG_ARG_FEDEFF7E =
"订阅 WAL 重放累计跳过了 %d 个无法读取的保留 WAL 文件,队列 %s,region %s。历史数据可能丢失。详情(近期文件;完整信息见服务端日志):%s";
public static final String MESSAGE_UNKNOWN_AD921D60 = "未知";
+ public static final String
+ MESSAGE_WAL_METADATA_SNAPSHOT_FOR_FILE_ARG_VERSIONID_ARG_IS_UNAVAILABLE_AFTER_ROTATION_REPLAY_WILL_RETRY_WITHOUT_SKIPPING_D99C12A0 =
+ "WAL 文件 %s(versionId=%d)在轮转后无法获取对应的元数据快照;回放将重试,不跳过该文件";
+ public static final String
+ EXCEPTION_WAL_FILE_ARG_VERSIONID_ARG_IS_MISSING_FROM_TWO_SUCCESSIVE_DIRECTORY_SCANS_AFTER_A_NEWER_WAL_FILE_BECAME_VISIBLE_1E170B4B =
+ "后继 WAL 文件可见后,WAL 文件 %s(versionId=%d)在连续两次目录扫描中均不存在";
public static final String
MESSAGE_WAL_FILE_ARG_VERSIONID_ARG_ENTRYOFFSET_ARG_IS_TEMPORARILY_UNREADABLE_REPLAY_WILL_RETRY_WITHOUT_SKIPPING_ARG_EA11FBDD =
"WAL 文件 %s(versionId=%d,entryOffset=%d)暂不可读;重放将重试而不跳过:%s";
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/WALBuffer.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/WALBuffer.java
index 45432e7ab3d..ca975b397fc 100644
--- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/WALBuffer.java
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/WALBuffer.java
@@ -893,6 +893,17 @@ public class WALBuffer extends AbstractWALBuffer {
return writer == null ? new WALMetaData() : writer.snapshotMetaData();
}
+ /** Returns null if rotation has already installed a writer for another file version. */
+ public WALMetaData getWALMetaDataSnapshot(final long fileVersionId) {
+ final WALWriter writer = currentWALFileWriter;
+ // The writer and currentWALFileVersion are published separately during rotation. Bind the
+ // snapshot to the captured writer itself, including while the version field is still old.
+ return writer != null
+ && WALFileUtils.parseVersionId(writer.getLogFile().getName()) == fileVersionId
+ ? writer.snapshotMetaData()
+ : null;
+ }
+
public CheckpointManager getCheckpointManager() {
return checkpointManager;
}
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/node/WALNode.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/node/WALNode.java
index fd186d695d8..10526aaec51 100644
--- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/node/WALNode.java
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/node/WALNode.java
@@ -1090,6 +1090,10 @@ public class WALNode implements IWALNode {
return buffer.getCurrentWALMetaDataSnapshot();
}
+ public WALMetaData getWALMetaDataSnapshot(final long fileVersionId) {
+ return buffer.getWALMetaDataSnapshot(fileVersionId);
+ }
+
@Override
public long getTotalSize() {
return WALManager.getInstance().getTotalDiskUsage();
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
index 743beeeb7ad..812fc0f6f7d 100644
--- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
@@ -4741,8 +4741,8 @@ public class ConsensusPrefetchingQueue {
private static LiveWALMetaDataSnapshot captureLiveWALMetaDataSnapshot(final WALNode walNode) {
while (true) {
final long versionId = walNode.getCurrentWALFileVersion();
- final WALMetaData metadata = walNode.getCurrentWALMetaDataSnapshot();
- if (versionId == walNode.getCurrentWALFileVersion()) {
+ final WALMetaData metadata = walNode.getWALMetaDataSnapshot(versionId);
+ if (metadata != null && versionId == walNode.getCurrentWALFileVersion()) {
return new LiveWALMetaDataSnapshot(versionId, metadata);
}
}
diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIterator.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIterator.java
index ce33ee974bb..4b1a58a3009 100644
--- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIterator.java
+++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIterator.java
@@ -43,6 +43,8 @@ import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -127,6 +129,7 @@ public class ProgressWALIterator implements Closeable, Iterator<IndexedConsensus
private long[] walFileVersionIds;
private int currentFileIndex = -1;
private ProgressWALReader currentReader;
+ private File currentReaderFile;
private long currentReaderVersionId = -1L;
private boolean currentReaderUsesLiveSnapshot = false;
private int consumedEntryCountInCurrentFile = 0;
@@ -135,6 +138,8 @@ public class ProgressWALIterator implements Closeable, Iterator<IndexedConsensus
private final List<String> skippedBrokenWalFileDetails = new ArrayList<>();
private long skippedBrokenWalEntryCount;
private boolean retryCurrentReader;
+ private boolean missingRetryFileObserved;
+ private String currentReaderRetryDetail;
private long eofRetryVersionId = -1L;
private int eofRetryEntryOffset = -1;
private long eofRetryFileLength = -1L;
@@ -152,6 +157,7 @@ public class ProgressWALIterator implements Closeable, Iterator<IndexedConsensus
private IndexedConsensusRequest nextReady;
private boolean scanYielded;
private long bodyReadAttempts;
+ private long readerOpenAttempts;
public ProgressWALIterator(final File logDirectory) {
this(logDirectory, Long.MIN_VALUE);
@@ -398,7 +404,10 @@ public class ProgressWALIterator implements Closeable, Iterator<IndexedConsensus
final WALMetaData metadata;
final long versionId = walFileVersionIds[fileIndex];
if (liveWalNode != null && versionId == liveWalNode.getCurrentWALFileVersion()) {
- metadata = liveWalNode.getCurrentWALMetaDataSnapshot();
+ metadata = liveWalNode.getWALMetaDataSnapshot(versionId);
+ if (metadata == null) {
+ return false;
+ }
} else {
try (final ProgressWALReader reader = new ProgressWALReader(walFile)) {
metadata = reader.getMetaData();
@@ -503,8 +512,13 @@ public class ProgressWALIterator implements Closeable, Iterator<IndexedConsensus
+ consumedEntryCountInCurrentFile
+ ", bodyReadAttempts="
+ bodyReadAttempts
+ + ", readerOpenAttempts="
+ + readerOpenAttempts
+ ", minimumSearchIndex="
+ minimumSearchIndex
+ + (retryCurrentReader
+ ? ", retryFile=" + currentReaderFile + ", retryReason=" + currentReaderRetryDetail
+ : "")
+ "]";
}
@@ -558,7 +572,9 @@ public class ProgressWALIterator implements Closeable, Iterator<IndexedConsensus
refresh();
final int retryFileIndex = findFileIndexByVersion(currentReaderVersionId);
if (retryFileIndex < 0) {
- // Do not let a temporarily invisible file make replay silently move to its successor.
+ if (resumeAfterFilteredRetryFile()) {
+ continue;
+ }
return null;
}
if (!openReaderAtIndex(retryFileIndex, consumedEntryCountInCurrentFile)) {
@@ -664,6 +680,97 @@ public class ProgressWALIterator implements Closeable, Iterator<IndexedConsensus
}
}
+ private boolean resumeAfterFilteredRetryFile() throws IOException {
+ final long versionId = currentReaderVersionId;
+ final boolean sealed =
+ liveWalNode != null
+ ? versionId < liveWalNode.getCurrentWALFileVersion()
+ : walFileVersionIds.length > 0
+ && versionId < walFileVersionIds[walFileVersionIds.length - 1];
+ final File[] discoveredFiles = WALFileUtils.listAllWALFiles(logDirectory);
+ if (discoveredFiles == null) {
+ missingRetryFileObserved = false;
+ return false;
+ }
+ File retryFile = null;
+ for (final File file : discoveredFiles) {
+ if (WALFileUtils.parseVersionId(file.getName()) == versionId) {
+ if (retryFile != null) {
+ missingRetryFileObserved = false;
+ return false;
+ }
+ retryFile = file;
+ }
+ }
+ if (retryFile != null) {
+ // Refresh deliberately omits sealed header-only files. A retry of such a file must finish
+ // instead of waiting forever for its version to reappear in the filtered list.
+ missingRetryFileObserved = false;
+ if (sealed && isHeaderOnlyWalFile(retryFile)) {
+ try (FileChannel channel = FileChannel.open(retryFile.toPath())) {
+ if (WALFileVersion.isEmptyOrHeaderOnly(channel)) {
+ if (consumedEntryCountInCurrentFile > 0) {
+ // This version used to contain entries. Confirm truncation before discarding its
+ // incomplete request, rather than treating its remaining header as a normal empty
+ // WAL.
+ final EOFException truncation =
+ new EOFException(
+ String.format(
+ DataNodePipeMessages
+ .MESSAGE_FAILED_TO_REOPEN_WAL_FILE_ARG_AT_ENTRY_OFFSET_ARG_ITERATOR_COULD_NOT_SKIP_TO_THE_REQUESTED_POSITION_332B3AD9,
+ retryFile.getName(),
+ consumedEntryCountInCurrentFile));
+ if (!isConfirmedRetainedWalTruncation(
+ retryFile, versionId, consumedEntryCountInCurrentFile, false)) {
+ markIncompleteScan(truncation.getMessage(), truncation);
+ return false;
+ }
+ recordSkippedBrokenWalFile(
+ versionId, retryFile, truncation, null, consumedEntryCountInCurrentFile);
+ pendingRequests.clear();
+ pendingSearchIndex = Long.MIN_VALUE;
+ pendingLocalSeq = Long.MIN_VALUE;
+ }
+ resetCurrentFileTracking();
+ return true;
+ }
+ }
+ }
+ return false;
+ } else {
+ // Do not skip a temporarily invisible active file, an inaccessible directory, or a rename
+ // for which another filename with the same version is still present. Require the retry
+ // version to be sealed, a visible successor, and two listings confirming the path is absent.
+ if (!sealed
+ || currentReaderFile == null
+ || walFileVersionIds.length == 0
+ || walFileVersionIds[walFileVersionIds.length - 1] <= versionId
+ || !Files.notExists(currentReaderFile.toPath())) {
+ missingRetryFileObserved = false;
+ return false;
+ }
+ if (!missingRetryFileObserved) {
+ missingRetryFileObserved = true;
+ return false;
+ }
+ final IOException missingFile =
+ new IOException(
+ String.format(
+ DataNodePipeMessages
+ .EXCEPTION_WAL_FILE_ARG_VERSIONID_ARG_IS_MISSING_FROM_TWO_SUCCESSIVE_DIRECTORY_SCANS_AFTER_A_NEWER_WAL_FILE_BECAME_VISIBLE_1E170B4B,
+ currentReaderFile.getAbsolutePath(),
+ versionId));
+ recordSkippedBrokenWalFile(
+ versionId, currentReaderFile, missingFile, null, consumedEntryCountInCurrentFile);
+ // The missing file may have continued the incomplete request. Never publish its prefix.
+ pendingRequests.clear();
+ pendingSearchIndex = Long.MIN_VALUE;
+ pendingLocalSeq = Long.MIN_VALUE;
+ resetCurrentFileTracking();
+ return true;
+ }
+ }
+
private boolean openNextReader(final ScanBudget scanBudget) throws IOException {
while (currentFileIndex + 1 < walFiles.length) {
if (scanBudget != null && scanBudget.shouldYield()) {
@@ -692,7 +799,15 @@ public class ProgressWALIterator implements Closeable, Iterator<IndexedConsensus
final long currentLiveVersionId = liveWalNode.getCurrentWALFileVersion();
if (currentLiveVersionId == currentReaderVersionId) {
- final WALMetaData snapshot = liveWalNode.getCurrentWALMetaDataSnapshot();
+ final WALMetaData snapshot = liveWalNode.getWALMetaDataSnapshot(currentReaderVersionId);
+ if (snapshot == null) {
+ final int fileIndex = findFileIndexByVersion(currentReaderVersionId);
+ if (fileIndex < 0) {
+ retryCurrentReader = true;
+ return false;
+ }
+ return openReaderAtIndex(fileIndex, consumedEntryCountInCurrentFile);
+ }
if (snapshot.getBuffersSize().size() <= consumedEntryCountInCurrentFile) {
return false;
}
@@ -736,39 +851,52 @@ public class ProgressWALIterator implements Closeable, Iterator<IndexedConsensus
final File walFile = walFiles[fileIndex];
final long versionId = walFileVersionIds[fileIndex];
currentFileIndex = fileIndex;
+ currentReaderFile = walFile;
+ readerOpenAttempts++;
final boolean useLiveSnapshot =
liveWalNode != null && versionId == liveWalNode.getCurrentWALFileVersion();
ProgressWALReader reader = null;
try {
- reader =
- useLiveSnapshot
- ? new ProgressWALReader(
- walFile,
- liveMetaDataSnapshot != null
- ? liveMetaDataSnapshot
- : liveWalNode.getCurrentWALMetaDataSnapshot())
- : new ProgressWALReader(walFile);
- if (!skipEntries(reader, skipEntries)) {
- reader.close();
+ final WALMetaData snapshot =
+ !useLiveSnapshot
+ ? null
+ : liveMetaDataSnapshot != null
+ ? liveMetaDataSnapshot
+ : liveWalNode.getWALMetaDataSnapshot(versionId);
+ if (useLiveSnapshot && snapshot == null) {
markIncompleteScan(
String.format(
DataNodePipeMessages
- .MESSAGE_FAILED_TO_REOPEN_WAL_FILE_ARG_AT_ENTRY_OFFSET_ARG_ITERATOR_COULD_NOT_SKIP_TO_THE_REQUESTED_POSITION_332B3AD9,
- walFile.getName(),
- skipEntries),
+ .MESSAGE_WAL_METADATA_SNAPSHOT_FOR_FILE_ARG_VERSIONID_ARG_IS_UNAVAILABLE_AFTER_ROTATION_REPLAY_WILL_RETRY_WITHOUT_SKIPPING_D99C12A0,
+ walFile.getAbsolutePath(),
+ versionId),
null);
currentReaderVersionId = versionId;
consumedEntryCountInCurrentFile = skipEntries;
retryCurrentReader = true;
return false;
}
+ reader =
+ useLiveSnapshot
+ ? new ProgressWALReader(walFile, snapshot)
+ : new ProgressWALReader(walFile);
+ if (!skipEntries(reader, skipEntries)) {
+ throw new EOFException(
+ String.format(
+ DataNodePipeMessages
+ .MESSAGE_FAILED_TO_REOPEN_WAL_FILE_ARG_AT_ENTRY_OFFSET_ARG_ITERATOR_COULD_NOT_SKIP_TO_THE_REQUESTED_POSITION_332B3AD9,
+ walFile.getName(),
+ skipEntries));
+ }
currentReader = reader;
currentFileIndex = fileIndex;
currentReaderVersionId = versionId;
currentReaderUsesLiveSnapshot = useLiveSnapshot;
consumedEntryCountInCurrentFile = skipEntries;
retryCurrentReader = false;
+ missingRetryFileObserved = false;
+ currentReaderRetryDetail = null;
return true;
} catch (final IOException e) {
if (reader != null) {
@@ -778,15 +906,19 @@ public class ProgressWALIterator implements Closeable, Iterator<IndexedConsensus
e.addSuppressed(closeException);
}
}
- if (isNearLiveWalVersion(versionId)
- || !(e instanceof BrokenWALFileException)
- || (liveWalNode == null && fileIndex == walFiles.length - 1)) {
+ final boolean confirmedTruncation =
+ e instanceof EOFException
+ && isConfirmedRetainedWalTruncation(walFile, versionId, skipEntries, useLiveSnapshot);
+ if (!confirmedTruncation
+ && (isNearLiveWalVersion(versionId)
+ || !(e instanceof BrokenWALFileException)
+ || (liveWalNode == null && fileIndex == walFiles.length - 1))) {
LOGGER.debug(
DataNodePipeMessages
.PIPE_LOG_PROGRESSWALITERATOR_FAILED_TO_OPEN_NEAR_LIVE_WAL_FILE_RETRYING_5AEB94AC,
walFile.getName(),
e);
- if (allowNearLiveRetry) {
+ if (allowNearLiveRetry && !(e instanceof EOFException)) {
refresh();
final int refreshedIndex = findFileIndexByVersion(versionId);
if (refreshedIndex >= 0) {
@@ -809,7 +941,8 @@ public class ProgressWALIterator implements Closeable, Iterator<IndexedConsensus
retryCurrentReader = true;
return false;
}
- recordSkippedBrokenWalFile(versionId, walFile, e, null, skipEntries);
+ recordSkippedBrokenWalFile(
+ versionId, walFile, e, reader == null ? null : reader.getMetaData(), skipEntries);
pendingRequests.clear();
pendingSearchIndex = Long.MIN_VALUE;
pendingLocalSeq = Long.MIN_VALUE;
@@ -838,10 +971,11 @@ public class ProgressWALIterator implements Closeable, Iterator<IndexedConsensus
// Filename boundaries are file-level search-index bounds, not an entry count. Footer entry
// ordinals also include request fragments and entries from other writers.
final long firstSearchIndex = WALFileUtils.parseStartSearchIndex(walFile.getName());
- final int fileIndex = findFileIndexByVersion(versionId);
+ final int fileIndex = Arrays.binarySearch(walFileVersionIds, versionId);
+ final int successorIndex = fileIndex >= 0 ? fileIndex + 1 : -fileIndex - 1;
final long lastSearchIndex =
- fileIndex >= 0 && fileIndex + 1 < walFiles.length
- ? WALFileUtils.parseStartSearchIndex(walFiles[fileIndex + 1].getName())
+ successorIndex < walFiles.length
+ ? WALFileUtils.parseStartSearchIndex(walFiles[successorIndex].getName())
: -1L;
final String detail =
String.format(
@@ -871,20 +1005,31 @@ public class ProgressWALIterator implements Closeable, Iterator<IndexedConsensus
}
private boolean isConfirmedRetainedWalTruncation() {
- final File walFile = walFiles[currentFileIndex];
+ return isConfirmedRetainedWalTruncation(
+ walFiles[currentFileIndex],
+ currentReaderVersionId,
+ consumedEntryCountInCurrentFile,
+ currentReaderUsesLiveSnapshot);
+ }
+
+ private boolean isConfirmedRetainedWalTruncation(
+ final File walFile,
+ final long versionId,
+ final int entryOffset,
+ final boolean usesLiveSnapshot) {
final boolean sealed =
liveWalNode != null
- ? !isNearLiveWalVersion(currentReaderVersionId)
+ ? !isNearLiveWalVersion(versionId)
: currentFileIndex + 1 < walFiles.length;
final boolean confirmed =
- !currentReaderUsesLiveSnapshot
+ !usesLiveSnapshot
&& sealed
- && eofRetryVersionId == currentReaderVersionId
- && eofRetryEntryOffset == consumedEntryCountInCurrentFile
+ && eofRetryVersionId == versionId
+ && eofRetryEntryOffset == entryOffset
&& eofRetryFileLength == walFile.length()
&& eofRetryFileModified == walFile.lastModified();
- eofRetryVersionId = currentReaderVersionId;
- eofRetryEntryOffset = consumedEntryCountInCurrentFile;
+ eofRetryVersionId = versionId;
+ eofRetryEntryOffset = entryOffset;
eofRetryFileLength = walFile.length();
eofRetryFileModified = walFile.lastModified();
return confirmed;
@@ -1068,15 +1213,19 @@ public class ProgressWALIterator implements Closeable, Iterator<IndexedConsensus
}
private void resetCurrentFileTracking() {
+ currentReaderFile = null;
currentReaderVersionId = -1L;
currentReaderUsesLiveSnapshot = false;
consumedEntryCountInCurrentFile = 0;
retryCurrentReader = false;
+ missingRetryFileObserved = false;
+ currentReaderRetryDetail = null;
eofRetryVersionId = -1L;
eofRetryEntryOffset = -1;
}
private void markIncompleteScan(final String detail, final IOException cause) {
+ currentReaderRetryDetail = detail;
incompleteScan = true;
if (incompleteScanDetail == null) {
incompleteScanDetail = detail;
diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/WALBufferRollRecoveryTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/WALBufferRollRecoveryTest.java
index 84e61d7f00c..b6f6d8f5bd6 100644
--- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/WALBufferRollRecoveryTest.java
+++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/WALBufferRollRecoveryTest.java
@@ -57,6 +57,7 @@ import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doCallRealMethod;
@@ -140,6 +141,28 @@ public class WALBufferRollRecoveryTest {
assertEquals(Arrays.asList(2L), readTimes(successor));
}
+ @Test
+ public void testMetadataSnapshotIsBoundToWriterDuringVersionPublication() throws Exception {
+ writeAndAwait(entry(1, 1, "first"), Status.SUCCESS);
+ assertEquals(1, buffer.getWALMetaDataSnapshot(0L).getBuffersSize().size());
+ roll(Status.SUCCESS);
+ writeAndAwait(entry(2, 2, "second"), Status.SUCCESS);
+ assertNull(buffer.getWALMetaDataSnapshot(0L));
+ final WALMetaData successorMetadata = buffer.getWALMetaDataSnapshot(1L);
+ assertEquals(1, successorMetadata.getBuffersSize().size());
+
+ // Reproduce the interval in rollLogWriter between publishing the new writer and publishing
+ // its version. Checking the version field before and after snapshot capture is insufficient.
+ buffer.currentWALFileVersion = 0L;
+ try {
+ assertNull(buffer.getWALMetaDataSnapshot(0L));
+ assertEquals(
+ successorMetadata.getBuffersSize(), buffer.getWALMetaDataSnapshot(1L).getBuffersSize());
+ } finally {
+ buffer.currentWALFileVersion = 1L;
+ }
+ }
+
/** A retry containing only the roll signal should open exactly one successor. */
@Test
public void testRollSignalResumesWithoutRollingTwice() throws Exception {
diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueDeliveryTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueDeliveryTest.java
index e1ff4aaeb0b..df12f63caa6 100644
--- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueDeliveryTest.java
+++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueDeliveryTest.java
@@ -134,7 +134,7 @@ public class ConsensusPrefetchingQueueDeliveryTest {
when(walNode.getLogDirectory()).thenReturn(walDirectory);
when(walNode.getCurrentSearchIndex()).thenAnswer(ignored -> currentSearchIndex.get());
when(walNode.getCurrentWALFileVersion()).thenReturn(1L);
- when(walNode.getCurrentWALMetaDataSnapshot()).thenReturn(new WALMetaData());
+ when(walNode.getWALMetaDataSnapshot(1L)).thenReturn(new WALMetaData());
when(walNode.getSortedWalFilesSnapshot())
.thenAnswer(
ignored -> {
diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueInitializationTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueInitializationTest.java
index a663437e360..ad7333751d1 100644
--- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueInitializationTest.java
+++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueInitializationTest.java
@@ -59,6 +59,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@@ -90,7 +91,7 @@ public class ConsensusPrefetchingQueueInitializationTest {
when(walNode.getLogDirectory()).thenReturn(walDirectory);
when(walNode.getCurrentSearchIndex()).thenReturn(3L);
when(walNode.getCurrentWALFileVersion()).thenReturn(3L);
- when(walNode.getCurrentWALMetaDataSnapshot()).thenReturn(new WALMetaData());
+ when(walNode.getWALMetaDataSnapshot(anyLong())).thenReturn(new WALMetaData());
when(converter.convert(any())).thenReturn(Collections.emptyList());
when(converter.isTableModel()).thenReturn(true);
@@ -168,7 +169,7 @@ public class ConsensusPrefetchingQueueInitializationTest {
Files.delete(liveWal.toPath());
final WALMetaData metadata = writeRequests(liveWal);
- when(walNode.getCurrentWALMetaDataSnapshot()).thenReturn(metadata);
+ when(walNode.getWALMetaDataSnapshot(anyLong())).thenReturn(metadata);
assertResumesAfterCommittedProgress();
}
diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
index e3de8b31279..7ee54b5f830 100644
--- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
+++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
@@ -104,6 +104,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
@@ -1813,7 +1814,7 @@ public class ConsensusPrefetchingQueueTest {
flushedMetadata.add(Integer.BYTES, 1L, 0L, 0L, 7, 1L);
when(walNode.getLogDirectory()).thenReturn(walDirectory);
when(walNode.getCurrentWALFileVersion()).thenReturn(0L);
- when(walNode.getCurrentWALMetaDataSnapshot())
+ when(walNode.getWALMetaDataSnapshot(anyLong()))
.thenAnswer(ignored -> rolled.get() ? flushedMetadata.copy() : new WALMetaData());
doAnswer(
ignored -> {
@@ -1876,7 +1877,7 @@ public class ConsensusPrefetchingQueueTest {
final WALNode walNode = mock(WALNode.class);
when(walNode.getLogDirectory()).thenReturn(walDirectory);
when(walNode.getCurrentWALFileVersion()).thenReturn(1L);
- when(walNode.getCurrentWALMetaDataSnapshot()).thenReturn(new WALMetaData());
+ when(walNode.getWALMetaDataSnapshot(anyLong())).thenReturn(new WALMetaData());
final IoTConsensusServerImpl serverImpl = mock(IoTConsensusServerImpl.class);
when(serverImpl.getConsensusReqReader()).thenReturn(walNode);
queue =
diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWalBackpressureTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWalBackpressureTest.java
index adcf18a50d8..24d76d2d123 100644
--- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWalBackpressureTest.java
+++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWalBackpressureTest.java
@@ -79,6 +79,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -129,7 +130,7 @@ public class ConsensusPrefetchingQueueWalBackpressureTest {
when(walNode.getLogDirectory()).thenReturn(walDirectory);
when(walNode.getCurrentSearchIndex()).thenReturn(3L);
when(walNode.getCurrentWALFileVersion()).thenReturn(1L);
- when(walNode.getCurrentWALMetaDataSnapshot()).thenReturn(new WALMetaData());
+ when(walNode.getWALMetaDataSnapshot(anyLong())).thenReturn(new WALMetaData());
final IoTConsensusServerImpl serverImpl = mock(IoTConsensusServerImpl.class);
when(serverImpl.getConsensusReqReader()).thenReturn(walNode);
when(serverImpl.getWriterSafeFrontierTracker()).thenReturn(new WriterSafeFrontierTracker());
@@ -304,7 +305,7 @@ public class ConsensusPrefetchingQueueWalBackpressureTest {
when(walNode.getLogDirectory()).thenReturn(walDirectory);
when(walNode.getCurrentSearchIndex()).thenReturn((long) requestCount);
when(walNode.getCurrentWALFileVersion()).thenReturn(1L);
- when(walNode.getCurrentWALMetaDataSnapshot()).thenReturn(new WALMetaData());
+ when(walNode.getWALMetaDataSnapshot(anyLong())).thenReturn(new WALMetaData());
final IoTConsensusServerImpl serverImpl = mock(IoTConsensusServerImpl.class);
when(serverImpl.getConsensusReqReader()).thenReturn(walNode);
when(serverImpl.getWriterSafeFrontierTracker()).thenReturn(new WriterSafeFrontierTracker());
@@ -453,7 +454,7 @@ public class ConsensusPrefetchingQueueWalBackpressureTest {
when(walNode.getLogDirectory()).thenReturn(walDirectory);
when(walNode.getCurrentSearchIndex()).thenReturn((long) REQUEST_COUNT);
when(walNode.getCurrentWALFileVersion()).thenReturn(1L);
- when(walNode.getCurrentWALMetaDataSnapshot()).thenReturn(new WALMetaData());
+ when(walNode.getWALMetaDataSnapshot(anyLong())).thenReturn(new WALMetaData());
final IoTConsensusServerImpl serverImpl = mock(IoTConsensusServerImpl.class);
when(serverImpl.getConsensusReqReader()).thenReturn(walNode);
@@ -588,7 +589,7 @@ public class ConsensusPrefetchingQueueWalBackpressureTest {
when(walNode.getLogDirectory()).thenReturn(walDirectory);
when(walNode.getCurrentSearchIndex()).thenReturn((long) REQUEST_COUNT);
when(walNode.getCurrentWALFileVersion()).thenReturn(1L);
- when(walNode.getCurrentWALMetaDataSnapshot()).thenReturn(new WALMetaData());
+ when(walNode.getWALMetaDataSnapshot(anyLong())).thenReturn(new WALMetaData());
final IoTConsensusServerImpl serverImpl = mock(IoTConsensusServerImpl.class);
when(serverImpl.getConsensusReqReader()).thenReturn(walNode);
@@ -726,7 +727,7 @@ public class ConsensusPrefetchingQueueWalBackpressureTest {
when(walNode.getLogDirectory()).thenReturn(walDirectory);
when(walNode.getCurrentSearchIndex()).thenReturn((long) requestCount);
when(walNode.getCurrentWALFileVersion()).thenReturn(1L);
- when(walNode.getCurrentWALMetaDataSnapshot()).thenReturn(new WALMetaData());
+ when(walNode.getWALMetaDataSnapshot(anyLong())).thenReturn(new WALMetaData());
final IoTConsensusServerImpl serverImpl = mock(IoTConsensusServerImpl.class);
when(serverImpl.getConsensusReqReader()).thenReturn(walNode);
diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWriterOrderingTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWriterOrderingTest.java
index b34dd71b6f5..d943ca41241 100644
--- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWriterOrderingTest.java
+++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWriterOrderingTest.java
@@ -476,7 +476,7 @@ public class ConsensusPrefetchingQueueWriterOrderingTest {
when(walNode.getLogDirectory()).thenReturn(directory);
when(walNode.getCurrentSearchIndex()).thenReturn(currentIndex);
when(walNode.getCurrentWALFileVersion()).thenReturn(1L);
- when(walNode.getCurrentWALMetaDataSnapshot()).thenReturn(new WALMetaData());
+ when(walNode.getWALMetaDataSnapshot(1L)).thenReturn(new WALMetaData());
return walNode;
}
diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIteratorTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIteratorTest.java
index 370fca53796..a4bfb29304d 100644
--- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIteratorTest.java
+++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIteratorTest.java
@@ -52,6 +52,7 @@ import java.util.concurrent.atomic.AtomicLong;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -666,7 +667,7 @@ public class ProgressWALIteratorTest {
final WALNode walNode = mock(WALNode.class);
when(walNode.getLogDirectory()).thenReturn(dir.toFile());
when(walNode.getCurrentWALFileVersion()).thenReturn(1L);
- when(walNode.getCurrentWALMetaDataSnapshot()).thenReturn(successorMetadata);
+ when(walNode.getWALMetaDataSnapshot(anyLong())).thenReturn(successorMetadata);
try (ProgressWALIterator iterator =
nearLive
? new ProgressWALIterator(walNode, 1L)
@@ -732,6 +733,61 @@ public class ProgressWALIteratorTest {
}
}
+ @Test
+ public void testTruncatedWalReopenDoesNotRetryUnreachableOffsetForever() throws Exception {
+ assertTruncatedWalReplayResumes(false);
+ }
+
+ @Test
+ public void testHeaderOnlyWalTruncationDoesNotPublishIncompleteRequest() throws Exception {
+ assertTruncatedWalReplayResumes(true);
+ }
+
+ private void assertTruncatedWalReplayResumes(final boolean headerOnly) throws Exception {
+ final Path dir = Files.createTempDirectory("progress-wal-retry-truncated-offset");
+ final File dataWal =
+ dir.resolve(WALFileUtils.getLogFileName(0, 0, WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+ final File successorWal =
+ dir.resolve(WALFileUtils.getLogFileName(1, 2, WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+ try {
+ try (WALWriter writer = new WALWriter(dataWal, WALFileVersion.V3)) {
+ writer.write(searchableEntry(1L), singleEntryMeta(19, 1L, 1L, 100L, 7, 1L));
+ writer.write(searchableEntry(2L), singleEntryMeta(19, 2L, 1L, 200L, 7, 2L));
+ writer.write(searchableEntry(2L), singleEntryMeta(38, 2L, 1L, 200L, 7, 2L));
+ }
+ try (WALWriter writer = new WALWriter(successorWal, WALFileVersion.V3)) {
+ writer.write(searchableEntry(3L), singleEntryMeta(19, 3L, 1L, 300L, 7, 3L));
+ }
+ try (ProgressWALIterator iterator = new ProgressWALIterator(dir.toFile(), 1L)) {
+ assertTrue(iterator.hasNext());
+ assertEquals(1L, iterator.next().getSearchIndex());
+ assertFalse(iterator.hasNext());
+ assertTrue(iterator.isWaitingForReadableWal());
+
+ // A repaired/truncated retained file now has fewer entries than the saved retry offset.
+ Files.delete(dataWal.toPath());
+ try (WALWriter writer = new WALWriter(dataWal, WALFileVersion.V3)) {
+ if (!headerOnly) {
+ writer.write(searchableEntry(1L), singleEntryMeta(19, 1L, 1L, 100L, 7, 1L));
+ }
+ }
+ assertFalse(iterator.hasNext());
+ assertEquals(0, iterator.getSkippedBrokenWalFileCount());
+ assertTrue(iterator.hasNext());
+ assertEquals(3L, iterator.next().getSearchIndex());
+ assertEquals(1, iterator.getSkippedBrokenWalFileCount());
+ assertTrue(iterator.getSkippedBrokenWalFileDetails(0).contains("EOFException"));
+ assertFalse(iterator.hasNext());
+ }
+ } finally {
+ Files.deleteIfExists(dataWal.toPath());
+ Files.deleteIfExists(successorWal.toPath());
+ Files.deleteIfExists(dir);
+ }
+ }
+
@Test
public void testLiveSnapshotEofPreservesIncompleteRequestUntilBytesAreVisible() throws Exception {
final Path dir = Files.createTempDirectory("progress-wal-iterator-live-eof-retry");
@@ -744,7 +800,7 @@ public class ProgressWALIteratorTest {
final WALNode walNode = mock(WALNode.class);
when(walNode.getLogDirectory()).thenReturn(dir.toFile());
when(walNode.getCurrentWALFileVersion()).thenReturn(0L);
- when(walNode.getCurrentWALMetaDataSnapshot()).thenReturn(metadata);
+ when(walNode.getWALMetaDataSnapshot(anyLong())).thenReturn(metadata);
try (WALWriter writer = new WALWriter(liveWal, WALFileVersion.V3)) {
writer.write(searchableEntry(1L), singleEntryMeta(19, 1L, 1L, 100L, 7, 1L));
try (ProgressWALIterator iterator = new ProgressWALIterator(walNode, 1L)) {
@@ -811,6 +867,170 @@ public class ProgressWALIteratorTest {
}
}
+ @Test
+ public void testRetryContinuesAfterSealedEmptyWalIsFiltered() throws Exception {
+ final Path dir = Files.createTempDirectory("progress-wal-retry-empty-file");
+ final File emptyWal =
+ dir.resolve(WALFileUtils.getLogFileName(0, 0, WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+ final File successorWal =
+ dir.resolve(WALFileUtils.getLogFileName(1, 0, WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+ try {
+ // Simulate an open failure while this is the active WAL. The same version will become a
+ // valid sealed header-only file, which refreshFileList deliberately filters out.
+ assertTrue(emptyWal.mkdir());
+ final WALNode walNode = mock(WALNode.class);
+ when(walNode.getLogDirectory()).thenReturn(dir.toFile());
+ when(walNode.getCurrentWALFileVersion()).thenReturn(0L);
+ when(walNode.getWALMetaDataSnapshot(anyLong())).thenReturn(new WALMetaData());
+ try (ProgressWALIterator iterator = new ProgressWALIterator(walNode, 1L)) {
+ assertFalse(iterator.hasNext());
+ assertTrue(iterator.isWaitingForReadableWal());
+ Files.delete(emptyWal.toPath());
+ try (WALWriter ignored = new WALWriter(emptyWal, WALFileVersion.V3)) {
+ // A sealed empty WAL has only its version header.
+ }
+ final WALMetaData metadata = singleEntryMeta(19, 1L, 1L, 100L, 7, 1L);
+ try (WALWriter writer = new WALWriter(successorWal, WALFileVersion.V3)) {
+ writer.write(searchableEntry(1L), metadata);
+ }
+ when(walNode.getCurrentWALFileVersion()).thenReturn(1L);
+ when(walNode.getWALMetaDataSnapshot(anyLong())).thenReturn(metadata);
+
+ assertTrue(iterator.toString(), iterator.hasNext());
+ assertEquals(1L, iterator.next().getSearchIndex());
+ assertFalse(iterator.hasNext());
+ assertFalse(iterator.isWaitingForReadableWal());
+ assertEquals(0, iterator.getSkippedBrokenWalFileCount());
+ }
+ } finally {
+ Files.deleteIfExists(emptyWal.toPath());
+ Files.deleteIfExists(successorWal.toPath());
+ Files.deleteIfExists(dir);
+ }
+ }
+
+ @Test
+ public void testMissingActiveWalRetriesUntilItReappears() throws Exception {
+ final Path dir = Files.createTempDirectory("progress-wal-retry-invisible-file");
+ final File liveWal =
+ dir.resolve(WALFileUtils.getLogFileName(0, 0, WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+ try {
+ assertTrue(liveWal.mkdir());
+ final WALMetaData metadata = singleEntryMeta(19, 1L, 1L, 100L, 7, 1L);
+ final WALNode walNode = mock(WALNode.class);
+ when(walNode.getLogDirectory()).thenReturn(dir.toFile());
+ when(walNode.getCurrentWALFileVersion()).thenReturn(0L);
+ when(walNode.getWALMetaDataSnapshot(0L)).thenReturn(metadata);
+ try (ProgressWALIterator iterator = new ProgressWALIterator(walNode, 1L)) {
+ assertFalse(iterator.hasNext());
+ Files.delete(liveWal.toPath());
+ for (int attempt = 0; attempt < 3; attempt++) {
+ assertFalse(iterator.hasNext());
+ assertTrue(iterator.isWaitingForReadableWal());
+ assertEquals(0, iterator.getSkippedBrokenWalFileCount());
+ }
+ assertTrue(iterator.toString().contains("retryFile=" + liveWal));
+ assertTrue(iterator.toString().contains("retryReason="));
+ try (WALWriter writer = new WALWriter(liveWal, WALFileVersion.V3)) {
+ writer.write(searchableEntry(1L), metadata);
+ }
+ assertTrue(iterator.hasNext());
+ assertEquals(1L, iterator.next().getSearchIndex());
+ assertEquals(0, iterator.getSkippedBrokenWalFileCount());
+ }
+ } finally {
+ Files.deleteIfExists(liveWal.toPath());
+ Files.deleteIfExists(dir);
+ }
+ }
+
+ @Test
+ public void testMissingRetainedRetryFileIsReportedBeforeReplayContinues() throws Exception {
+ final Path dir = Files.createTempDirectory("progress-wal-retry-missing-file");
+ final File missingWal =
+ dir.resolve(WALFileUtils.getLogFileName(0, 0, WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+ final File successorWal =
+ dir.resolve(WALFileUtils.getLogFileName(1, 1, WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+ try {
+ assertTrue(missingWal.mkdir());
+ final WALNode walNode = mock(WALNode.class);
+ when(walNode.getLogDirectory()).thenReturn(dir.toFile());
+ when(walNode.getCurrentWALFileVersion()).thenReturn(0L);
+ when(walNode.getWALMetaDataSnapshot(0L)).thenReturn(new WALMetaData());
+ try (ProgressWALIterator iterator = new ProgressWALIterator(walNode, 1L)) {
+ assertFalse(iterator.hasNext());
+ Files.delete(missingWal.toPath());
+ final WALMetaData metadata = singleEntryMeta(19, 2L, 1L, 200L, 7, 2L);
+ try (WALWriter writer = new WALWriter(successorWal, WALFileVersion.V3)) {
+ writer.write(searchableEntry(2L), metadata);
+ }
+ when(walNode.getCurrentWALFileVersion()).thenReturn(1L);
+ when(walNode.getWALMetaDataSnapshot(1L)).thenReturn(metadata);
+ assertFalse(iterator.hasNext());
+ assertEquals(0, iterator.getSkippedBrokenWalFileCount());
+ assertTrue(iterator.hasNext());
+ assertEquals(2L, iterator.next().getSearchIndex());
+ assertEquals(1, iterator.getSkippedBrokenWalFileCount());
+ final String detail = iterator.getSkippedBrokenWalFileDetails(0);
+ assertTrue(detail.contains(missingWal.getName()));
+ assertTrue(detail.contains("fileSearchIndexRange=(0, 1]"));
+ assertFalse(iterator.hasNext());
+ }
+ } finally {
+ Files.deleteIfExists(missingWal.toPath());
+ Files.deleteIfExists(successorWal.toPath());
+ Files.deleteIfExists(dir);
+ }
+ }
+
+ @Test
+ public void testRotationDuringSnapshotCaptureDoesNotUseSuccessorMetadata() throws Exception {
+ final Path dir = Files.createTempDirectory("progress-wal-snapshot-rotation");
+ final File emptyWal =
+ dir.resolve(WALFileUtils.getLogFileName(0, 0, WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+ final File successorWal =
+ dir.resolve(WALFileUtils.getLogFileName(1, 0, WALFileStatus.CONTAINS_SEARCH_INDEX))
+ .toFile();
+ try {
+ try (WALWriter ignored = new WALWriter(emptyWal, WALFileVersion.V3)) {
+ // Empty WAL that is current when opening starts.
+ }
+ final WALMetaData metadata = singleEntryMeta(19, 1L, 1L, 100L, 7, 1L);
+ final AtomicLong currentVersion = new AtomicLong(0L);
+ final WALNode walNode = mock(WALNode.class);
+ when(walNode.getLogDirectory()).thenReturn(dir.toFile());
+ when(walNode.getCurrentWALFileVersion()).thenAnswer(ignored -> currentVersion.get());
+ when(walNode.getWALMetaDataSnapshot(0L))
+ .thenAnswer(
+ ignored -> {
+ try (WALWriter writer = new WALWriter(successorWal, WALFileVersion.V3)) {
+ writer.write(searchableEntry(1L), metadata);
+ }
+ currentVersion.set(1L);
+ return null;
+ });
+ when(walNode.getWALMetaDataSnapshot(1L)).thenReturn(metadata);
+ try (ProgressWALIterator iterator = new ProgressWALIterator(walNode, 1L)) {
+ assertFalse(iterator.hasNext());
+ assertTrue(iterator.isWaitingForReadableWal());
+ assertTrue(iterator.hasNext());
+ assertEquals(1L, iterator.next().getSearchIndex());
+ assertFalse(iterator.hasNext());
+ assertEquals(0, iterator.getSkippedBrokenWalFileCount());
+ }
+ } finally {
+ Files.deleteIfExists(emptyWal.toPath());
+ Files.deleteIfExists(successorWal.toPath());
+ Files.deleteIfExists(dir);
+ }
+ }
+
@Test
public void testLiveWalReopenReusesMetadataSnapshot() throws Exception {
final Path dir = Files.createTempDirectory("progress-wal-iterator-live-snapshot");
@@ -831,14 +1051,14 @@ public class ProgressWALIteratorTest {
final WALNode walNode = mock(WALNode.class);
when(walNode.getLogDirectory()).thenReturn(dir.toFile());
when(walNode.getCurrentWALFileVersion()).thenReturn(0L);
- when(walNode.getCurrentWALMetaDataSnapshot()).thenReturn(firstSnapshot, secondSnapshot);
+ when(walNode.getWALMetaDataSnapshot(anyLong())).thenReturn(firstSnapshot, secondSnapshot);
try (ProgressWALIterator iterator = new ProgressWALIterator(walNode, 1L)) {
assertTrue(iterator.hasNext());
assertEquals(1L, iterator.next().getSearchIndex());
assertTrue(iterator.hasNext());
assertEquals(2L, iterator.next().getSearchIndex());
- verify(walNode, times(2)).getCurrentWALMetaDataSnapshot();
+ verify(walNode, times(2)).getWALMetaDataSnapshot(anyLong());
}
} finally {
Files.deleteIfExists(liveWal.toPath());
@@ -859,7 +1079,7 @@ public class ProgressWALIteratorTest {
final WALNode walNode = mock(WALNode.class);
when(walNode.getLogDirectory()).thenReturn(dir.toFile());
when(walNode.getCurrentWALFileVersion()).thenReturn(7L);
- when(walNode.getCurrentWALMetaDataSnapshot()).thenReturn(new WALMetaData());
+ when(walNode.getWALMetaDataSnapshot(anyLong())).thenReturn(new WALMetaData());
try (ProgressWALIterator iterator = new ProgressWALIterator(walNode, Long.MIN_VALUE)) {
assertFalse(iterator.hasNext());