This is an automated email from the ASF dual-hosted git repository.
HTHou 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 9eaf5f7baf8 Add optional WAL and checkpoint format extension
interfaces (#19002)
9eaf5f7baf8 is described below
commit 9eaf5f7baf8eeb5da9f8cb0ce16ee1cef376fbb2
Author: Haonan <[email protected]>
AuthorDate: Mon Oct 26 18:32:30 2026 +0800
Add optional WAL and checkpoint format extension interfaces (#19002)
---
.../iotdb/db/i18n/StorageEngineMessages.java | 6 +
.../iotdb/db/i18n/StorageEngineMessages.java | 6 +
.../iotdb/db/storageengine/StorageEngine.java | 8 +
.../dataregion/wal/io/CheckpointReader.java | 6 +
.../dataregion/wal/io/CheckpointWriter.java | 2 +-
.../storageengine/dataregion/wal/io/LogWriter.java | 38 +-
.../dataregion/wal/io/WALFileContext.java | 56 ++
.../dataregion/wal/io/WALFileVersion.java | 17 +-
.../dataregion/wal/io/WALFormatException.java | 54 ++
.../dataregion/wal/io/WALFormatProvider.java | 51 ++
.../dataregion/wal/io/WALFormats.java | 149 +++++
.../dataregion/wal/io/WALInputStream.java | 97 ++-
.../dataregion/wal/io/WALMetaData.java | 6 +
.../storageengine/dataregion/wal/io/WALReader.java | 9 +
.../storageengine/dataregion/wal/io/WALWriter.java | 19 +-
.../dataregion/wal/recover/WALNodeRecoverTask.java | 130 +++--
.../dataregion/wal/recover/WALRecoverManager.java | 56 +-
.../dataregion/wal/recover/WALRepairWriter.java | 4 +
.../dataregion/wal/utils/WALFileUtils.java | 6 +-
.../dataregion/wal/io/WALFormatExtensionTest.java | 649 +++++++++++++++++++++
.../dataregion/wal/io/WALFormatsTest.java | 150 +++++
21 files changed, 1445 insertions(+), 74 deletions(-)
diff --git
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/StorageEngineMessages.java
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/StorageEngineMessages.java
index 25b73c30152..e669b1a4b80 100644
---
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/StorageEngineMessages.java
+++
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/StorageEngineMessages.java
@@ -20,6 +20,12 @@
package org.apache.iotdb.db.i18n;
public final class StorageEngineMessages {
+ public static final String
EXCEPTION_UNSUPPORTED_OR_UNAVAILABLE_WAL_FORMAT_4629CFE4 =
+ "Unsupported or unavailable WAL format";
+ public static final String
EXCEPTION_MULTIPLE_WAL_FORMAT_PROVIDERS_ARE_CONFIGURED_DD78877D =
+ "Multiple WAL format providers are configured";
+ public static final String EXCEPTION_INVALID_WAL_LOGICAL_POSITION_8CF34AFE =
+ "Invalid WAL logical position";
public static final String
LOG_REBUILT_UNREADABLE_WAL_FOOTER_FILE_ARG_VERSION_ARG_ORIGINALFILEBYTES_ARG_WRITERPROGRESS_ARG_1DBF836E
=
"Rebuilt unreadable WAL footer: file={}, version={},
originalFileBytes={}, writerProgress={}.";
diff --git
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/StorageEngineMessages.java
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/StorageEngineMessages.java
index 93f9b29b6f8..5f11f94560d 100644
---
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/StorageEngineMessages.java
+++
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/StorageEngineMessages.java
@@ -20,6 +20,12 @@
package org.apache.iotdb.db.i18n;
public final class StorageEngineMessages {
+ public static final String
EXCEPTION_UNSUPPORTED_OR_UNAVAILABLE_WAL_FORMAT_4629CFE4 =
+ "WAL 格式不支持或对应实现不可用";
+ public static final String
EXCEPTION_MULTIPLE_WAL_FORMAT_PROVIDERS_ARE_CONFIGURED_DD78877D =
+ "配置了多个 WAL 格式 Provider";
+ public static final String EXCEPTION_INVALID_WAL_LOGICAL_POSITION_8CF34AFE =
+ "WAL 逻辑位置无效";
public static final String
LOG_REBUILT_UNREADABLE_WAL_FOOTER_FILE_ARG_VERSION_ARG_ORIGINALFILEBYTES_ARG_WRITERPROGRESS_ARG_1DBF836E
=
"已重建不可读的 WAL
footer:file={},version={},originalFileBytes={},writerProgress={}。";
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/StorageEngine.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/StorageEngine.java
index f66a76c9f95..36bb9679f81 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/StorageEngine.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/StorageEngine.java
@@ -77,6 +77,7 @@ import
org.apache.iotdb.db.storageengine.dataregion.flush.TsFileFlushPolicy;
import
org.apache.iotdb.db.storageengine.dataregion.flush.TsFileFlushPolicy.DirectFlushPolicy;
import org.apache.iotdb.db.storageengine.dataregion.wal.WALManager;
import org.apache.iotdb.db.storageengine.dataregion.wal.exception.WALException;
+import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALFormatException;
import
org.apache.iotdb.db.storageengine.dataregion.wal.recover.WALRecoverManager;
import org.apache.iotdb.db.storageengine.load.LoadTsFilePieceNodeAssembler;
import org.apache.iotdb.db.storageengine.load.active.ActiveLoadAgent;
@@ -231,6 +232,13 @@ public class StorageEngine implements IService {
WALRecoverManager.getInstance().recover();
} catch (WALException e) {
LOGGER.error(StorageEngineMessages.FAIL_TO_RECOVER_WAL, e);
+ if (WALFormatException.isFatal(e)) {
+ cachedThreadPool.shutdownNow();
+ StartupException failure =
+ new StartupException(StorageEngineMessages.FAIL_TO_RECOVER_WAL);
+ failure.initCause(e);
+ throw failure;
+ }
}
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/CheckpointReader.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/CheckpointReader.java
index 7069781fd8b..dfd13cfd571 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/CheckpointReader.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/CheckpointReader.java
@@ -29,6 +29,7 @@ import java.io.DataInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
+import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;
@@ -53,6 +54,8 @@ public class CheckpointReader {
Checkpoint checkpoint = Checkpoint.deserialize(logStream);
checkpoints.add(checkpoint);
}
+ } catch (WALFormatException e) {
+ throw new UncheckedIOException(e);
} catch (EOFException e) {
logger.debug(
StorageEngineMessages
@@ -60,6 +63,9 @@ public class CheckpointReader {
logFile,
e.getMessage());
} catch (IOException e) {
+ if (WALFormatException.isFatal(e)) {
+ throw new UncheckedIOException(e);
+ }
logger.warn(
StorageEngineMessages
.STORAGE_LOG_MEET_ERROR_WHEN_READING_CHECKPOINT_FILE_SKIP_BROKEN_CHECKPOINTS_DADF9E9D,
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/CheckpointWriter.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/CheckpointWriter.java
index f31f2e9b846..1f8cd943360 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/CheckpointWriter.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/CheckpointWriter.java
@@ -27,6 +27,6 @@ import java.io.IOException;
/** CheckpointWriter writes the binary {@link Checkpoint} into .checkpoint
file. */
public class CheckpointWriter extends LogWriter {
public CheckpointWriter(File logFile) throws IOException {
- super(logFile, WALFileVersion.V2);
+ super(logFile, WALFormats.writeVersion(WALFileVersion.V2));
}
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/LogWriter.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/LogWriter.java
index 3652c826682..abf96cb19c3 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/LogWriter.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/LogWriter.java
@@ -41,6 +41,9 @@ public abstract class LogWriter implements ILogWriter {
protected final File logFile;
protected final FileChannel logChannel;
protected long originalSize = 0;
+ protected WALFileContext fileContext;
+ protected long blockIndex;
+ protected long dataStartOffset;
/**
* 1 byte for whether enable compression, 4 byte for compressedSize, 4 byte
for uncompressedSize
@@ -68,14 +71,17 @@ public abstract class LogWriter implements ILogWriter {
StandardOpenOption.WRITE,
StandardOpenOption.APPEND);
try {
- if (logChannel.size() == 0
+ if (version == WALFileVersion.EXTENDED) {
+ fileContext = WALFormats.create(logFile, logChannel);
+ } else if (logChannel.size() == 0
&& (version == WALFileVersion.V2 || version == WALFileVersion.V3)) {
ByteBuffer magic = ByteBuffer.wrap(version.getVersionBytes());
while (magic.hasRemaining()) {
logChannel.write(magic);
}
}
- } catch (IOException e) {
+ dataStartOffset = logChannel.position();
+ } catch (IOException | RuntimeException | Error e) {
// A full disk can fail initialization after open() succeeds. Release
the orphan channel
// before the owner retries creating the successor.
try {
@@ -83,6 +89,13 @@ public abstract class LogWriter implements ILogWriter {
} catch (IOException closeException) {
e.addSuppressed(closeException);
}
+ if (fileContext != null) {
+ try {
+ fileContext.close();
+ } catch (RuntimeException | Error closeException) {
+ e.addSuppressed(closeException);
+ }
+ }
throw e;
}
}
@@ -132,6 +145,19 @@ public abstract class LogWriter implements ILogWriter {
WritingMetrics.getInstance().recordCompressWALBufferCost(System.nanoTime() -
startTime);
}
startTime = System.nanoTime();
+ if (fileContext != null) {
+ int written =
+ fileContext.writeData(
+ logChannel,
+ blockIndex,
+ compressed ? compressionType : CompressionType.UNCOMPRESSED,
+ uncompressedSize,
+ buffer);
+ blockIndex++;
+ WritingMetrics.getInstance()
+ .recordWroteWALBuffer(uncompressedSize, written, System.nanoTime() -
startTime);
+ return ((double) written / uncompressedSize);
+ }
headerBuffer.flip();
while (headerBuffer.hasRemaining()) {
logChannel.write(headerBuffer);
@@ -183,7 +209,13 @@ public abstract class LogWriter implements ILogWriter {
logChannel.force(true);
}
} finally {
- logChannel.close();
+ try {
+ logChannel.close();
+ } finally {
+ if (fileContext != null) {
+ fileContext.close();
+ }
+ }
}
}
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFileContext.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFileContext.java
new file mode 100644
index 00000000000..8c925cf9e68
--- /dev/null
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFileContext.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iotdb.db.storageengine.dataregion.wal.io;
+
+import org.apache.tsfile.file.metadata.enums.CompressionType;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+
+/**
+ * One extension-format file. The caller owns the channel; the context owns
only format resources.
+ * Read methods return decoded segments and must distinguish an incomplete
tail (EOFException) from
+ * an unrecoverable format failure (WALFormatException).
+ */
+public interface WALFileContext extends AutoCloseable {
+ int writeData(
+ FileChannel channel,
+ long index,
+ CompressionType compression,
+ int uncompressedSize,
+ ByteBuffer data)
+ throws IOException;
+
+ /** Returns an uncompressed segment, or throws EOFException at metadata/the
incomplete tail. */
+ ByteBuffer readSegment(FileChannel channel, long endOffset, long index)
throws IOException;
+
+ /** Writes the format-specific metadata frame and trailer after the final
data segment. */
+ void seal(FileChannel channel, WALMetaData metadata, long index) throws
IOException;
+
+ /**
+ * Releases a returned segment, including any sensitive scratch state;
accepts null. The provider
+ * owns its allocation and may recycle it. The caller must not separately
free its native memory.
+ */
+ void release(ByteBuffer data);
+
+ /** Releases format resources; repeated calls must be safe. The channel
belongs to the caller. */
+ @Override
+ void close();
+}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFileVersion.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFileVersion.java
index f643c78e8dd..c51821b3fb4 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFileVersion.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFileVersion.java
@@ -19,6 +19,7 @@
package org.apache.iotdb.db.storageengine.dataregion.wal.io;
import org.apache.iotdb.commons.utils.IOUtils;
+import org.apache.iotdb.db.i18n.StorageEngineMessages;
import java.io.File;
import java.io.IOException;
@@ -29,19 +30,30 @@ import java.nio.charset.StandardCharsets;
public enum WALFileVersion {
V1("WAL"),
V2("V2-WAL"),
- V3("V3-WAL");
+ V3("V3-WAL"),
+ /** A dispatch marker only; its on-disk magic and layout belong to the
installed provider. */
+ EXTENDED(null);
private final String versionString;
private byte[] versionBytes;
public String getVersionString() {
+ requireBuiltInVersion();
return versionString;
}
public byte[] getVersionBytes() {
+ requireBuiltInVersion();
return versionBytes;
}
+ private void requireBuiltInVersion() {
+ if (this == EXTENDED) {
+ throw new IllegalStateException(
+
StorageEngineMessages.EXCEPTION_UNSUPPORTED_OR_UNAVAILABLE_WAL_FORMAT_4629CFE4);
+ }
+ }
+
WALFileVersion(String versionString) {
this.versionString = versionString;
if (versionString != null) {
@@ -73,6 +85,9 @@ public enum WALFileVersion {
return version;
}
}
+ if (WALFormats.recognizes(channel)) {
+ return EXTENDED;
+ }
// v1 by default
return V1;
} finally {
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFormatException.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFormatException.java
new file mode 100644
index 00000000000..100a68e1be4
--- /dev/null
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFormatException.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iotdb.db.storageengine.dataregion.wal.io;
+
+import org.apache.iotdb.db.i18n.StorageEngineMessages;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.Set;
+
+/** A format/policy failure that recovery must never downgrade to tail
truncation or skip. */
+public class WALFormatException extends IOException {
+ public WALFormatException(String message) {
+ super(message);
+ }
+
+ public WALFormatException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public static WALFormatException unsupported() {
+ return new WALFormatException(
+
StorageEngineMessages.EXCEPTION_UNSUPPORTED_OR_UNAVAILABLE_WAL_FORMAT_4629CFE4);
+ }
+
+ public static boolean isFatal(Throwable failure) {
+ Set<Throwable> seen = Collections.newSetFromMap(new IdentityHashMap<>());
+ for (Throwable current = failure;
+ current != null && seen.add(current);
+ current = current.getCause()) {
+ if (current instanceof WALFormatException) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFormatProvider.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFormatProvider.java
new file mode 100644
index 00000000000..17ada89c5ab
--- /dev/null
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFormatProvider.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iotdb.db.storageengine.dataregion.wal.io;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+
+/** Optional WAL/checkpoint format implementation, without prescribing
algorithms or key storage. */
+public interface WALFormatProvider {
+ /**
+ * Stable magic in the reserved {@code V<digits>-WAL} namespace, independent
of the write policy.
+ * It must not reuse a built-in version, so missing providers can be
rejected without guessing V1.
+ */
+ byte[] getMagic();
+
+ boolean isEnabled();
+
+ /** Initializes a new file, including its magic and header; does not take
channel ownership. */
+ WALFileContext create(File file, FileChannel channel) throws IOException;
+
+ /** Opens an existing extension-format file, leaving the channel at its
first segment. */
+ WALFileContext open(File file, FileChannel channel) throws IOException;
+
+ WALMetaData readMetadata(File file, FileChannel channel) throws IOException;
+
+ /** Enforces product policy before opening either a legacy or
extension-format file. */
+ default void validateRead(File file, WALFileVersion version) throws
IOException {}
+
+ /** Checks the file before replay or cleanup may have destructive side
effects. */
+ void validateForRecovery(File file) throws IOException;
+
+ /** Product-specific diagnostics, called once when recovery terminates with
a fatal failure. */
+ default void onRecoveryFailure(Throwable failure) {}
+}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFormats.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFormats.java
new file mode 100644
index 00000000000..8691e467096
--- /dev/null
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFormats.java
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iotdb.db.storageengine.dataregion.wal.io;
+
+import org.apache.iotdb.commons.utils.IOUtils;
+import org.apache.iotdb.db.i18n.StorageEngineMessages;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.ServiceLoader;
+
+/** Dispatches optional formats without exposing their binary layouts to the
standard WAL path. */
+public final class WALFormats {
+ private WALFormats() {}
+
+ private static class Holder {
+ private static final WALFormatProvider PROVIDER =
+ selectProvider(ServiceLoader.load(WALFormatProvider.class).iterator());
+ }
+
+ static WALFormatProvider selectProvider(Iterator<WALFormatProvider>
providers) {
+ if (!providers.hasNext()) {
+ return null;
+ }
+ WALFormatProvider provider = providers.next();
+ if (providers.hasNext()) {
+ throw new IllegalStateException(
+
StorageEngineMessages.EXCEPTION_MULTIPLE_WAL_FORMAT_PROVIDERS_ARE_CONFIGURED_DD78877D);
+ }
+ byte[] magic = provider.getMagic();
+ if (magic == null
+ || magic.length > 64
+ || !new String(magic, StandardCharsets.US_ASCII).matches("V[0-9]+-WAL")
+ || Arrays.equals(magic, WALFileVersion.V2.getVersionBytes())
+ || Arrays.equals(magic, WALFileVersion.V3.getVersionBytes())) {
+ throw new IllegalStateException(
+
StorageEngineMessages.EXCEPTION_UNSUPPORTED_OR_UNAVAILABLE_WAL_FORMAT_4629CFE4);
+ }
+ return provider;
+ }
+
+ public static WALFileVersion writeVersion(WALFileVersion requested) {
+ return Holder.PROVIDER != null && Holder.PROVIDER.isEnabled()
+ ? WALFileVersion.EXTENDED
+ : requested;
+ }
+
+ static boolean recognizes(FileChannel channel) throws IOException {
+ return recognizes(channel, Holder.PROVIDER);
+ }
+
+ static boolean recognizes(FileChannel channel, WALFormatProvider provider)
throws IOException {
+ long position = channel.position();
+ try {
+ if (provider != null) {
+ byte[] magic = provider.getMagic();
+ if (channel.size() >= magic.length) {
+ ByteBuffer bytes = ByteBuffer.allocate(magic.length);
+ IOUtils.readFully(channel, bytes, 0);
+ if (Arrays.equals(magic, bytes.array())) {
+ return true;
+ }
+ }
+ }
+ // Versioned magic is reserved: an unavailable future format must not
fall back to V1.
+ int size = (int) Math.min(channel.size(), 64);
+ ByteBuffer prefix = ByteBuffer.allocate(size);
+ IOUtils.readFully(channel, prefix, 0);
+ if (new String(prefix.array(),
StandardCharsets.US_ASCII).matches("(?s)^V[0-9]+-WAL.*")) {
+ throw WALFormatException.unsupported();
+ }
+ return false;
+ } finally {
+ channel.position(position);
+ }
+ }
+
+ public static WALFileContext create(File file, FileChannel channel) throws
IOException {
+ if (Holder.PROVIDER == null || !Holder.PROVIDER.isEnabled()) {
+ throw WALFormatException.unsupported();
+ }
+ return requireContext(Holder.PROVIDER.create(file, channel));
+ }
+
+ public static WALFileContext open(File file, FileChannel channel) throws
IOException {
+ if (Holder.PROVIDER == null) {
+ throw WALFormatException.unsupported();
+ }
+ return requireContext(Holder.PROVIDER.open(file, channel));
+ }
+
+ private static WALFileContext requireContext(WALFileContext context) throws
WALFormatException {
+ if (context == null) {
+ throw WALFormatException.unsupported();
+ }
+ return context;
+ }
+
+ public static void validateRead(File file, WALFileVersion version) throws
IOException {
+ if (Holder.PROVIDER != null) {
+ Holder.PROVIDER.validateRead(file, version);
+ }
+ }
+
+ public static WALMetaData readMetadata(File file, FileChannel channel)
throws IOException {
+ if (Holder.PROVIDER == null) {
+ throw WALFormatException.unsupported();
+ }
+ Holder.PROVIDER.validateRead(file, WALFileVersion.EXTENDED);
+ return Holder.PROVIDER.readMetadata(file, channel);
+ }
+
+ public static void validateForRecovery(File file) throws IOException {
+ try (FileChannel channel = FileChannel.open(file.toPath())) {
+ WALFileVersion version = WALFileVersion.getVersion(channel);
+ validateRead(file, version);
+ if (version == WALFileVersion.EXTENDED) {
+ Holder.PROVIDER.validateForRecovery(file);
+ }
+ }
+ }
+
+ public static void onRecoveryFailure(Throwable failure) {
+ if (Holder.PROVIDER != null) {
+ Holder.PROVIDER.onRecoveryFailure(failure);
+ }
+ }
+}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALInputStream.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALInputStream.java
index d4b4a8b78f9..7a512ff5a95 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALInputStream.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALInputStream.java
@@ -53,6 +53,7 @@ public class WALInputStream extends InputStream implements
AutoCloseable {
private ByteBuffer dataBuffer = null;
private ByteBuffer compressedBuffer = null;
private long currentSegmentOffset = -1;
+ private long currentSegmentBlockIndex;
private long suspendedSegmentOffset = -1;
private int suspendedSegmentPosition;
private int suspendedSegmentRemaining;
@@ -67,6 +68,9 @@ public class WALInputStream extends InputStream implements
AutoCloseable {
private long endOffset = -1;
private long logicalReadOffset;
private boolean recoveringEntries;
+ private WALFileContext fileContext;
+ private long dataStartOffset;
+ private long blockIndex;
WALFileVersion version;
@@ -81,7 +85,12 @@ public class WALInputStream extends InputStream implements
AutoCloseable {
try {
fileSize = channel.size();
analyzeFileVersion();
- if (ignoreMetadata) {
+ WALFormats.validateRead(logFile, version);
+ if (version == WALFileVersion.EXTENDED) {
+ fileContext = WALFormats.open(logFile, channel);
+ dataStartOffset = channel.position();
+ endOffset = fileSize;
+ } else if (ignoreMetadata) {
// Recovery must not trust even a plausible footer length. Stop at the
entry end marker
// or the first unreadable entry instead.
endOffset = fileSize;
@@ -89,8 +98,23 @@ public class WALInputStream extends InputStream implements
AutoCloseable {
} else {
getEndOffset();
}
- } catch (Exception e) {
- channel.close();
+ } catch (IOException | RuntimeException | Error e) {
+ try {
+ channel.close();
+ } catch (IOException closeException) {
+ e.addSuppressed(closeException);
+ }
+ if (fileContext != null) {
+ try {
+ fileContext.close();
+ } catch (RuntimeException | Error closeException) {
+ e.addSuppressed(closeException);
+ }
+ }
+ if (version == WALFileVersion.EXTENDED &&
!WALFormatException.isFatal(e)) {
+ throw new WALFormatException(
+
StorageEngineMessages.EXCEPTION_UNSUPPORTED_OR_UNAVAILABLE_WAL_FORMAT_4629CFE4,
e);
+ }
throw e;
}
}
@@ -212,9 +236,18 @@ public class WALInputStream extends InputStream implements
AutoCloseable {
try {
channel.close();
} finally {
- releaseReadBuffers();
- suspendedSegmentOffset = -1;
- suspendedSegmentRemaining = 0;
+ try {
+ releaseReadBuffers();
+ } finally {
+ try {
+ if (fileContext != null) {
+ fileContext.close();
+ }
+ } finally {
+ suspendedSegmentOffset = -1;
+ suspendedSegmentRemaining = 0;
+ }
+ }
}
}
@@ -229,7 +262,7 @@ public class WALInputStream extends InputStream implements
AutoCloseable {
suspendedSegmentPosition = dataBuffer.position();
suspendedSegmentRemaining = dataBuffer.remaining();
}
- MmapUtil.clean(dataBuffer);
+ releaseSegment();
MmapUtil.clean(compressedBuffer);
dataBuffer = null;
compressedBuffer = null;
@@ -249,13 +282,18 @@ public class WALInputStream extends InputStream
implements AutoCloseable {
private void loadNextSegment() throws IOException {
if (suspendedSegmentOffset >= 0) {
channel.position(suspendedSegmentOffset);
+ if (version == WALFileVersion.EXTENDED) {
+ blockIndex = currentSegmentBlockIndex;
+ }
}
if (channel.position() >= endOffset) {
throw new
EOFException(StorageEngineMessages.REACH_END_OFFSET_OF_WAL_FILE);
}
long startTime = System.nanoTime();
long startPosition = channel.position();
- if (version == WALFileVersion.V2 || version == WALFileVersion.V3) {
+ if (version == WALFileVersion.EXTENDED) {
+ loadNextExtendedSegment();
+ } else if (version == WALFileVersion.V2 || version == WALFileVersion.V3) {
loadNextSegmentV2();
} else if (version == WALFileVersion.V1) {
loadNextSegmentV1();
@@ -275,6 +313,30 @@ public class WALInputStream extends InputStream implements
AutoCloseable {
.recordWALRead(channel.position() - startPosition, System.nanoTime() -
startTime);
}
+ private void loadNextExtendedSegment() throws IOException {
+ releaseSegment();
+ currentSegmentOffset = channel.position();
+ currentSegmentBlockIndex = blockIndex;
+ dataBuffer = fileContext.readSegment(channel, endOffset, blockIndex);
+ if (dataBuffer == null || !dataBuffer.hasRemaining()) {
+ throw WALFormatException.unsupported();
+ }
+ blockIndex++;
+ }
+
+ private void releaseSegment() {
+ ByteBuffer segment = dataBuffer;
+ dataBuffer = null;
+ if (segment == null) {
+ return;
+ }
+ if (fileContext != null) {
+ fileContext.release(segment);
+ } else {
+ MmapUtil.clean(segment);
+ }
+ }
+
private void loadNextSegmentV1() throws IOException {
// just read raw data as input
if (channel.position() >= fileSize) {
@@ -389,6 +451,25 @@ public class WALInputStream extends InputStream implements
AutoCloseable {
releaseReadBuffers();
suspendedSegmentOffset = -1;
suspendedSegmentRemaining = 0;
+ if (version == WALFileVersion.EXTENDED) {
+ if (pos < 0) {
+ throw new WALFormatException(
+
StorageEngineMessages.EXCEPTION_INVALID_WAL_LOGICAL_POSITION_8CF34AFE);
+ }
+ channel.position(dataStartOffset);
+ blockIndex = 0;
+ logicalReadOffset = 0;
+ long remaining = pos;
+ while (true) {
+ loadNextExtendedSegment();
+ if (remaining < dataBuffer.remaining()) {
+ dataBuffer.position((int) remaining);
+ logicalReadOffset = pos;
+ return;
+ }
+ remaining -= dataBuffer.remaining();
+ }
+ }
if (version == WALFileVersion.V2 || version == WALFileVersion.V3) {
channel.position(version.getVersionBytes().length);
long posRemain = pos;
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALMetaData.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALMetaData.java
index 5d316a7508f..0cf97ba8809 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALMetaData.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALMetaData.java
@@ -412,6 +412,9 @@ public class WALMetaData implements SerializedSize {
try {
return readFromWALFileWithoutRecovery(logFile, channel);
} catch (IOException metadataFailure) {
+ if (WALFormatException.isFatal(metadataFailure)) {
+ throw metadataFailure;
+ }
logger.warn(StorageEngineMessages.FAIL_TO_READ_WAL_LOGS_SKIP, logFile,
metadataFailure);
// Keep the original file when some entries are readable. The
reconstructed metadata is an
// in-memory view of that prefix; rewriting compressed segments while
readers hold the file
@@ -457,6 +460,9 @@ public class WALMetaData implements SerializedSize {
return new WALMetaData();
}
WALFileVersion version = WALFileVersion.getVersion(channel);
+ if (version == WALFileVersion.EXTENDED) {
+ return WALFormats.readMetadata(logFile, channel);
+ }
if (channel.size() < version.getVersionBytes().length + Integer.BYTES
|| !isValidMagicString(channel, version)) {
throw new BrokenWALFileException(logFile);
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALReader.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALReader.java
index c54f3e45bc4..a214b25211c 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALReader.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALReader.java
@@ -32,6 +32,7 @@ import java.io.DataInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
+import java.io.UncheckedIOException;
import java.util.Iterator;
import java.util.NoSuchElementException;
@@ -85,16 +86,24 @@ public class WALReader implements Closeable {
nextEntry = null;
return false;
}
+ } catch (WALFormatException e) {
+ throw new UncheckedIOException(e);
} catch (EOFException e) {
fileCorrupted = true;
return false;
} catch (IOException e) {
+ if (WALFormatException.isFatal(e)) {
+ throw new UncheckedIOException(e);
+ }
fileCorrupted = true;
// log only when file should be complete
if (!fileMayCorrupt) {
logger.warn(StorageEngineMessages.FAIL_TO_READ_WAL_ENTRY_SKIP_BROKEN,
logFile, e);
}
} catch (Exception e) {
+ if (WALFormatException.isFatal(e)) {
+ throw new IllegalStateException(e);
+ }
fileCorrupted = true;
logger.warn(StorageEngineMessages.FAIL_TO_READ_WAL_ENTRY_SKIP_BROKEN,
logFile, e);
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALWriter.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALWriter.java
index d340b7ce0df..2c3a1c27acb 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALWriter.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALWriter.java
@@ -42,8 +42,8 @@ public class WALWriter extends LogWriter {
}
public WALWriter(File logFile, WALFileVersion version) throws IOException {
- super(logFile, version);
- this.version = version;
+ super(logFile, WALFormats.writeVersion(version));
+ this.version = WALFormats.writeVersion(version);
}
/**
@@ -68,7 +68,11 @@ public class WALWriter extends LogWriter {
private synchronized void endFile() throws IOException {
// The writer channel is write-only; its known version and size identify
an empty header.
- if (version != WALFileVersion.V1 && logChannel.size() ==
version.getVersionBytes().length) {
+ if (version != WALFileVersion.V1
+ && logChannel.size()
+ == (version == WALFileVersion.EXTENDED
+ ? dataStartOffset
+ : version.getVersionBytes().length)) {
// A WAL with no entries is valid and has no marker or metadata trailer
to append.
super.close();
return;
@@ -78,6 +82,10 @@ public class WALWriter extends LogWriter {
// mark info part ends
endMarker.serialize(markerBuffer);
write(markerBuffer, false);
+ if (fileContext != null) {
+ fileContext.seal(logChannel, metaData, blockIndex++);
+ return;
+ }
int metaDataSize = metaData.serializedSize(version);
ByteBuffer buffer =
@@ -100,9 +108,12 @@ public class WALWriter extends LogWriter {
@Override
public void close() throws IOException {
+ if (!logChannel.isOpen()) {
+ return;
+ }
try {
endFile();
- } catch (IOException | RuntimeException e) {
+ } catch (IOException | RuntimeException | Error e) {
// In particular, failed recovery-file sealing must release the handle
before its temporary
// file can be removed on Windows, while preserving the original failure.
try {
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/WALNodeRecoverTask.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/WALNodeRecoverTask.java
index 73d429e3ccb..180890e0738 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/WALNodeRecoverTask.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/WALNodeRecoverTask.java
@@ -33,6 +33,8 @@ import
org.apache.iotdb.db.storageengine.dataregion.wal.buffer.WALEntry;
import
org.apache.iotdb.db.storageengine.dataregion.wal.checkpoint.MemTableInfo;
import
org.apache.iotdb.db.storageengine.dataregion.wal.exception.BrokenWALFileException;
import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALByteBufReader;
+import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALFormatException;
+import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALFormats;
import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALMetaData;
import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALReader;
import
org.apache.iotdb.db.storageengine.dataregion.wal.recover.file.UnsealedTsFileRecoverPerformer;
@@ -47,6 +49,7 @@ import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
+import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.util.ArrayList;
@@ -72,7 +75,7 @@ public class WALNodeRecoverTask implements Runnable {
// version id of first valid .wal file
private long firstValidVersionId = Long.MAX_VALUE;
private Map<Long, MemTableInfo> memTableId2Info;
- private Map<Long, UnsealedTsFileRecoverPerformer>
memTableId2RecoverPerformer;
+ private Map<Long, UnsealedTsFileRecoverPerformer>
memTableId2RecoverPerformer = new HashMap<>();
public WALNodeRecoverTask(File logDirectory, CountDownLatch
allNodesRecoveredLatch) {
this.logDirectory = logDirectory;
@@ -81,6 +84,32 @@ public class WALNodeRecoverTask implements Runnable {
@Override
public void run() {
+ try {
+ validateFilesBeforeRecovery();
+ recoverNode();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ } finally {
+ allNodesRecoveredLatch.countDown();
+ }
+ }
+
+ private void validateFilesBeforeRecovery() throws IOException {
+ File[] walFiles = WALFileUtils.listAllWALFiles(logDirectory);
+ File[] checkpoints =
CheckpointFileUtils.listAllCheckpointFiles(logDirectory);
+ if (walFiles != null) {
+ for (File file : walFiles) {
+ WALFormats.validateForRecovery(file);
+ }
+ }
+ if (checkpoints != null) {
+ for (File file : checkpoints) {
+ WALFormats.validateForRecovery(file);
+ }
+ }
+ }
+
+ private void recoverNode() {
logger.info(StorageEngineMessages.START_RECOVERING_WAL_NODE_IN_DIR,
logDirectory);
// recover version id and search index
@@ -95,6 +124,9 @@ public class WALNodeRecoverTask implements Runnable {
for (UnsealedTsFileRecoverPerformer recoverPerformer :
memTableId2RecoverPerformer.values()) {
recoverPerformer.getRecoverListener().fail(e);
}
+ if (WALFormatException.isFatal(e)) {
+ throw new IllegalStateException(e);
+ }
} finally {
for (UnsealedTsFileRecoverPerformer recoverPerformer :
memTableId2RecoverPerformer.values()) {
try {
@@ -107,53 +139,49 @@ public class WALNodeRecoverTask implements Runnable {
}
}
- try {
- if
(config.getDataRegionConsensusProtocolClass().equals(ConsensusFactory.IOT_CONSENSUS))
{
- // delete checkpoint info to avoid repeated recover
- File[] checkpointFiles =
CheckpointFileUtils.listAllCheckpointFiles(logDirectory);
- for (File checkpointFile : checkpointFiles) {
- try {
- Files.delete(checkpointFile.toPath());
- } catch (IOException e) {
- logger.error(StorageEngineMessages.ERROR_DELETE_CHECKPOINT_FILE,
checkpointFile, e);
- }
- }
- // register wal node
- WALManager.getInstance()
- .registerWALNode(
- logDirectory.getName(),
- logDirectory.getAbsolutePath(),
- lastVersionId + 1,
- lastSearchIndex);
- logger.info(
- StorageEngineMessages
-
.STORAGE_LOG_SUCCESSFULLY_RECOVER_WAL_NODE_IN_THE_DIRECTORY_ADD_THIS_FA6ADE22,
- logDirectory);
- } else {
- if (cleanupRecoveredDirectory(logDirectory)) {
- logger.info(
- StorageEngineMessages
-
.STORAGE_LOG_SUCCESSFULLY_RECOVER_WAL_NODE_IN_THE_DIRECTORY_SO_DELETE_A17892D9,
- logDirectory);
+ if
(config.getDataRegionConsensusProtocolClass().equals(ConsensusFactory.IOT_CONSENSUS))
{
+ // delete checkpoint info to avoid repeated recover
+ File[] checkpointFiles =
CheckpointFileUtils.listAllCheckpointFiles(logDirectory);
+ for (File checkpointFile : checkpointFiles) {
+ try {
+ Files.delete(checkpointFile.toPath());
+ } catch (IOException e) {
+ logger.error(StorageEngineMessages.ERROR_DELETE_CHECKPOINT_FILE,
checkpointFile, e);
}
}
-
- // IoTConsensusV2 will not only delete WAL node folder, but also
register WAL node.
- if
(config.getDataRegionConsensusProtocolClass().equals(ConsensusFactory.IOT_CONSENSUS_V2))
{
- // register wal node
- WALManager.getInstance()
- .registerWALNode(
- logDirectory.getName(),
- logDirectory.getAbsolutePath(),
- lastVersionId + 1,
- lastSearchIndex);
+ // register wal node
+ WALManager.getInstance()
+ .registerWALNode(
+ logDirectory.getName(),
+ logDirectory.getAbsolutePath(),
+ lastVersionId + 1,
+ lastSearchIndex);
+ logger.info(
+ StorageEngineMessages
+
.STORAGE_LOG_SUCCESSFULLY_RECOVER_WAL_NODE_IN_THE_DIRECTORY_ADD_THIS_FA6ADE22,
+ logDirectory);
+ } else {
+ if (cleanupRecoveredDirectory(logDirectory)) {
logger.info(
StorageEngineMessages
-
.STORAGE_LOG_SUCCESSFULLY_RECOVER_WAL_NODE_IN_THE_DIRECTORY_ADD_THIS_FA6ADE22,
+
.STORAGE_LOG_SUCCESSFULLY_RECOVER_WAL_NODE_IN_THE_DIRECTORY_SO_DELETE_A17892D9,
logDirectory);
}
- } finally {
- allNodesRecoveredLatch.countDown();
+ }
+
+ // IoTConsensusV2 will not only delete WAL node folder, but also register
WAL node.
+ if
(config.getDataRegionConsensusProtocolClass().equals(ConsensusFactory.IOT_CONSENSUS_V2))
{
+ // register wal node
+ WALManager.getInstance()
+ .registerWALNode(
+ logDirectory.getName(),
+ logDirectory.getAbsolutePath(),
+ lastVersionId + 1,
+ lastSearchIndex);
+ logger.info(
+ StorageEngineMessages
+
.STORAGE_LOG_SUCCESSFULLY_RECOVER_WAL_NODE_IN_THE_DIRECTORY_ADD_THIS_FA6ADE22,
+ logDirectory);
}
}
@@ -192,6 +220,9 @@ public class WALNodeRecoverTask implements Runnable {
previousLogicalOffset = logicalOffset;
}
} catch (Exception e) {
+ if (WALFormatException.isFatal(e)) {
+ throw new IllegalStateException(e);
+ }
logger.warn(StorageEngineMessages.FAIL_TO_READ_WAL_LOGS_SKIP,
lastWALFile, e);
}
// make sure last wal file is correct
@@ -241,6 +272,9 @@ public class WALNodeRecoverTask implements Runnable {
try {
return walRepairWriter.repair(metaData);
} catch (IOException e) {
+ if (WALFormatException.isFatal(e)) {
+ throw new IllegalStateException(e);
+ }
logger.error(StorageEngineMessages.FAIL_TO_RECOVER_WAL_METADATA,
walFile, e);
DataNodeExceptionMetrics.getInstance().recordSuspiciousDiskException(e);
return false;
@@ -284,6 +318,9 @@ public class WALNodeRecoverTask implements Runnable {
recoverPerformer.startRecovery();
} catch (Exception e) {
recoverPerformer.getRecoverListener().fail(e);
+ if (WALFormatException.isFatal(e)) {
+ throw new IllegalStateException(e);
+ }
}
}
// find all valid .wal files
@@ -335,12 +372,18 @@ public class WALNodeRecoverTask implements Runnable {
logDirectory.getName(),
e);
} catch (IOException e) {
+ if (WALFormatException.isFatal(e)) {
+ throw new IllegalStateException(e);
+ }
logger.warn(
StorageEngineMessages
.STORAGE_LOG_FAIL_TO_READ_MEMTABLE_IDS_FROM_THE_WAL_FILE_OF_WAL_NODE_FBCE8D93,
walFile.getAbsoluteFile(),
e);
} catch (Exception e) {
+ if (WALFormatException.isFatal(e)) {
+ throw new IllegalStateException(e);
+ }
logger.warn(StorageEngineMessages.FAIL_TO_READ_WAL_LOGS_SKIP, walFile,
e);
}
}
@@ -355,6 +398,9 @@ public class WALNodeRecoverTask implements Runnable {
recoverPerformer.getRecoverListener().succeed();
} catch (Exception e) {
recoverPerformer.getRecoverListener().fail(e);
+ if (WALFormatException.isFatal(e)) {
+ throw new IllegalStateException(e);
+ }
}
}
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/WALRecoverManager.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/WALRecoverManager.java
index 54a45626a3d..c6ae45b497c 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/WALRecoverManager.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/WALRecoverManager.java
@@ -31,6 +31,8 @@ import org.apache.iotdb.db.exception.DataRegionException;
import org.apache.iotdb.db.exception.runtime.StorageEngineFailureException;
import org.apache.iotdb.db.i18n.StorageEngineMessages;
import
org.apache.iotdb.db.storageengine.dataregion.wal.exception.WALRecoverException;
+import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALFormatException;
+import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALFormats;
import
org.apache.iotdb.db.storageengine.dataregion.wal.recover.file.UnsealedTsFileRecoverPerformer;
import
org.apache.iotdb.db.storageengine.dataregion.wal.utils.listener.WALRecoverListener;
@@ -108,12 +110,32 @@ public class WALRecoverManager {
recoverThreadPool =
IoTDBThreadPoolFactory.newCachedThreadPool(ThreadName.WAL_RECOVER.getName());
CountDownLatch allNodesRecoveredLatch = new
CountDownLatch(walNodeDirs.size());
+ List<Future<?>> recoveries = new ArrayList<>();
for (File walNodeDir : walNodeDirs) {
- recoverThreadPool.submit(new WALNodeRecoverTask(walNodeDir,
allNodesRecoveredLatch));
+ recoveries.add(
+ recoverThreadPool.submit(new WALNodeRecoverTask(walNodeDir,
allNodesRecoveredLatch)));
}
try {
allNodesRecoveredLatch.await();
+ ExecutionException failure = null;
+ for (Future<?> recovery : recoveries) {
+ try {
+ recovery.get();
+ } catch (ExecutionException e) {
+ if (failure == null) {
+ failure = e;
+ } else if (WALFormatException.isFatal(e) &&
!WALFormatException.isFatal(failure)) {
+ e.addSuppressed(failure);
+ failure = e;
+ } else {
+ failure.addSuppressed(e);
+ }
+ }
+ }
+ if (failure != null) {
+ throw failure;
+ }
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new
WALRecoverException(StorageEngineMessages.FAIL_TO_RECOVER_WAL, e);
@@ -128,6 +150,10 @@ public class WALRecoverManager {
absolutePath2RecoverPerformer.values()) {
recoverPerformer.getRecoverListener().fail(e);
}
+ if (WALFormatException.isFatal(e)) {
+ WALFormats.onRecoveryFailure(e);
+ throw new
WALRecoverException(StorageEngineMessages.FAIL_TO_RECOVER_WAL, e);
+ }
} finally {
for (UnsealedTsFileRecoverPerformer recoverPerformer :
absolutePath2RecoverPerformer.values()) {
@@ -169,25 +195,31 @@ public class WALRecoverManager {
recoverPerformer.getTsFileAbsolutePath(),
e);
recoverPerformer.getRecoverListener().fail(e);
+ if (WALFormatException.isFatal(e)) {
+ throw e;
+ }
}
return null;
};
futures.add(recoverTsFilesThreadPool.submit(recoverTsFileTask));
}
// wait until all tasks done
- for (Future<Void> future : futures) {
- try {
- future.get();
- } catch (ExecutionException e) {
- throw new StorageEngineFailureException(
- StorageEngineMessages.STORAGE_ENGINE_FAILED_TO_RECOVER, e);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new StorageEngineFailureException(
- StorageEngineMessages.STORAGE_ENGINE_FAILED_TO_RECOVER, e);
+ try {
+ for (Future<Void> future : futures) {
+ try {
+ future.get();
+ } catch (ExecutionException e) {
+ throw new StorageEngineFailureException(
+ StorageEngineMessages.STORAGE_ENGINE_FAILED_TO_RECOVER, e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new StorageEngineFailureException(
+ StorageEngineMessages.STORAGE_ENGINE_FAILED_TO_RECOVER, e);
+ }
}
+ } finally {
+ recoverTsFilesThreadPool.shutdownNow();
}
- recoverTsFilesThreadPool.shutdown();
}
public WALRecoverListener addRecoverPerformer(UnsealedTsFileRecoverPerformer
recoverPerformer) {
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/WALRepairWriter.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/WALRepairWriter.java
index 7572eac68cb..962c4d5077f 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/WALRepairWriter.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/WALRepairWriter.java
@@ -23,6 +23,7 @@ import org.apache.iotdb.db.i18n.StorageEngineMessages;
import org.apache.iotdb.db.storageengine.dataregion.wal.buffer.WALEntryType;
import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALByteBufReader;
import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALFileVersion;
+import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALFormatException;
import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALMetaData;
import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALWriter;
@@ -146,6 +147,9 @@ public class WALRepairWriter {
WALMetaData.readFromWALFileWithoutRecovery(logFile, channel);
return true;
} catch (IOException | RuntimeException e) {
+ if (WALFormatException.isFatal(e)) {
+ throw new IllegalStateException(e);
+ }
return false;
}
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/utils/WALFileUtils.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/utils/WALFileUtils.java
index 5cff6d4b13c..abb137448b8 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/utils/WALFileUtils.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/utils/WALFileUtils.java
@@ -339,11 +339,11 @@ public class WALFileUtils {
for (final File walFile : walFiles) {
try (final FileChannel channel = FileChannel.open(walFile.toPath())) {
- if (WALFileVersion.getVersion(channel) != WALFileVersion.V3) {
+ WALFileVersion version = WALFileVersion.getVersion(channel);
+ if (version != WALFileVersion.V3 && version !=
WALFileVersion.EXTENDED) {
continue;
}
- // V3 WAL stores writer progress in the footer, so these lookups do
not deserialize entry
- // bodies.
+ // Formats with writer progress in the footer avoid deserializing
entry bodies here.
final WALMetaData metadata = WALMetaData.readFromWALFile(walFile,
channel);
final List<Long> physicalTimes = metadata.getPhysicalTimes();
final List<Short> nodeIds = metadata.getNodeIds();
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFormatExtensionTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFormatExtensionTest.java
new file mode 100644
index 00000000000..3ead148f06a
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFormatExtensionTest.java
@@ -0,0 +1,649 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iotdb.db.storageengine.dataregion.wal.io;
+
+import org.apache.iotdb.commons.concurrent.ExceptionalCountDownLatch;
+import org.apache.iotdb.commons.conf.CommonDescriptor;
+import org.apache.iotdb.commons.exception.StartupException;
+import org.apache.iotdb.consensus.ConsensusFactory;
+import org.apache.iotdb.db.conf.IoTDBConfig;
+import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.storageengine.StorageEngine;
+import org.apache.iotdb.db.storageengine.dataregion.memtable.IMemTable;
+import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource;
+import org.apache.iotdb.db.storageengine.dataregion.wal.checkpoint.Checkpoint;
+import
org.apache.iotdb.db.storageengine.dataregion.wal.checkpoint.CheckpointType;
+import
org.apache.iotdb.db.storageengine.dataregion.wal.checkpoint.MemTableInfo;
+import
org.apache.iotdb.db.storageengine.dataregion.wal.exception.WALRecoverException;
+import
org.apache.iotdb.db.storageengine.dataregion.wal.recover.WALNodeRecoverTask;
+import
org.apache.iotdb.db.storageengine.dataregion.wal.recover.WALRecoverManager;
+import
org.apache.iotdb.db.storageengine.dataregion.wal.recover.WALRepairWriter;
+import
org.apache.iotdb.db.storageengine.dataregion.wal.recover.file.UnsealedTsFileRecoverPerformer;
+import
org.apache.iotdb.db.storageengine.dataregion.wal.utils.listener.WALRecoverListener;
+import org.apache.iotdb.db.utils.MmapUtil;
+
+import io.netty.util.internal.PlatformDependent;
+import org.apache.tsfile.file.metadata.enums.CompressionType;
+import org.apache.tsfile.fileSystem.FSFactoryProducer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.ArgumentCaptor;
+
+import java.io.EOFException;
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.regex.Pattern;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/** Exercises dispatch with a test-only provider; it implements no encryption
or product format. */
+public class WALFormatExtensionTest {
+ @Rule public final TemporaryFolder folder = new TemporaryFolder();
+
+ @Test
+ public void testExtensionDispatchInIsolatedRuntime() throws Exception {
+ String serviceName = "META-INF/services/" +
WALFormatProvider.class.getName();
+ File serviceFile = new File(folder.getRoot(), serviceName);
+ Files.createDirectories(serviceFile.toPath().getParent());
+ Files.writeString(serviceFile.toPath(), StubProvider.class.getName());
+ URL service = serviceFile.toURI().toURL();
+ String[] classpath =
+ System.getProperty("surefire.test.class.path",
System.getProperty("java.class.path"))
+ .split(Pattern.quote(File.pathSeparator));
+ URL[] urls = new URL[classpath.length];
+ for (int i = 0; i < classpath.length; i++) {
+ urls[i] = new File(classpath[i]).toURI().toURL();
+ }
+ // Do not register a global test provider: downstream editions install
their own provider.
+ ClassLoader previous = Thread.currentThread().getContextClassLoader();
+ try (URLClassLoader isolated =
+ new URLClassLoader(urls, ClassLoader.getPlatformClassLoader()) {
+ @Override
+ public Enumeration<URL> getResources(String name) throws IOException
{
+ return serviceName.equals(name)
+ ? Collections.enumeration(Collections.singleton(service))
+ : super.getResources(name);
+ }
+ }) {
+ Thread.currentThread().setContextClassLoader(isolated);
+ Class<?> cases = isolated.loadClass(Cases.class.getName());
+ Object result =
+ isolated
+ .loadClass("org.junit.runner.JUnitCore")
+ .getMethod("runClasses", Class[].class)
+ .invoke(null, (Object) new Class<?>[] {cases});
+ assertTrue(
+ result.getClass().getMethod("getFailures").invoke(result).toString(),
+ (boolean)
result.getClass().getMethod("wasSuccessful").invoke(result));
+ assertEquals(16,
result.getClass().getMethod("getRunCount").invoke(result));
+ } finally {
+ Thread.currentThread().setContextClassLoader(previous);
+ }
+ }
+
+ public static class Cases {
+ @Rule public final TemporaryFolder folder = new TemporaryFolder();
+ private WALFormatProvider provider;
+ private WALFileContext context;
+
+ @Before
+ public void setUp() throws Exception {
+ provider = mock(WALFormatProvider.class);
+ context = mock(WALFileContext.class);
+ when(provider.isEnabled()).thenReturn(true);
+ when(provider.create(any(), any())).thenReturn(context);
+ when(provider.open(any(), any())).thenReturn(context);
+ StubProvider.delegate = provider;
+ }
+
+ @After
+ public void tearDown() {
+ StubProvider.delegate = null;
+ }
+
+ @Test
+ public void testInitializationFailureClosesCallerChannels() throws
Exception {
+ File file = folder.newFile();
+ WALFormatException failure = WALFormatException.unsupported();
+ when(provider.create(any(), any())).thenThrow(failure);
+ assertSame(failure, assertThrows(IOException.class, () -> new
WALWriter(file)));
+ ArgumentCaptor<FileChannel> created =
ArgumentCaptor.forClass(FileChannel.class);
+ verify(provider).create(eq(file), created.capture());
+ assertFalse(created.getValue().isOpen());
+ when(provider.open(any(), any())).thenThrow(failure);
+ assertSame(failure, assertThrows(IOException.class, () -> new
WALInputStream(file)));
+ ArgumentCaptor<FileChannel> opened =
ArgumentCaptor.forClass(FileChannel.class);
+ verify(provider).open(eq(file), opened.capture());
+ assertFalse(opened.getValue().isOpen());
+ }
+
+ @Test
+ public void testWriterDelegatesSegmentsMetadataAndClose() throws Exception
{
+ when(context.writeData(any(), anyLong(), any(), anyInt(), any()))
+ .thenAnswer(
+ invocation -> {
+ FileChannel channel = invocation.getArgument(0);
+ ByteBuffer bytes = invocation.getArgument(4);
+ int size = bytes.remaining();
+ while (bytes.hasRemaining()) {
+ channel.write(bytes);
+ }
+ return size;
+ });
+ File file = folder.newFile();
+ try (WALWriter writer = new WALWriter(file)) {
+ assertEquals(WALFileVersion.EXTENDED, WALFileVersion.getVersion(file));
+ ByteBuffer bytes = ByteBuffer.allocate(Integer.BYTES).putInt(42);
+ writer.write(bytes, false);
+ }
+ ArgumentCaptor<Long> indexes = ArgumentCaptor.forClass(Long.class);
+ verify(context, times(2))
+ .writeData(any(), indexes.capture(),
eq(CompressionType.UNCOMPRESSED), anyInt(), any());
+ assertEquals(Arrays.asList(0L, 1L), indexes.getAllValues());
+ verify(context).seal(any(), any(), eq(2L));
+ verify(context).close();
+ }
+
+ @Test
+ public void testCheckpointUsesExtensionWithoutWalMetadata() throws
Exception {
+ try (CheckpointWriter writer = new CheckpointWriter(folder.newFile())) {
+ writer.write(ByteBuffer.allocate(Long.BYTES).putLong(7), false);
+ }
+ verify(context).writeData(any(), eq(0L),
eq(CompressionType.UNCOMPRESSED), eq(8), any());
+ verify(context, never()).seal(any(), any(), anyLong());
+ verify(context).close();
+ }
+
+ @Test
+ public void testReaderResumesAndSeeksThroughProvider() throws Exception {
+ File file = extensionFile();
+ when(context.readSegment(any(), anyLong(), anyLong()))
+ .thenAnswer(
+ invocation -> {
+ if ((long) invocation.getArgument(2) != 0) {
+ throw new EOFException();
+ }
+ FileChannel channel = invocation.getArgument(0);
+ ByteBuffer bytes = ByteBuffer.allocate(4);
+ while (bytes.hasRemaining()) {
+ if (channel.read(bytes) < 0) {
+ throw new EOFException();
+ }
+ }
+ return bytes.flip();
+ });
+ doAnswer(
+ invocation -> {
+ ByteBuffer bytes = invocation.getArgument(0);
+ if (bytes != null) {
+ Arrays.fill(bytes.array(), (byte) 0);
+ }
+ return null;
+ })
+ .when(context)
+ .release(any());
+ try (WALInputStream reader = new WALInputStream(file)) {
+ assertEquals(1, reader.read());
+ reader.releaseReadBuffers();
+ assertEquals(2, reader.read());
+ reader.skipToGivenLogicalPosition(3);
+ assertEquals(4, reader.read());
+ }
+ verify(context, times(3)).readSegment(any(), anyLong(), eq(0L));
+ verify(context).close();
+ }
+
+ @Test
+ public void testFatalMetadataCannotTriggerRepairOrQuarantine() throws
Exception {
+ File file = extensionFile();
+ byte[] original = Files.readAllBytes(file.toPath());
+ WALFormatException failure = WALFormatException.unsupported();
+ when(provider.readMetadata(any(), any())).thenThrow(failure);
+ try (FileChannel channel = FileChannel.open(file.toPath())) {
+ assertSame(
+ failure,
+ assertThrows(IOException.class, () ->
WALMetaData.readFromWALFile(file, channel)));
+ }
+ IllegalStateException repairFailure =
+ assertThrows(
+ IllegalStateException.class,
+ () -> new WALRepairWriter(file).repair(new WALMetaData()));
+ assertTrue(WALFormatException.isFatal(repairFailure));
+ assertArrayEquals(original, Files.readAllBytes(file.toPath()));
+ assertFalse(new File(file + ".broken").exists());
+ verify(provider, never()).open(any(), any());
+ }
+
+ @Test
+ public void testFatalSegmentFailureIsNotSwallowedByReaders() throws
Exception {
+ File file = extensionFile();
+ when(context.readSegment(any(), anyLong(), anyLong()))
+ .thenThrow(new IOException(WALFormatException.unsupported()));
+ try (WALReader reader = new WALReader(file, true)) {
+ assertTrue(
+
WALFormatException.isFatal(assertThrows(UncheckedIOException.class,
reader::hasNext)));
+ }
+ assertTrue(
+ WALFormatException.isFatal(
+ assertThrows(UncheckedIOException.class, () -> new
CheckpointReader(file))));
+ }
+
+ @Test
+ public void testRecoveryPreflightFailureReachesCallerAndKeepsFiles()
throws Exception {
+ File walRoot = folder.newFolder("wal");
+ File node = new File(walRoot, "node");
+ assertTrue(node.mkdir());
+ File wal = new File(node, "_0-0-1.wal");
+ Files.write(wal.toPath(), StubProvider.MAGIC);
+ byte[] original = Files.readAllBytes(wal.toPath());
+
doThrow(WALFormatException.unsupported()).when(provider).validateForRecovery(any());
+ String[] previous =
CommonDescriptor.getInstance().getConfig().getWalDirs();
+ WALRecoverManager recovery = WALRecoverManager.getInstance();
+ try {
+ recovery.clear();
+ CommonDescriptor.getInstance()
+ .getConfig()
+ .setWalDirs(new String[] {walRoot.getAbsolutePath()});
+ recovery.setAllDataRegionScannedLatch(new
ExceptionalCountDownLatch(0));
+ assertTrue(
+ WALFormatException.isFatal(assertThrows(WALRecoverException.class,
recovery::recover)));
+ assertArrayEquals(original, Files.readAllBytes(wal.toPath()));
+ verify(provider).onRecoveryFailure(any());
+ } finally {
+ recovery.clear();
+ CommonDescriptor.getInstance().getConfig().setWalDirs(previous);
+ }
+ }
+
+ @Test
+ public void testOrdinaryFailureDoesNotHideLaterFatalFailure() throws
Exception {
+ assertMultipleNodeFailures(false, true);
+ }
+
+ @Test
+ public void testFatalFailureRetainsLaterOrdinaryFailure() throws Exception
{
+ assertMultipleNodeFailures(true, true);
+ }
+
+ @Test
+ public void testOrdinaryNodeFailuresKeepExistingRecoveryPolicy() throws
Exception {
+ assertMultipleNodeFailures(false, false);
+ }
+
+ private void assertMultipleNodeFailures(boolean fatalFirst, boolean
hasFatal) throws Exception {
+ File firstRoot = folder.newFolder("first-wal");
+ File secondRoot = folder.newFolder("second-wal");
+ File[] roots = {firstRoot, secondRoot};
+ IOException ordinary = new IOException("ordinary recovery failure");
+ IOException other = hasFatal ? WALFormatException.unsupported() : new
IOException("other");
+ IOException[] failures =
+ fatalFirst ? new IOException[] {other, ordinary} : new IOException[]
{ordinary, other};
+ List<File> files = new ArrayList<>();
+ for (int i = 0; i < roots.length; i++) {
+ File node = new File(roots[i], "node");
+ assertTrue(node.mkdir());
+ File wal = new File(node, "_0-0-1.wal");
+ Files.write(wal.toPath(), StubProvider.MAGIC);
+ files.add(wal);
+ doThrow(failures[i]).when(provider).validateForRecovery(wal);
+ }
+ String[] previous =
CommonDescriptor.getInstance().getConfig().getWalDirs();
+ WALRecoverManager recovery = WALRecoverManager.getInstance();
+ try {
+ recovery.clear();
+ CommonDescriptor.getInstance()
+ .getConfig()
+ .setWalDirs(new String[] {firstRoot.getAbsolutePath(),
secondRoot.getAbsolutePath()});
+ recovery.setAllDataRegionScannedLatch(new
ExceptionalCountDownLatch(0));
+ if (hasFatal) {
+ WALRecoverException thrown = assertThrows(WALRecoverException.class,
recovery::recover);
+ assertTrue(WALFormatException.isFatal(thrown));
+ Throwable primary = thrown.getCause();
+ assertSame(other, primary.getCause().getCause());
+ assertEquals(1, primary.getSuppressed().length);
+ assertSame(ordinary,
primary.getSuppressed()[0].getCause().getCause());
+ verify(provider).onRecoveryFailure(primary);
+ } else {
+ recovery.recover();
+ verify(provider, never()).onRecoveryFailure(any());
+ }
+ for (File wal : files) {
+ verify(provider).validateForRecovery(wal);
+ assertArrayEquals(StubProvider.MAGIC,
Files.readAllBytes(wal.toPath()));
+ }
+ } finally {
+ recovery.clear();
+ CommonDescriptor.getInstance().getConfig().setWalDirs(previous);
+ }
+ }
+
+ private File extensionFile() throws IOException {
+ File file = folder.newFile();
+ ByteBuffer bytes = ByteBuffer.allocate(StubProvider.MAGIC.length + 4);
+ bytes.put(StubProvider.MAGIC).put(new byte[] {1, 2, 3, 4});
+ Files.write(file.toPath(), bytes.array());
+ return file;
+ }
+
+ @Test
+ public void testProviderOwnsPooledDirectBufferAcrossPauseSeekAndClose()
throws Exception {
+ File file = extensionFile();
+ ByteBuffer pooled = ByteBuffer.allocateDirect(4);
+ long address = PlatformDependent.directBufferAddress(pooled);
+ assertTrue(address != 0);
+ when(context.readSegment(any(), anyLong(), anyLong()))
+ .thenAnswer(
+ call -> {
+ FileChannel channel = call.getArgument(0);
+ pooled.clear();
+ while (pooled.hasRemaining()) {
+ if (channel.read(pooled) < 0) {
+ throw new EOFException();
+ }
+ }
+ return pooled.flip();
+ });
+ try {
+ try (WALInputStream reader = new WALInputStream(file)) {
+ assertEquals(1, reader.read());
+ reader.releaseReadBuffers();
+ assertEquals(address, PlatformDependent.directBufferAddress(pooled));
+ assertEquals(2, reader.read());
+ reader.skipToGivenLogicalPosition(3);
+ assertEquals(address, PlatformDependent.directBufferAddress(pooled));
+ assertEquals(4, reader.read());
+ }
+ assertEquals(address, PlatformDependent.directBufferAddress(pooled));
+ verify(context, times(3)).release(pooled);
+ } finally {
+ if (PlatformDependent.directBufferAddress(pooled) != 0) {
+ MmapUtil.clean(pooled);
+ }
+ }
+ }
+
+ @Test
+ public void testFatalRepairWriteFailureKeepsOriginalFile() throws
Exception {
+ assertFatalRepairFailure(false);
+ }
+
+ @Test
+ public void testFatalRepairSealFailureKeepsOriginalFile() throws Exception
{
+ assertFatalRepairFailure(true);
+ }
+
+ private void assertFatalRepairFailure(boolean seal) throws Exception {
+ File file = extensionFile();
+ byte[] original = Files.readAllBytes(file.toPath());
+ when(provider.readMetadata(any(), any())).thenThrow(new
IOException("missing footer"));
+ when(context.writeData(any(), anyLong(), any(), anyInt(), any()))
+ .thenAnswer(
+ call -> {
+ FileChannel channel = call.getArgument(0);
+ ByteBuffer data = call.getArgument(4);
+ int size = data.remaining();
+ while (data.hasRemaining()) {
+ channel.write(data);
+ }
+ return size;
+ });
+ when(context.readSegment(any(), anyLong(), anyLong()))
+ .thenReturn(ByteBuffer.wrap(new byte[] {1, 2, 3, 4}));
+ WALFormatException failure = WALFormatException.unsupported();
+ if (seal) {
+ doThrow(failure).when(context).seal(any(), any(), anyLong());
+ } else {
+ doThrow(failure).when(context).writeData(any(), anyLong(), any(),
anyInt(), any());
+ }
+ WALMetaData metadata = new WALMetaData();
+ metadata.add(4, 0, 1);
+ Method repair =
+ WALNodeRecoverTask.class.getDeclaredMethod(
+ "repairWalFileIfBroken", File.class, WALMetaData.class);
+ repair.setAccessible(true);
+ InvocationTargetException thrown =
+ assertThrows(InvocationTargetException.class, () ->
repair.invoke(null, file, metadata));
+ assertTrue(WALFormatException.isFatal(thrown.getCause()));
+ assertArrayEquals(original, Files.readAllBytes(file.toPath()));
+ assertFalse(new File(file + ".broken").exists());
+ assertEquals(1, folder.getRoot().listFiles().length);
+ }
+
+ @Test(timeout = 10000)
+ public void testFatalReplayFailureCompletesAllTaskListeners() throws
Exception {
+ assertTaskListenersComplete(false);
+ }
+
+ @Test(timeout = 10000)
+ public void testFatalFinalizationFailureCompletesAllTaskListeners() throws
Exception {
+ assertTaskListenersComplete(true);
+ }
+
+ private void assertTaskListenersComplete(boolean failOnEnd) throws
Exception {
+ WALRecoverManager recovery = WALRecoverManager.getInstance();
+ recovery.clear();
+ File node = folder.newFolder("node");
+ List<UnsealedTsFileRecoverPerformer> performers = new ArrayList<>();
+ List<MemTableInfo> tables = new ArrayList<>();
+ WALFormatException failure = WALFormatException.unsupported();
+ try {
+ for (int i = 1; i <= 2; i++) {
+ File tsFile = new File(folder.getRoot(), "sequence/root.sg/0/0/" + i
+ "-1-0-0.tsfile");
+ UnsealedTsFileRecoverPerformer performer =
mock(UnsealedTsFileRecoverPerformer.class);
+ WALRecoverListener listener = new
WALRecoverListener(tsFile.getAbsolutePath());
+ when(performer.getTsFileResource()).thenReturn(new
TsFileResource(tsFile));
+
when(performer.getTsFileAbsolutePath()).thenReturn(tsFile.getAbsolutePath());
+ when(performer.getRecoverListener()).thenReturn(listener);
+ recovery.addRecoverPerformer(performer);
+ performers.add(performer);
+ IMemTable table = mock(IMemTable.class);
+ when(table.getMemTableId()).thenReturn((long) i);
+ when(table.getDataRegionId()).thenReturn("0");
+ tables.add(new MemTableInfo(table, tsFile.getAbsolutePath(), 0));
+ }
+ Checkpoint checkpoint = new
Checkpoint(CheckpointType.GLOBAL_MEMORY_TABLE_INFO, tables);
+ ByteBuffer bytes = ByteBuffer.allocate(Long.BYTES +
checkpoint.serializedSize());
+ bytes.putLong(2);
+ checkpoint.serialize(bytes);
+ File checkpointFile = new File(node, "_0.checkpoint");
+ ByteBuffer fileBytes = ByteBuffer.allocate(StubProvider.MAGIC.length +
bytes.position());
+ fileBytes.put(StubProvider.MAGIC).put(bytes.array());
+ Files.write(checkpointFile.toPath(), fileBytes.array());
+ when(context.readSegment(any(), anyLong(), anyLong()))
+ .thenAnswer(
+ call -> {
+ FileChannel channel = call.getArgument(0);
+ ByteBuffer data = ByteBuffer.allocate(bytes.capacity());
+ while (data.hasRemaining()) {
+ if (channel.read(data) < 0) {
+ throw new EOFException();
+ }
+ }
+ return data.flip();
+ });
+ if (failOnEnd) {
+ doThrow(new WALRecoverException(failure.getMessage(), failure))
+ .when(performers.get(0))
+ .endRecovery();
+ } else {
+ doThrow(failure).when(performers.get(0)).startRecovery();
+ }
+ CountDownLatch latch = new CountDownLatch(1);
+ assertTrue(
+ WALFormatException.isFatal(
+ assertThrows(
+ IllegalStateException.class, () -> new
WALNodeRecoverTask(node, latch).run())));
+ assertEquals(0, latch.getCount());
+ for (UnsealedTsFileRecoverPerformer performer : performers) {
+ assertEquals(
+ WALRecoverListener.Status.FAILURE,
performer.getRecoverListener().waitForResult());
+ verify(performer).close();
+ }
+ assertTrue(checkpointFile.exists());
+ } finally {
+ recovery.clear();
+ }
+ }
+
+ @Test
+ public void testFatalWalFormatPreventsStorageEngineReadiness() throws
Exception {
+ File walRoot = folder.newFolder("startup-wal");
+ File node = new File(walRoot, "node");
+ assertTrue(node.mkdir());
+ Files.writeString(new File(node, "_0-0-1.wal").toPath(), "V99-WAL");
+ String[] previousDirs =
CommonDescriptor.getInstance().getConfig().getWalDirs();
+ IoTDBConfig config = IoTDBDescriptor.getInstance().getConfig();
+ String previousConsensus = config.getDataRegionConsensusProtocolClass();
+ WALRecoverManager recovery = WALRecoverManager.getInstance();
+ StorageEngine engine = mock(StorageEngine.class, CALLS_REAL_METHODS);
+ setField(engine, "fsFactory", FSFactoryProducer.getFSFactory());
+ setField(engine, "isReadyForReadAndWrite", new AtomicBoolean(true));
+ setField(engine, "isReadyForNonReadWriteFunctions", new
AtomicBoolean(true));
+ doReturn(Collections.emptyMap()).when(engine).getLocalDataRegionInfo();
+ try {
+ recovery.clear();
+ CommonDescriptor.getInstance().getConfig().setWalDirs(new String[]
{walRoot.getPath()});
+
config.setDataRegionConsensusProtocolClass(ConsensusFactory.SIMPLE_CONSENSUS);
+ Method recover =
StorageEngine.class.getDeclaredMethod("asyncRecoverDataRegion");
+ recover.setAccessible(true);
+ InvocationTargetException thrown =
+ assertThrows(InvocationTargetException.class, () ->
recover.invoke(engine));
+ assertTrue(thrown.getCause() instanceof StartupException);
+ assertTrue(WALFormatException.isFatal(thrown.getCause()));
+ assertFalse(engine.isReadyForReadAndWrite());
+ assertFalse(engine.isReadyForNonReadWriteFunctions());
+ } finally {
+ Field pool = StorageEngine.class.getDeclaredField("cachedThreadPool");
+ pool.setAccessible(true);
+ ExecutorService executor = (ExecutorService) pool.get(engine);
+ if (executor != null) {
+ executor.shutdownNow();
+ }
+ recovery.clear();
+ CommonDescriptor.getInstance().getConfig().setWalDirs(previousDirs);
+ config.setDataRegionConsensusProtocolClass(previousConsensus);
+ }
+ }
+
+ private static void setField(Object target, String name, Object value)
throws Exception {
+ Field field = StorageEngine.class.getDeclaredField(name);
+ field.setAccessible(true);
+ field.set(target, value);
+ }
+ }
+
+ /** Registered only inside the isolated test runtime, never in a
distribution. */
+ public static final class StubProvider implements WALFormatProvider {
+ private static final byte[] MAGIC =
"V88-WAL".getBytes(StandardCharsets.US_ASCII);
+ private static volatile WALFormatProvider delegate;
+
+ @Override
+ public byte[] getMagic() {
+ return MAGIC.clone();
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return delegate != null && delegate.isEnabled();
+ }
+
+ private WALFormatProvider implementation() throws IOException {
+ if (delegate == null) {
+ throw WALFormatException.unsupported();
+ }
+ return delegate;
+ }
+
+ @Override
+ public WALFileContext create(File file, FileChannel channel) throws
IOException {
+ ByteBuffer magic = ByteBuffer.wrap(MAGIC);
+ while (magic.hasRemaining()) {
+ channel.write(magic);
+ }
+ return implementation().create(file, channel);
+ }
+
+ @Override
+ public WALFileContext open(File file, FileChannel channel) throws
IOException {
+ channel.position(MAGIC.length);
+ return implementation().open(file, channel);
+ }
+
+ @Override
+ public WALMetaData readMetadata(File file, FileChannel channel) throws
IOException {
+ return implementation().readMetadata(file, channel);
+ }
+
+ @Override
+ public void validateRead(File file, WALFileVersion version) throws
IOException {
+ if (delegate != null) {
+ delegate.validateRead(file, version);
+ }
+ }
+
+ @Override
+ public void validateForRecovery(File file) throws IOException {
+ implementation().validateForRecovery(file);
+ }
+
+ @Override
+ public void onRecoveryFailure(Throwable failure) {
+ delegate.onRecoveryFailure(failure);
+ }
+ }
+}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFormatsTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFormatsTest.java
new file mode 100644
index 00000000000..cd36779ae90
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFormatsTest.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iotdb.db.storageengine.dataregion.wal.io;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.InOrder;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class WALFormatsTest {
+ @Rule public final TemporaryFolder folder = new TemporaryFolder();
+
+ @Test
+ public void testProviderIsOptionalAndUnambiguous() {
+ assertNull(WALFormats.selectProvider(Collections.emptyIterator()));
+ WALFormatProvider provider = mock(WALFormatProvider.class);
+
when(provider.getMagic()).thenReturn("V88-WAL".getBytes(StandardCharsets.US_ASCII));
+ assertSame(provider,
WALFormats.selectProvider(Collections.singleton(provider).iterator()));
+ assertThrows(
+ IllegalStateException.class,
+ () -> WALFormats.selectProvider(Arrays.asList(provider,
provider).iterator()));
+ }
+
+ @Test
+ public void testProviderMagicIsBounded() {
+ for (byte[] magic :
+ new byte[][] {
+ null, new byte[0], new byte[65], new byte[] {1, 2, 3},
WALFileVersion.V3.getVersionBytes()
+ }) {
+ WALFormatProvider provider = mock(WALFormatProvider.class);
+ when(provider.getMagic()).thenReturn(magic);
+ assertThrows(
+ IllegalStateException.class,
+ () ->
WALFormats.selectProvider(Collections.singleton(provider).iterator()));
+ }
+ }
+
+ @Test
+ public void testUnknownVersionIsNotGuessedAsLegacy() throws Exception {
+ File file = folder.newFile();
+ Files.write(file.toPath(),
"V77-WALpayload".getBytes(StandardCharsets.US_ASCII));
+ try (FileChannel channel = FileChannel.open(file.toPath())) {
+ channel.position(2);
+ assertThrows(WALFormatException.class, () ->
WALFileVersion.getVersion(channel));
+ assertEquals(2, channel.position());
+ assertThrows(WALFormatException.class, () ->
WALFormats.recognizes(channel, null));
+ assertEquals(2, channel.position());
+ }
+ assertThrows(WALFormatException.class, () -> new WALInputStream(file));
+ }
+
+ @Test
+ public void testRecognitionDoesNotDependOnWritePolicy() throws Exception {
+ WALFormatProvider provider = mock(WALFormatProvider.class);
+ byte[] magic = "V88-WAL".getBytes(StandardCharsets.US_ASCII);
+ when(provider.getMagic()).thenReturn(magic);
+ when(provider.isEnabled()).thenReturn(false);
+ File file = folder.newFile();
+ Files.write(file.toPath(), magic);
+ try (FileChannel channel = FileChannel.open(file.toPath())) {
+ channel.position(1);
+ assertTrue(WALFormats.recognizes(channel, provider));
+ assertEquals(1, channel.position());
+ }
+ }
+
+ @Test
+ public void testPlainAndEmptyFilesDoNotRequireProvider() throws Exception {
+ File file = folder.newFile();
+ try (FileChannel channel = FileChannel.open(file.toPath())) {
+ assertFalse(WALFormats.recognizes(channel, null));
+ assertEquals(WALFileVersion.V1, WALFileVersion.getVersion(channel));
+ }
+ Files.write(file.toPath(), new byte[] {0, 1, 2});
+ try (FileChannel channel = FileChannel.open(file.toPath())) {
+ assertFalse(WALFormats.recognizes(channel, null));
+ assertEquals(WALFileVersion.V1, WALFileVersion.getVersion(channel));
+ }
+ Files.write(file.toPath(), WALFileVersion.V3.getVersionBytes());
+ try (FileChannel channel = FileChannel.open(file.toPath())) {
+ assertEquals(WALFileVersion.V3, WALFileVersion.getVersion(channel));
+ assertTrue(WALFileVersion.isEmptyOrHeaderOnly(channel));
+ }
+ }
+
+ @Test
+ public void testReaderReleasesBufferBeforeClosingFormatContext() throws
Exception {
+ File file = folder.newFile();
+ WALInputStream reader = new WALInputStream(file);
+ WALFileContext context = mock(WALFileContext.class);
+ ByteBuffer data = ByteBuffer.wrap(new byte[] {1, 2, 3});
+ Field contextField = WALInputStream.class.getDeclaredField("fileContext");
+ contextField.setAccessible(true);
+ contextField.set(reader, context);
+ Field dataField = WALInputStream.class.getDeclaredField("dataBuffer");
+ dataField.setAccessible(true);
+ dataField.set(reader, data);
+ reader.close();
+ InOrder order = inOrder(context);
+ order.verify(context).release(data);
+ order.verify(context).close();
+ }
+
+ @Test
+ public void testUnrecoverableFailureSurvivesWrappingWithoutLoopingOnCauses()
{
+ WALFormatException fatal = WALFormatException.unsupported();
+ assertTrue(WALFormatException.isFatal(new IllegalStateException(new
IOException(fatal))));
+ IOException first = new IOException();
+ IOException second = new IOException(first);
+ first.initCause(second);
+ assertFalse(WALFormatException.isFatal(first));
+ assertFalse(WALFormatException.isFatal(new IOException()));
+ }
+}