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 11470dafb6f Fix WAL roll recovery after closed channel (#18653)
11470dafb6f is described below

commit 11470dafb6f25eb02f2d64ab96b7cad2845aca8d
Author: Jiang Tian <[email protected]>
AuthorDate: Thu Sep 17 09:31:10 2026 +0800

    Fix WAL roll recovery after closed channel (#18653)
---
 .../dataregion/wal/buffer/AbstractWALBuffer.java   |  39 ++-
 .../dataregion/wal/buffer/WALBuffer.java           |  81 +++++-
 .../storageengine/dataregion/wal/io/LogWriter.java |  39 +--
 .../storageengine/dataregion/wal/io/WALWriter.java |   5 +-
 .../wal/buffer/WALBufferRollRecoveryTest.java      | 300 +++++++++++++++++++++
 .../dataregion/wal/io/WALFileTest.java             |  15 ++
 6 files changed, 444 insertions(+), 35 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/AbstractWALBuffer.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/AbstractWALBuffer.java
index a1f27ff62f2..a036ee3a508 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/AbstractWALBuffer.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/AbstractWALBuffer.java
@@ -31,6 +31,7 @@ import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.Files;
 import java.nio.file.StandardCopyOption;
 import java.util.Arrays;
@@ -56,6 +57,12 @@ public abstract class AbstractWALBuffer implements 
IWALBuffer {
   @SuppressWarnings("squid:S3077")
   protected volatile WALWriter currentWALFileWriter;
 
+  // Only the sync thread accesses this state. Once sealed, the old WAL must 
never be written or
+  // closed again, even if renaming it or creating its successor fails because 
the disk is full.
+  private File pendingRollFile;
+  private WALFileStatus pendingRollStatus;
+  private long pendingRollSearchIndex;
+
   protected AbstractWALBuffer(
       String identifier, String logDirectory, long startFileVersion, long 
startSearchIndex)
       throws IOException {
@@ -99,18 +106,22 @@ public abstract class AbstractWALBuffer implements 
IWALBuffer {
    * @throws IOException If failing to close or open the log writer
    */
   protected File rollLogWriter(long searchIndex, WALFileStatus fileStatus) 
throws IOException {
-    // close file
-    currentWALFileWriter.close();
-    addDiskUsage(currentWALFileWriter.size());
-    addFileNum(1);
-    File lastFile = currentWALFileWriter.getLogFile();
+    if (!hasPendingRoll()) {
+      // Record the boundary only after sealing and forcing the old WAL have 
both succeeded.
+      currentWALFileWriter.close();
+      pendingRollFile = currentWALFileWriter.getLogFile();
+      pendingRollStatus = fileStatus;
+      pendingRollSearchIndex = searchIndex;
+      addDiskUsage(currentWALFileWriter.size());
+    }
+    File lastFile = pendingRollFile;
     String lastName = lastFile.getName();
-    if (WALFileUtils.parseStatusCode(lastName) != fileStatus) {
+    if (WALFileUtils.parseStatusCode(lastName) != pendingRollStatus) {
       String targetName =
           WALFileUtils.getLogFileName(
               WALFileUtils.parseVersionId(lastName),
               WALFileUtils.parseStartSearchIndex(lastName),
-              fileStatus);
+              pendingRollStatus);
       File targetFile = SystemFileFactory.INSTANCE.getFile(logDirectory, 
targetName);
       Files.move(
           lastFile.toPath(),
@@ -118,6 +129,7 @@ public abstract class AbstractWALBuffer implements 
IWALBuffer {
           StandardCopyOption.REPLACE_EXISTING,
           StandardCopyOption.ATOMIC_MOVE);
       lastFile = targetFile;
+      pendingRollFile = targetFile;
     }
     // roll file
     long nextFileVersion = currentWALFileVersion + 1;
@@ -125,13 +137,24 @@ public abstract class AbstractWALBuffer implements 
IWALBuffer {
         SystemFileFactory.INSTANCE.getFile(
             logDirectory,
             WALFileUtils.getLogFileName(
-                nextFileVersion, searchIndex, 
WALFileStatus.CONTAINS_SEARCH_INDEX));
+                nextFileVersion, pendingRollSearchIndex, 
WALFileStatus.CONTAINS_SEARCH_INDEX));
+    // A failed header write may leave a partial successor. Do not append to 
it on retry, or
+    // overwrite an unexpected existing WAL; either requires recovery rather 
than online rotation.
+    if (nextLogFile.length() > 0) {
+      throw new FileAlreadyExistsException(nextLogFile.toString());
+    }
     currentWALFileWriter = new WALWriter(nextLogFile);
     currentWALFileVersion = nextFileVersion;
+    addFileNum(1);
+    pendingRollFile = null;
     logger.debug(StorageEngineMessages.OPEN_NEW_WAL_FILE_FOR_BUFFER, 
nextLogFile, identifier);
     return lastFile;
   }
 
+  protected boolean hasPendingRoll() {
+    return pendingRollFile != null;
+  }
+
   public long getDiskUsage() {
     return diskUsage;
   }
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 d67cd88a4c4..5cee2ff2675 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
@@ -126,6 +126,12 @@ public class WALBuffer extends AbstractWALBuffer {
   private final Map<Long, Set<Long>> memTableIdsOfWal = new 
ConcurrentHashMap<>();
   private final BiConsumer<File, File> walFileRolledListener;
 
+  // An entry may span several sync tasks. Never acknowledge its final chunk 
after an earlier
+  // chunk failed. Failures before writing a pending successor can be cleared 
at the batch boundary;
+  // failures while writing the active WAL require recovery because its record 
boundary is unknown.
+  private Exception syncFailure;
+  private boolean retryAfterFailedBatch;
+
   public WALBuffer(String identifier, String logDirectory) throws IOException {
     this(
         identifier,
@@ -585,6 +591,45 @@ public class WALBuffer extends AbstractWALBuffer {
     public void run() {
       final long startTime = System.nanoTime();
 
+      if (syncFailure != null) {
+        failListeners(syncFailure);
+        // SET SYSTEM TO RUNNING does not repair a failed batch or an unknown 
record boundary.
+        if (CommonDescriptor.getInstance().getConfig().isRunning()) {
+          
CommonDescriptor.getInstance().getConfig().handleUnrecoverableError();
+        }
+        if (forceFlag && retryAfterFailedBatch) {
+          syncFailure = null;
+        }
+        switchSyncingBufferToIdle();
+        return;
+      }
+
+      boolean resumedRoll = false;
+      final boolean hasData = syncingBuffer.position() > 0;
+      try {
+        if (hasPendingRoll()) {
+          // The previous task sealed the old file. Finish opening its 
successor before touching
+          // this buffer, so no bytes or metadata can be appended after the 
old WAL's end marker.
+          rollLogWriter(searchIndex, fileStatus);
+          resumedRoll = true;
+        }
+      } catch (IOException e) {
+        logger.error(
+            StorageEngineMessages
+                
.STORAGE_LOG_FAIL_TO_ROLL_WAL_NODE_S_LOG_WRITER_CHANGE_SYSTEM_MODE_TO_A384AA54,
+            identifier,
+            e);
+        if (!forceFlag) {
+          syncFailure = e;
+          retryAfterFailedBatch = true;
+        }
+        failListeners(e);
+        
DataNodeExceptionMetrics.getInstance().recordSuspiciousDiskException(e);
+        CommonDescriptor.getInstance().getConfig().handleUnrecoverableError();
+        switchSyncingBufferToIdle();
+        return;
+      }
+
       makeMemTableCheckpoints();
 
       long walFileVersionId = currentWALFileVersion;
@@ -610,7 +655,11 @@ public class WALBuffer extends AbstractWALBuffer {
                 
.STORAGE_LOG_FAIL_TO_SYNC_WAL_NODE_S_BUFFER_CHANGE_SYSTEM_MODE_TO_ERROR_8C379D57,
             identifier,
             e);
+        syncFailure = e instanceof Exception exception ? exception : new 
IOException(e);
+        retryAfterFailedBatch = false;
+        failListeners(syncFailure);
         CommonDescriptor.getInstance().getConfig().handleUnrecoverableError();
+        return;
       } finally {
         switchSyncingBufferToIdle();
       }
@@ -623,24 +672,24 @@ public class WALBuffer extends AbstractWALBuffer {
 
       boolean forceSuccess = false;
       // try to roll log writer
-      if (info.rollWALFileWriterListener != null
+      if ((info.rollWALFileWriterListener != null && (!resumedRoll || hasData))
           // TODO: Control the wal file by the number of WALEntry
           || (forceFlag
               && currentWALFileWriter.originalSize() >= 
config.getWalFileSizeThresholdInByte())) {
         try {
           rollLogWriter(searchIndex, currentWALFileWriter.getWalFileStatus());
           forceSuccess = true;
-          if (info.rollWALFileWriterListener != null) {
-            info.rollWALFileWriterListener.succeed();
-          }
         } catch (IOException e) {
           logger.error(
               StorageEngineMessages
                   
.STORAGE_LOG_FAIL_TO_ROLL_WAL_NODE_S_LOG_WRITER_CHANGE_SYSTEM_MODE_TO_A384AA54,
               identifier,
               e);
-          if (info.rollWALFileWriterListener != null) {
-            info.rollWALFileWriterListener.fail(e);
+          failListeners(e);
+          if (!hasPendingRoll()) {
+            // A failed seal has no known durable boundary from which to 
resume rotation.
+            syncFailure = e;
+            retryAfterFailedBatch = false;
           }
           
DataNodeExceptionMetrics.getInstance().recordSuspiciousDiskException(e);
           
CommonDescriptor.getInstance().getConfig().handleUnrecoverableError();
@@ -657,15 +706,18 @@ public class WALBuffer extends AbstractWALBuffer {
               identifier,
               e);
           
DataNodeExceptionMetrics.getInstance().recordSuspiciousDiskException(e);
-          for (WALFlushListener fsyncListener : info.fsyncListeners) {
-            fsyncListener.fail(e);
-          }
+          failListeners(e);
+          syncFailure = e;
+          retryAfterFailedBatch = false;
           
CommonDescriptor.getInstance().getConfig().handleUnrecoverableError();
         }
       }
 
       // notify all waiting listeners
       if (forceSuccess) {
+        if (info.rollWALFileWriterListener != null) {
+          info.rollWALFileWriterListener.succeed();
+        }
         for (WALFlushListener fsyncListener : info.fsyncListeners) {
           fsyncListener.succeed();
         }
@@ -675,6 +727,15 @@ public class WALBuffer extends AbstractWALBuffer {
       WRITING_METRICS.recordSyncWALBufferCost(System.nanoTime() - startTime, 
forceFlag);
     }
 
+    private void failListeners(Exception e) {
+      if (info.rollWALFileWriterListener != null) {
+        info.rollWALFileWriterListener.fail(e);
+      }
+      for (WALFlushListener fsyncListener : info.fsyncListeners) {
+        fsyncListener.fail(e);
+      }
+    }
+
     private void makeMemTableCheckpoints() {
       if (info.checkpoints.isEmpty()) {
         return;
@@ -767,7 +828,7 @@ public class WALBuffer extends AbstractWALBuffer {
       shutdownThread(syncBufferThread, ThreadName.WAL_SYNC);
     }
 
-    if (currentWALFileWriter != null) {
+    if (currentWALFileWriter != null && !hasPendingRoll()) {
       try {
         currentWALFileWriter.close();
       } catch (IOException e) {
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 52675dae37e..3652c826682 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
@@ -20,20 +20,16 @@
 package org.apache.iotdb.db.storageengine.dataregion.wal.io;
 
 import org.apache.iotdb.db.conf.IoTDBDescriptor;
-import org.apache.iotdb.db.i18n.StorageEngineMessages;
 import org.apache.iotdb.db.service.metrics.WritingMetrics;
 import org.apache.iotdb.db.storageengine.dataregion.wal.buffer.WALEntry;
 import org.apache.iotdb.db.storageengine.dataregion.wal.checkpoint.Checkpoint;
 
 import org.apache.tsfile.compress.ICompressor;
 import org.apache.tsfile.file.metadata.enums.CompressionType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.nio.channels.ClosedChannelException;
 import java.nio.channels.FileChannel;
 import java.nio.file.StandardOpenOption;
 
@@ -42,8 +38,6 @@ import java.nio.file.StandardOpenOption;
  * and writing {@link Checkpoint} into .checkpoint file.
  */
 public abstract class LogWriter implements ILogWriter {
-  private static final Logger logger = 
LoggerFactory.getLogger(LogWriter.class);
-
   protected final File logFile;
   protected final FileChannel logChannel;
   protected long originalSize = 0;
@@ -73,9 +67,23 @@ public abstract class LogWriter implements ILogWriter {
             StandardOpenOption.CREATE,
             StandardOpenOption.WRITE,
             StandardOpenOption.APPEND);
-    if ((!logFile.exists() || logFile.length() == 0)
-        && (version == WALFileVersion.V2 || version == WALFileVersion.V3)) {
-      this.logChannel.write(ByteBuffer.wrap(version.getVersionBytes()));
+    try {
+      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) {
+      // A full disk can fail initialization after open() succeeds. Release 
the orphan channel
+      // before the owner retries creating the successor.
+      try {
+        logChannel.close();
+      } catch (IOException closeException) {
+        e.addSuppressed(closeException);
+      }
+      throw e;
     }
   }
 
@@ -124,12 +132,12 @@ public abstract class LogWriter implements ILogWriter {
       
WritingMetrics.getInstance().recordCompressWALBufferCost(System.nanoTime() - 
startTime);
     }
     startTime = System.nanoTime();
-    try {
-      headerBuffer.flip();
+    headerBuffer.flip();
+    while (headerBuffer.hasRemaining()) {
       logChannel.write(headerBuffer);
+    }
+    while (buffer.hasRemaining()) {
       logChannel.write(buffer);
-    } catch (ClosedChannelException e) {
-      logger.warn(StorageEngineMessages.CANNOT_WRITE_TO, logFile, e);
     }
     WritingMetrics.getInstance()
         .recordWroteWALBuffer(uncompressedSize, bufferSize, System.nanoTime() 
- startTime);
@@ -149,9 +157,8 @@ public abstract class LogWriter implements ILogWriter {
 
   @Override
   public void force(boolean metaData) throws IOException {
-    if (logChannel != null && logChannel.isOpen()) {
-      logChannel.force(metaData);
-    }
+    // A closed channel is a failed durability operation, not a successful 
no-op.
+    logChannel.force(metaData);
   }
 
   @Override
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 10d164f3851..37e32d85beb 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
@@ -90,7 +90,10 @@ public class WALWriter extends LogWriter {
 
   private void writeMetadata(ByteBuffer buffer) throws IOException {
     buffer.flip();
-    logChannel.write(buffer);
+    // A successful seal is the recovery boundary for switching to the next 
WAL file.
+    while (buffer.hasRemaining()) {
+      logChannel.write(buffer);
+    }
   }
 
   @Override
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
new file mode 100644
index 00000000000..ab081fa1387
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/wal/buffer/WALBufferRollRecoveryTest.java
@@ -0,0 +1,300 @@
+/*
+ * 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.buffer;
+
+import org.apache.iotdb.commons.cluster.NodeStatus;
+import org.apache.iotdb.commons.conf.CommonConfig;
+import org.apache.iotdb.commons.conf.CommonDescriptor;
+import org.apache.iotdb.commons.path.PartialPath;
+import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeId;
+import 
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode;
+import 
org.apache.iotdb.db.storageengine.dataregion.wal.checkpoint.CheckpointManager;
+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.io.WALWriter;
+import org.apache.iotdb.db.storageengine.dataregion.wal.utils.WALFileStatus;
+import org.apache.iotdb.db.storageengine.dataregion.wal.utils.WALFileUtils;
+import 
org.apache.iotdb.db.storageengine.dataregion.wal.utils.listener.AbstractResultListener.Status;
+
+import org.apache.tsfile.common.conf.TSFileConfig;
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.utils.Binary;
+import org.apache.tsfile.write.schema.MeasurementSchema;
+import org.awaitility.Awaitility;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+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.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+public class WALBufferRollRecoveryTest {
+  @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+  private final CommonConfig commonConfig = 
CommonDescriptor.getInstance().getConfig();
+  private final List<File> sealedFiles = new CopyOnWriteArrayList<>();
+  private NodeStatus previousStatus;
+  private String previousReason;
+  private File directory;
+  private WALBuffer buffer;
+  private WALWriter firstWriter;
+
+  @Before
+  public void setUp() throws Exception {
+    previousStatus = commonConfig.getNodeStatus();
+    previousReason = commonConfig.getStatusReason();
+    commonConfig.setNodeStatus(NodeStatus.Running);
+    directory = temporaryFolder.newFolder("wal");
+    buffer =
+        new WALBuffer(
+            "roll-recovery",
+            directory.getPath(),
+            new CheckpointManager("roll-recovery", directory.getPath()),
+            0,
+            0,
+            (sealedFile, currentFile) -> sealedFiles.add(sealedFile));
+    firstWriter = spy(buffer.currentWALFileWriter);
+    buffer.currentWALFileWriter = firstWriter;
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    try {
+      // Tests may inject a failure before close(); restore cleanup without 
reopening any file.
+      doCallRealMethod().when(firstWriter).close();
+      buffer.close();
+    } finally {
+      commonConfig.setNodeStatus(previousStatus);
+      commonConfig.setStatusReason(previousReason);
+    }
+  }
+
+  /**
+   * Failed successor creation must not reseal the old WAL, inflate counters, 
or lose later writes.
+   */
+  @Test
+  public void testResumeBeforeWritingAfterRepeatedOpenFailures() throws 
Exception {
+    writeAndAwait(entry(1, 1, "before"), Status.SUCCESS);
+    File successor = walFile(1, 1, WALFileStatus.CONTAINS_SEARCH_INDEX);
+    blockPath(successor);
+    roll(Status.FAILURE);
+    awaitReadOnly();
+    byte[] sealedBytes = Files.readAllBytes(firstWriter.getLogFile().toPath());
+    long diskUsage = buffer.getDiskUsage();
+    roll(Status.FAILURE);
+    assertEquals(0, buffer.getCurrentWALFileVersion());
+    assertEquals(1, buffer.getFileNum());
+    assertEquals(diskUsage, buffer.getDiskUsage());
+    assertTrue(sealedFiles.isEmpty());
+    verify(firstWriter, times(1)).close();
+
+    unblockPath(successor);
+    commonConfig.setNodeStatus(NodeStatus.Running);
+    writeAndAwait(entry(2, 2, "after"), Status.SUCCESS);
+    assertEquals(NodeStatus.Running, commonConfig.getNodeStatus());
+    assertEquals(1, buffer.getCurrentWALFileVersion());
+    assertEquals(successor, buffer.currentWALFileWriter.getLogFile());
+    assertEquals(2, buffer.getFileNum());
+    assertEquals(diskUsage, buffer.getDiskUsage());
+    assertEquals(Arrays.asList(firstWriter.getLogFile()), sealedFiles);
+    assertArrayEquals(sealedBytes, 
Files.readAllBytes(firstWriter.getLogFile().toPath()));
+    assertEquals(Arrays.asList(1L), readTimes(firstWriter.getLogFile()));
+    roll(Status.SUCCESS);
+    assertEquals(Arrays.asList(2L), readTimes(successor));
+  }
+
+  /** A retry containing only the roll signal should open exactly one 
successor. */
+  @Test
+  public void testRollSignalResumesWithoutRollingTwice() throws Exception {
+    writeAndAwait(entry(1, 1, "before"), Status.SUCCESS);
+    File successor = walFile(1, 1, WALFileStatus.CONTAINS_SEARCH_INDEX);
+    blockPath(successor);
+    roll(Status.FAILURE);
+    unblockPath(successor);
+    roll(Status.SUCCESS);
+    assertEquals(1, buffer.getCurrentWALFileVersion());
+    assertEquals(2, buffer.getFileNum());
+    assertEquals(1, sealedFiles.size());
+    verify(firstWriter, times(1)).close();
+  }
+
+  /** Failed rename and then failed open must each resume at the saved stage 
and notify once. */
+  @Test
+  public void testResumeRenameThenOpen() throws Exception {
+    writeAndAwait(entry(1, -1, "unindexed"), Status.SUCCESS);
+    File renamed = walFile(0, 0, WALFileStatus.CONTAINS_NONE_SEARCH_INDEX);
+    File successor = walFile(1, 0, WALFileStatus.CONTAINS_SEARCH_INDEX);
+    blockPath(renamed);
+    roll(Status.FAILURE);
+    long diskUsage = buffer.getDiskUsage();
+    unblockPath(renamed);
+    blockPath(successor);
+    roll(Status.FAILURE);
+    assertFalse(firstWriter.getLogFile().exists());
+    byte[] sealedBytes = Files.readAllBytes(renamed.toPath());
+    unblockPath(successor);
+    roll(Status.SUCCESS);
+    assertEquals(Arrays.asList(renamed), sealedFiles);
+    assertArrayEquals(sealedBytes, Files.readAllBytes(renamed.toPath()));
+    assertEquals(Arrays.asList(1L), readTimes(renamed));
+    assertEquals(diskUsage, buffer.getDiskUsage());
+    assertEquals(2, buffer.getFileNum());
+    verify(firstWriter, times(1)).close();
+  }
+
+  /** Do not append to a partial header or an unexpected nonempty successor 
left on disk. */
+  @Test
+  public void testNonemptySuccessorIsNotOverwritten() throws Exception {
+    writeAndAwait(entry(1, 1, "before"), Status.SUCCESS);
+    File successor = walFile(1, 1, WALFileStatus.CONTAINS_SEARCH_INDEX);
+    byte[] partialHeader = new byte[] {1, 2};
+    Files.write(successor.toPath(), partialHeader);
+    roll(Status.FAILURE);
+    roll(Status.FAILURE);
+    assertArrayEquals(partialHeader, Files.readAllBytes(successor.toPath()));
+    assertEquals(0, buffer.getCurrentWALFileVersion());
+    verify(firstWriter, times(1)).close();
+  }
+
+  /** A failed first chunk must fail the whole large entry; the next batch can 
recover cleanly. */
+  @Test
+  public void testFailedSplitEntryDoesNotLeakIntoSuccessor() throws Exception {
+    writeAndAwait(entry(1, 1, "before"), Status.SUCCESS);
+    File successor = walFile(1, 1, WALFileStatus.CONTAINS_SEARCH_INDEX);
+    blockPath(successor);
+    roll(Status.FAILURE);
+    buffer.setBufferSize(192);
+    writeAndAwait(entry(2, 2, new String(new char[4096]).replace('\0', 'x')), 
Status.FAILURE);
+    unblockPath(successor);
+    writeAndAwait(entry(3, 3, "after"), Status.SUCCESS);
+    roll(Status.SUCCESS);
+    assertEquals(Arrays.asList(3L), readTimes(successor));
+    assertEquals(Arrays.asList(1L), readTimes(firstWriter.getLogFile()));
+  }
+
+  /** A failed seal has no proven durable boundary, so an empty successor must 
not hide it. */
+  @Test
+  public void testSealFailureIsNotSkipped() throws Exception {
+    writeAndAwait(entry(1, 1, "before"), Status.SUCCESS);
+    doThrow(new ClosedChannelException()).when(firstWriter).close();
+    roll(Status.FAILURE);
+    roll(Status.FAILURE);
+    assertEquals(0, buffer.getCurrentWALFileVersion());
+    assertEquals(1, buffer.getFileNum());
+    assertTrue(sealedFiles.isEmpty());
+    verify(firstWriter, times(1)).close();
+  }
+
+  /** A write failure must never be turned into success by a subsequent force 
or roll task. */
+  @Test
+  public void testWriteFailureFailsSubsequentListeners() throws Exception {
+    doThrow(new ClosedChannelException())
+        .when(firstWriter)
+        .write(any(ByteBuffer.class), any(WALMetaData.class));
+    writeAndAwait(entry(1, 1, "failed"), Status.FAILURE);
+    awaitReadOnly();
+    commonConfig.setNodeStatus(NodeStatus.Running);
+    roll(Status.FAILURE);
+    awaitReadOnly();
+    writeAndAwait(entry(2, 2, "also failed"), Status.FAILURE);
+    assertEquals(0, buffer.getCurrentWALFileVersion());
+    assertTrue(sealedFiles.isEmpty());
+    verify(firstWriter, times(1)).write(any(ByteBuffer.class), 
any(WALMetaData.class));
+  }
+
+  private WALInfoEntry entry(long time, long searchIndex, String value) throws 
Exception {
+    InsertRowNode node =
+        new InsertRowNode(
+            new PlanNodeId(""),
+            new PartialPath("root.test.d"),
+            false,
+            new String[] {"s"},
+            new TSDataType[] {TSDataType.TEXT},
+            time,
+            new Object[] {new Binary(value, TSFileConfig.STRING_CHARSET)},
+            false);
+    node.setMeasurementSchemas(
+        new MeasurementSchema[] {new MeasurementSchema("s", TSDataType.TEXT)});
+    node.setSearchIndex(searchIndex);
+    return new WALInfoEntry(1, node, false);
+  }
+
+  private void writeAndAwait(WALEntry entry, Status expected) {
+    buffer.write(entry);
+    Awaitility.await()
+        .atMost(10, TimeUnit.SECONDS)
+        .untilAsserted(() -> assertEquals(expected, 
entry.getWalFlushListener().waitForResult()));
+  }
+
+  private void roll(Status expected) {
+    writeAndAwait(new WALSignalEntry(WALEntryType.ROLL_WAL_LOG_WRITER_SIGNAL, 
false), expected);
+  }
+
+  private void awaitReadOnly() {
+    Awaitility.await()
+        .atMost(10, TimeUnit.SECONDS)
+        .untilAsserted(() -> assertEquals(NodeStatus.ReadOnly, 
commonConfig.getNodeStatus()));
+  }
+
+  private File walFile(long version, long searchIndex, WALFileStatus status) {
+    return new File(directory, WALFileUtils.getLogFileName(version, 
searchIndex, status));
+  }
+
+  private void blockPath(File path) throws IOException {
+    Files.createDirectory(path.toPath());
+    // A nonempty directory blocks both file creation and replacement on 
Windows and Unix.
+    Files.write(path.toPath().resolve("blocker"), new byte[] {1});
+  }
+
+  private void unblockPath(File path) throws IOException {
+    Files.delete(path.toPath().resolve("blocker"));
+    Files.delete(path.toPath());
+  }
+
+  private List<Long> readTimes(File file) throws IOException {
+    List<Long> times = new ArrayList<>();
+    try (WALReader reader = new WALReader(file)) {
+      while (reader.hasNext()) {
+        times.add(((InsertRowNode) reader.next().getValue()).getTime());
+      }
+    }
+    return times;
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFileTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFileTest.java
index 16e9bb36f87..766e9cb9046 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFileTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALFileTest.java
@@ -50,6 +50,7 @@ import java.io.DataInputStream;
 import java.io.File;
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
 import java.nio.channels.FileChannel;
 import java.nio.file.Files;
 import java.util.ArrayList;
@@ -57,6 +58,7 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertThrows;
@@ -86,6 +88,19 @@ public class WALFileTest {
     }
   }
 
+  /** Unexpected channel closure must propagate to the buffer instead of 
acknowledging a write. */
+  @Test
+  public void testClosedChannelWriteAndForceFail() throws IOException {
+    WALWriter writer = new WALWriter(walFile);
+    writer.logChannel.close();
+    byte[] before = Files.readAllBytes(walFile.toPath());
+    ByteBuffer buffer = ByteBuffer.allocate(1);
+    buffer.put((byte) 1);
+    assertThrows(ClosedChannelException.class, () -> writer.write(buffer));
+    assertThrows(ClosedChannelException.class, writer::force);
+    assertArrayEquals(before, Files.readAllBytes(walFile.toPath()));
+  }
+
   @Test
   public void testReadNormalFile() throws IOException, IllegalPathException {
     int fakeMemTableId = 1;

Reply via email to