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 ab770467c8e Fix DataRegion snapshot failures when hard links are 
unavailable (#19049)
ab770467c8e is described below

commit ab770467c8e2092e6bdbc5388c49d60baa21b681
Author: Hongzhi Gao <[email protected]>
AuthorDate: Thu Oct 29 16:37:16 2026 +0800

    Fix DataRegion snapshot failures when hard links are unavailable (#19049)
---
 .../iotdb/consensus/i18n/IoTConsensusMessages.java |  3 +
 .../iotdb/consensus/i18n/IoTConsensusMessages.java |  3 +
 .../consensus/iot/IoTConsensusServerImpl.java      |  6 +-
 .../iotdb/db/i18n/StorageEngineMessages.java       |  2 +
 .../iotdb/db/i18n/StorageEngineMessages.java       |  2 +
 .../dataregion/snapshot/SnapshotFileUtils.java     | 49 +++++++++++
 .../dataregion/snapshot/SnapshotLoader.java        |  2 +-
 .../dataregion/snapshot/SnapshotTaker.java         |  8 +-
 .../dataregion/snapshot/SnapshotFileUtilsTest.java | 94 ++++++++++++++++++++++
 9 files changed, 165 insertions(+), 4 deletions(-)

diff --git 
a/iotdb-core/consensus/src/main/i18n/en/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java
 
b/iotdb-core/consensus/src/main/i18n/en/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java
index 6c1e4c1dc17..af74fa6e966 100644
--- 
a/iotdb-core/consensus/src/main/i18n/en/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java
+++ 
b/iotdb-core/consensus/src/main/i18n/en/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java
@@ -341,4 +341,7 @@ public final class IoTConsensusMessages {
       
LOG_FAILED_TO_RECORD_A_USER_DATA_TRANSFER_AUDIT_EVENT_CONSENSUS_REPLICATION_WILL_CONTINUE_F215E222
 =
           "Failed to record a user-data transfer audit event; consensus 
replication will continue.";
 
+  public static final String
+      
EXCEPTION_FAILED_TO_TAKE_SNAPSHOT_FOR_CONSENSUS_GROUP_ARG_IN_ARG_CHECK_STATE_MACHINE_LOGS_FOR_THE_UNDERLYING_CAUSE_74663D2D
 =
+          "Failed to take snapshot for consensus group %s in %s; check state 
machine logs for the underlying cause";
 }
diff --git 
a/iotdb-core/consensus/src/main/i18n/zh/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java
 
b/iotdb-core/consensus/src/main/i18n/zh/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java
index a7ef49bf467..b46713c83af 100644
--- 
a/iotdb-core/consensus/src/main/i18n/zh/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java
+++ 
b/iotdb-core/consensus/src/main/i18n/zh/org/apache/iotdb/consensus/i18n/IoTConsensusMessages.java
@@ -339,4 +339,7 @@ public final class IoTConsensusMessages {
       
LOG_FAILED_TO_RECORD_A_USER_DATA_TRANSFER_AUDIT_EVENT_CONSENSUS_REPLICATION_WILL_CONTINUE_F215E222
 =
           "记录用户数据传送审计事件失败;Consensus 复制将继续。";
 
+  public static final String
+      
EXCEPTION_FAILED_TO_TAKE_SNAPSHOT_FOR_CONSENSUS_GROUP_ARG_IN_ARG_CHECK_STATE_MACHINE_LOGS_FOR_THE_UNDERLYING_CAUSE_74663D2D
 =
+          "为共识组 %s 在 %s 创建快照失败;请检查状态机日志以获取底层原因";
 }
diff --git 
a/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImpl.java
 
b/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImpl.java
index aab4c7523cf..b0c6c463b26 100644
--- 
a/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImpl.java
+++ 
b/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImpl.java
@@ -417,7 +417,11 @@ public class IoTConsensusServerImpl {
       }
       if (!stateMachine.takeSnapshot(snapshotDir)) {
         throw new ConsensusGroupModifyPeerException(
-            IoTConsensusMessages.UNKNOWN_ERROR_TAKING_SNAPSHOT);
+            String.format(
+                IoTConsensusMessages
+                    
.EXCEPTION_FAILED_TO_TAKE_SNAPSHOT_FOR_CONSENSUS_GROUP_ARG_IN_ARG_CHECK_STATE_MACHINE_LOGS_FOR_THE_UNDERLYING_CAUSE_74663D2D,
+                thisNode.getGroupId(),
+                snapshotDir));
       }
       clearOldSnapshot();
     } catch (IOException e) {
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 9d1a23279b8..81f1a1000af 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
@@ -1760,4 +1760,6 @@ public final class StorageEngineMessages {
 
   public static final String LOG_LOAD_CONSENSUS_CLEANER_SWEEP_FAILED_7C3E2E6D =
       "Failed to sweep the staged directories of the finished LOAD tasks.";
+  public static final String 
LOG_FAILED_TO_CREATE_SNAPSHOT_FOR_DATABASE_ARG_REGION_ARG_1A7C8DEA =
+      "Failed to create snapshot for database {}, region {}";
 }
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 8f6ebfc871b..44e3076760b 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
@@ -1679,4 +1679,6 @@ public final class StorageEngineMessages {
 
   public static final String LOG_LOAD_CONSENSUS_CLEANER_SWEEP_FAILED_7C3E2E6D =
       "扫描已完成的 LOAD 任务暂存目录失败。";
+  public static final String 
LOG_FAILED_TO_CREATE_SNAPSHOT_FOR_DATABASE_ARG_REGION_ARG_1A7C8DEA =
+      "为 database {}、region {} 创建快照失败";
 }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/snapshot/SnapshotFileUtils.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/snapshot/SnapshotFileUtils.java
new file mode 100644
index 00000000000..3b6a7dece13
--- /dev/null
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/snapshot/SnapshotFileUtils.java
@@ -0,0 +1,49 @@
+/*
+ * 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.snapshot;
+
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+
+final class SnapshotFileUtils {
+  private SnapshotFileUtils() {}
+
+  static void createLinkOrCopy(Path target, Path source) throws IOException {
+    try {
+      Files.createLink(target, source);
+    } catch (IOException | UnsupportedOperationException linkFailure) {
+      // Snapshot sources are immutable or protected by the caller's file 
locks.
+      // Do not replace an existing destination: it may belong to another 
capture.
+      try {
+        Files.copy(source, target);
+        // Persist the new inode before the snapshot log can mark this file as 
captured.
+        try (FileChannel output = FileChannel.open(target, 
StandardOpenOption.WRITE)) {
+          output.force(true);
+        }
+      } catch (IOException | UnsupportedOperationException copyFailure) {
+        copyFailure.addSuppressed(linkFailure);
+        throw copyFailure;
+      }
+    }
+  }
+}
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/snapshot/SnapshotLoader.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/snapshot/SnapshotLoader.java
index 6b7f9663910..f029578cda5 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/snapshot/SnapshotLoader.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/snapshot/SnapshotLoader.java
@@ -593,7 +593,7 @@ public class SnapshotLoader {
                 StorageEngineMessages.FAILED_TO_CREATE_DIR,
                 targetFile.getParentFile().getAbsolutePath()));
       }
-      Files.createLink(targetFile.toPath(), file.toPath());
+      SnapshotFileUtils.createLinkOrCopy(targetFile.toPath(), file.toPath());
     }
   }
 
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/snapshot/SnapshotTaker.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/snapshot/SnapshotTaker.java
index 5b8f5491a39..854909bc14c 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/snapshot/SnapshotTaker.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/snapshot/SnapshotTaker.java
@@ -265,7 +265,11 @@ public class SnapshotTaker {
       }
       return true;
     } catch (IOException e) {
-      LOGGER.error(StorageEngineMessages.CATCH_IO_EXCEPTION_CREATING_SNAPSHOT, 
e);
+      LOGGER.error(
+          
StorageEngineMessages.LOG_FAILED_TO_CREATE_SNAPSHOT_FOR_DATABASE_ARG_REGION_ARG_1A7C8DEA,
+          dataRegion.getDatabaseName(),
+          dataRegion.getDataRegionIdString(),
+          e);
       return false;
     }
   }
@@ -278,7 +282,7 @@ public class SnapshotTaker {
       return;
     }
     Files.deleteIfExists(target.toPath());
-    Files.createLink(target.toPath(), source.toPath());
+    SnapshotFileUtils.createLinkOrCopy(target.toPath(), source.toPath());
     snapshotLogger.logFile(source);
   }
 
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/snapshot/SnapshotFileUtilsTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/snapshot/SnapshotFileUtilsTest.java
new file mode 100644
index 00000000000..de449583dff
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/snapshot/SnapshotFileUtilsTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.snapshot;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.IOException;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+
+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.junit.Assert.fail;
+
+public class SnapshotFileUtilsTest {
+  @Rule public TemporaryFolder folder = new TemporaryFolder();
+
+  @Test
+  public void testHardLink() throws Exception {
+    Path source = folder.newFile("source").toPath();
+    Files.write(source, new byte[] {1, 2, 3});
+    Path target = source.resolveSibling("target");
+    SnapshotFileUtils.createLinkOrCopy(target, source);
+    assertTrue(Files.isSameFile(source, target));
+    assertArrayEquals(Files.readAllBytes(source), Files.readAllBytes(target));
+  }
+
+  @Test
+  public void testCopyWhenLinksUnsupported() throws Exception {
+    Path archive = folder.getRoot().toPath().resolve("snapshot.zip");
+    try (FileSystem fs = FileSystems.newFileSystem(archive, Map.of("create", 
"true"))) {
+      Path source = fs.getPath("/source");
+      Path target = fs.getPath("/target");
+      Files.write(source, new byte[] {1, 2, 3});
+      SnapshotFileUtils.createLinkOrCopy(target, source);
+      Files.delete(source);
+      assertArrayEquals(new byte[] {1, 2, 3}, Files.readAllBytes(target));
+    }
+  }
+
+  @Test
+  public void testCopyFailureRetainsLinkFailure() throws Exception {
+    Path source = folder.getRoot().toPath().resolve("missing");
+    Path target = source.resolveSibling("target");
+    try {
+      SnapshotFileUtils.createLinkOrCopy(target, source);
+      fail("Missing source must fail the snapshot");
+    } catch (IOException e) {
+      assertTrue(e.getMessage().contains("missing"));
+      assertEquals(1, e.getSuppressed().length);
+      assertTrue(e.getSuppressed()[0] instanceof IOException);
+    }
+    assertFalse(Files.exists(target));
+  }
+
+  @Test
+  public void testExistingTargetIsNotOverwritten() throws Exception {
+    Path source = folder.newFile("source").toPath();
+    Path target = folder.newFile("target").toPath();
+    Files.write(source, new byte[] {1});
+    Files.write(target, new byte[] {2});
+    try {
+      SnapshotFileUtils.createLinkOrCopy(target, source);
+      fail("Existing target must not be replaced");
+    } catch (IOException e) {
+      assertEquals(1, e.getSuppressed().length);
+    }
+    assertArrayEquals(new byte[] {2}, Files.readAllBytes(target));
+  }
+}

Reply via email to