This is an automated email from the ASF dual-hosted git repository.

tomsun28 pushed a commit to branch 2.0.0
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git


The following commit(s) were added to refs/heads/2.0.0 by this push:
     new 6fba4a3565 [bugfix] Make standalone startup work on Windows NTFS 
(#4400) (#4419)
6fba4a3565 is described below

commit 6fba4a35650cf8d2a7599573f760c687cb74eef2
Author: 晚安code <[email protected]>
AuthorDate: Mon Oct 5 09:35:30 2026 +0800

    [bugfix] Make standalone startup work on Windows NTFS (#4400) (#4419)
    
    Co-authored-by: Cursor <[email protected]>
---
 .../setup/security/OwnerOnlyFilePermissions.java   |  7 ++-
 .../setup/security/SecureSetupFileLock.java        | 48 +++++++++++++++---
 .../workflow/FileMigrationOperationStore.java      | 11 +++-
 .../ManagedMigrationStartupRecoverySession.java    |  8 +++
 .../workflow/MigrationOperationStoreException.java | 12 ++++-
 ...ManagedMigrationStartupRecoverySessionTest.java | 14 ++++++
 .../startup/runtime/StandaloneDeploymentOwner.java | 58 ++++++++++++++++++++--
 .../startup/runtime/StartupFailureReporter.java    | 19 +++++--
 .../runtime/StandaloneDeploymentOwnerTest.java     | 14 ++++++
 .../runtime/StartupFailureReporterTest.java        | 27 ++++++++--
 10 files changed, 194 insertions(+), 24 deletions(-)

diff --git 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/setup/security/OwnerOnlyFilePermissions.java
 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/setup/security/OwnerOnlyFilePermissions.java
index 1eb4721bc9..c4566eeeab 100644
--- 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/setup/security/OwnerOnlyFilePermissions.java
+++ 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/setup/security/OwnerOnlyFilePermissions.java
@@ -43,9 +43,14 @@ final class OwnerOnlyFilePermissions {
             AclEntryPermission.READ_DATA,
             AclEntryPermission.WRITE_DATA,
             AclEntryPermission.APPEND_DATA,
+            AclEntryPermission.READ_NAMED_ATTRS,
+            AclEntryPermission.WRITE_NAMED_ATTRS,
             AclEntryPermission.READ_ATTRIBUTES,
+            AclEntryPermission.WRITE_ATTRIBUTES,
             AclEntryPermission.READ_ACL,
-            AclEntryPermission.SYNCHRONIZE);
+            AclEntryPermission.WRITE_ACL,
+            AclEntryPermission.SYNCHRONIZE,
+            AclEntryPermission.DELETE);
 
     private OwnerOnlyFilePermissions() {
     }
diff --git 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/setup/security/SecureSetupFileLock.java
 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/setup/security/SecureSetupFileLock.java
index dced6381b2..bed40364cd 100644
--- 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/setup/security/SecureSetupFileLock.java
+++ 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/setup/security/SecureSetupFileLock.java
@@ -12,9 +12,11 @@ import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
 import java.nio.channels.FileLock;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.AccessDeniedException;
 import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.Files;
 import java.nio.file.LinkOption;
+import java.nio.file.OpenOption;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
 import java.nio.file.attribute.BasicFileAttributes;
@@ -56,8 +58,7 @@ public final class SecureSetupFileLock {
         jvmLock.lock();
         try {
             LockIdentity identity = initializeAndValidate();
-            try (FileChannel channel = FileChannel.open(
-                    lockFile, Set.of(StandardOpenOption.READ, 
StandardOpenOption.WRITE, LinkOption.NOFOLLOW_LINKS));
+            try (FileChannel channel = openLockChannelWithRetry();
                  FileLock ignored = channel.lock()) {
                 validateLockedIdentity(channel, identity);
                 T result = operation.run();
@@ -84,9 +85,7 @@ public final class SecureSetupFileLock {
         }
         try {
             LockIdentity identity = initializeAndValidate();
-            try (FileChannel channel = FileChannel.open(
-                    lockFile, Set.of(StandardOpenOption.READ, 
StandardOpenOption.WRITE,
-                            LinkOption.NOFOLLOW_LINKS));
+            try (FileChannel channel = openLockChannelWithRetry();
                  FileLock acquired = channel.tryLock()) {
                 if (acquired == null) {
                     return TryResult.busy();
@@ -130,7 +129,9 @@ public final class SecureSetupFileLock {
             String created = IDENTITY_PREFIX + UUID.randomUUID() + '\n';
             SecureSetupFile.create(installationRoot, lockFile, 
created.getBytes(StandardCharsets.UTF_8));
         } catch (FileAlreadyExistsException existing) {
-            // Cooperating contexts converge on the existing owner-only inode.
+            // Cooperating contexts converge on the existing owner-only inode. 
Re-enforce the DACL so a
+            // stale lock created with an older owner-only permission set can 
still be opened on Windows.
+            SecureSetupFile.enforceOwnerOnly(lockFile);
         }
         validate();
         SecureSetupFile.forceParentDirectoryIfSupported(installationRoot, 
lockFile);
@@ -161,11 +162,44 @@ public final class SecureSetupFileLock {
         BasicFileAttributes attributes = Files.readAttributes(
                 lockFile, BasicFileAttributes.class, 
LinkOption.NOFOLLOW_LINKS);
         if (attributes.fileKey() == null) {
-            throw new IOException("Secure setup-file lock identity is 
unavailable");
+            // BasicFileAttributes.fileKey() is always null on Windows; fall 
back to a canonical-path
+            // identity so standalone setup locks remain usable there.
+            return lockFile.toRealPath();
         }
         return attributes.fileKey();
     }
 
+    /**
+     * Windows Defender / Search Indexer can briefly hold a deny-share handle 
on newly-created files.
+     * Retry the reopen with backoff before failing the cooperative lock 
handshake.
+     */
+    private FileChannel openLockChannelWithRetry() throws IOException {
+        Set<OpenOption> options = Set.of(
+                StandardOpenOption.READ, StandardOpenOption.WRITE, 
LinkOption.NOFOLLOW_LINKS);
+        long[] delaysMillis = {0L, 250L, 750L, 1500L, 3000L, 5000L, 8000L};
+        AccessDeniedException lastFailure = null;
+        for (int attempt = 0; attempt < delaysMillis.length; attempt++) {
+            long delay = delaysMillis[attempt];
+            if (delay > 0L) {
+                try {
+                    Thread.sleep(delay);
+                } catch (InterruptedException interrupted) {
+                    Thread.currentThread().interrupt();
+                    if (lastFailure != null) {
+                        throw lastFailure;
+                    }
+                    throw new IOException("Interrupted while retrying lock 
channel open", interrupted);
+                }
+            }
+            try {
+                return FileChannel.open(lockFile, options);
+            } catch (AccessDeniedException denied) {
+                lastFailure = denied;
+            }
+        }
+        throw lastFailure;
+    }
+
     private String readPathIdentity() throws IOException {
         byte[] encoded = SecureSetupFile.readOwnerOnlyWithoutLinks(
                 installationRoot, lockFile, MAXIMUM_IDENTITY_BYTES);
diff --git 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/setup/workflow/FileMigrationOperationStore.java
 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/setup/workflow/FileMigrationOperationStore.java
index 094f65a857..b4d48fec28 100644
--- 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/setup/workflow/FileMigrationOperationStore.java
+++ 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/setup/workflow/FileMigrationOperationStore.java
@@ -23,6 +23,8 @@ import 
org.apache.hertzbeat.manager.setup.security.CommittedSetupFileDurabilityE
 import org.apache.hertzbeat.manager.setup.security.SecureSetupFile;
 import org.apache.hertzbeat.manager.setup.security.SecureSetupFileLock;
 import 
org.apache.hertzbeat.manager.setup.security.SecureSetupFileLock.TryResult;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /** Root-bound owner-only file adapter for the single active migration 
operation. */
 public final class FileMigrationOperationStore implements 
MigrationOperationStore {
@@ -31,6 +33,7 @@ public final class FileMigrationOperationStore implements 
MigrationOperationStor
     static final int HISTORY_LIMIT = 8;
     private static final String LOCK_PATH = 
"data/config/.metadata-migration-operations.lock";
     private static final int MAXIMUM_BYTES = 64 * 1024;
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(FileMigrationOperationStore.class);
     private final Path installationRoot;
     private final Path operationFile;
     private final Publisher publisher;
@@ -346,7 +349,9 @@ public final class FileMigrationOperationStore implements 
MigrationOperationStor
         } catch (MigrationOperationStoreException failure) {
             throw failure;
         } catch (IOException failure) {
-            throw failure(SetupErrorCode.CONFIG_RECOVERY_REQUIRED);
+            LOGGER.warn("Migration operation store lock handshake failed; 
requiring configuration recovery",
+                    failure);
+            throw failure(SetupErrorCode.CONFIG_RECOVERY_REQUIRED, failure);
         }
     }
 
@@ -360,6 +365,10 @@ public final class FileMigrationOperationStore implements 
MigrationOperationStor
         return new MigrationOperationStoreException(errorCode);
     }
 
+    private MigrationOperationStoreException failure(SetupErrorCode errorCode, 
Throwable cause) {
+        return new MigrationOperationStoreException(errorCode, cause);
+    }
+
     private static Path normalize(Path root) {
         return Objects.requireNonNull(root, 
"installationRoot").toAbsolutePath().normalize();
     }
diff --git 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/setup/workflow/ManagedMigrationStartupRecoverySession.java
 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/setup/workflow/ManagedMigrationStartupRecoverySession.java
index e3c677e4cf..88451d7ddf 100644
--- 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/setup/workflow/ManagedMigrationStartupRecoverySession.java
+++ 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/setup/workflow/ManagedMigrationStartupRecoverySession.java
@@ -17,6 +17,8 @@ import 
org.apache.hertzbeat.manager.setup.api.DeploymentApiContract.MigrationOpe
 import 
org.apache.hertzbeat.manager.setup.api.DeploymentApiContract.MigrationStage;
 import org.apache.hertzbeat.manager.setup.api.SetupApiContract.ApplyMode;
 import org.apache.hertzbeat.manager.setup.security.SecureSetupFile;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Holds one exact migration startup recovery binding across retries without 
Spring or persistence beans.
@@ -24,6 +26,8 @@ import 
org.apache.hertzbeat.manager.setup.security.SecureSetupFile;
  */
 public final class ManagedMigrationStartupRecoverySession implements 
AutoCloseable {
 
+    private static final Logger LOGGER =
+            
LoggerFactory.getLogger(ManagedMigrationStartupRecoverySession.class);
     private final FileMigrationOperationStore store;
     private final ManagedMigrationStartupRecoveryRuntime runtime;
     private Selection selection;
@@ -67,8 +71,11 @@ public final class ManagedMigrationStartupRecoverySession 
implements AutoCloseab
             }
             return disposition;
         } catch (MigrationStartupReconciliationException | 
MigrationOperationStoreException failure) {
+            LOGGER.warn("Migration startup recovery reconciliation failed; 
gating recovery", failure);
             return ManagedMigrationStartupRecoveryDisposition.GATED_RECOVERY;
         } catch (RuntimeException failure) {
+            LOGGER.warn("Migration startup recovery reconciliation failed 
unexpectedly; gating recovery",
+                    failure);
             return ManagedMigrationStartupRecoveryDisposition.GATED_RECOVERY;
         }
     }
@@ -96,6 +103,7 @@ public final class ManagedMigrationStartupRecoverySession 
implements AutoCloseab
                     snapshot.operationId(), snapshot.target(), 
snapshot.applyMode(),
                     snapshot.createdAt(), snapshot.startedAt(), 
snapshot.managedCandidateGeneration()));
         } catch (MigrationOperationStoreException | IllegalArgumentException 
failure) {
+            LOGGER.warn("Migration startup preflight selection failed; gating 
recovery", failure);
             return 
Selection.fixed(ManagedMigrationStartupRecoveryDisposition.GATED_RECOVERY);
         }
     }
diff --git 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/setup/workflow/MigrationOperationStoreException.java
 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/setup/workflow/MigrationOperationStoreException.java
index bbbe459d64..ed6819f0c9 100644
--- 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/setup/workflow/MigrationOperationStoreException.java
+++ 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/setup/workflow/MigrationOperationStoreException.java
@@ -10,13 +10,21 @@ package org.apache.hertzbeat.manager.setup.workflow;
 import java.util.Objects;
 import org.apache.hertzbeat.manager.setup.api.SetupApiContract.SetupErrorCode;
 
-/** Stable store failure that never retains provider messages, paths, or 
operation payloads. */
+/**
+ * Stable store failure whose own message never retains provider messages, 
paths, or operation payloads.
+ * An optional cause may be attached for diagnostics without changing the 
stable public message.
+ */
 public final class MigrationOperationStoreException extends RuntimeException {
 
     private final SetupErrorCode errorCode;
 
     MigrationOperationStoreException(SetupErrorCode errorCode) {
-        super("Migration operation store failed: " + 
Objects.requireNonNull(errorCode, "errorCode").value());
+        this(errorCode, null);
+    }
+
+    MigrationOperationStoreException(SetupErrorCode errorCode, Throwable 
cause) {
+        super("Migration operation store failed: " + 
Objects.requireNonNull(errorCode, "errorCode").value(),
+                cause);
         this.errorCode = errorCode;
     }
 
diff --git 
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/setup/workflow/ManagedMigrationStartupRecoverySessionTest.java
 
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/setup/workflow/ManagedMigrationStartupRecoverySessionTest.java
index bb33b68fda..38a9671a35 100644
--- 
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/setup/workflow/ManagedMigrationStartupRecoverySessionTest.java
+++ 
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/setup/workflow/ManagedMigrationStartupRecoverySessionTest.java
@@ -16,6 +16,9 @@ import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
 import java.nio.file.Path;
 import java.time.Instant;
 import java.util.Optional;
@@ -27,6 +30,7 @@ import 
org.apache.hertzbeat.manager.setup.api.SetupApiContract.ApplyMode;
 import org.apache.hertzbeat.manager.setup.api.SetupApiContract.SetupErrorCode;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.LoggerFactory;
 
 class ManagedMigrationStartupRecoverySessionTest {
 
@@ -186,6 +190,10 @@ class ManagedMigrationStartupRecoverySessionTest {
 
     @Test
     void gatesJournalConflictAndSafeRuntimeFailuresButPreservesFatalError() {
+        Logger logger = (Logger) 
LoggerFactory.getLogger(ManagedMigrationStartupRecoverySession.class);
+        ListAppender<ILoggingEvent> appender = new ListAppender<>();
+        appender.start();
+        logger.addAppender(appender);
         FileMigrationOperationStore corrupt = 
mock(FileMigrationOperationStore.class);
         when(corrupt.selectUniqueNonterminalForStartup())
                 .thenThrow(new 
MigrationOperationStoreException(SetupErrorCode.CONFIG_RECOVERY_REQUIRED));
@@ -194,7 +202,13 @@ class ManagedMigrationStartupRecoverySessionTest {
                      new ManagedMigrationStartupRecoverySession(root, corrupt, 
unused)) {
             assertThat(session.reconcile())
                     
.isEqualTo(ManagedMigrationStartupRecoveryDisposition.GATED_RECOVERY);
+        } finally {
+            logger.detachAppender(appender);
+            appender.stop();
         }
+        assertThat(appender.list)
+                .anySatisfy(event -> assertThat(event.getFormattedMessage())
+                        .contains("Migration startup preflight selection 
failed"));
         verify(unused, never()).reconcile(any());
 
         ManagedMigrationStartupRecoveryRuntime failing = 
mock(ManagedMigrationStartupRecoveryRuntime.class);
diff --git 
a/hertzbeat-startup/src/main/java/org/apache/hertzbeat/startup/runtime/StandaloneDeploymentOwner.java
 
b/hertzbeat-startup/src/main/java/org/apache/hertzbeat/startup/runtime/StandaloneDeploymentOwner.java
index 95fa53686e..5b64b7dc66 100644
--- 
a/hertzbeat-startup/src/main/java/org/apache/hertzbeat/startup/runtime/StandaloneDeploymentOwner.java
+++ 
b/hertzbeat-startup/src/main/java/org/apache/hertzbeat/startup/runtime/StandaloneDeploymentOwner.java
@@ -11,9 +11,11 @@ import java.io.IOException;
 import java.nio.channels.FileChannel;
 import java.nio.channels.FileLock;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.AccessDeniedException;
 import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.Files;
 import java.nio.file.LinkOption;
+import java.nio.file.OpenOption;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
 import java.nio.file.attribute.BasicFileAttributes;
@@ -63,8 +65,7 @@ public final class StandaloneDeploymentOwner implements 
AutoCloseable {
             initializeLockFile(root.canonicalRoot(), lockPath);
             Object rootKey = fileKey(root.canonicalRoot());
             Object lockKey = fileKey(lockPath);
-            channel = FileChannel.open(lockPath,
-                    Set.of(StandardOpenOption.READ, StandardOpenOption.WRITE, 
LinkOption.NOFOLLOW_LINKS));
+            channel = openLockChannelWithRetry(lockPath);
             fileLock = channel.tryLock();
             if (fileLock == null) {
                 throw StandaloneDeploymentOwnerException.unavailable();
@@ -75,7 +76,9 @@ public final class StandaloneDeploymentOwner implements 
AutoCloseable {
             if (exception instanceof StandaloneDeploymentOwnerException 
ownerFailure) {
                 throw ownerFailure;
             }
-            throw StandaloneDeploymentOwnerException.unavailable();
+            StandaloneDeploymentOwnerException unavailable = 
StandaloneDeploymentOwnerException.unavailable();
+            unavailable.initCause(exception);
+            throw unavailable;
         }
     }
 
@@ -115,7 +118,10 @@ public final class StandaloneDeploymentOwner implements 
AutoCloseable {
         try {
             SecureSetupFile.create(root, lockPath, LOCK_CONTENT);
         } catch (FileAlreadyExistsException existing) {
-            // The lock inode is persistent and is never unlinked during 
normal shutdown.
+            // The lock inode is persistent and is never unlinked during 
normal shutdown. Re-enforce the
+            // DACL so a stale lock created with an older owner-only 
permission set (for example without
+            // FILE_READ_EA / FILE_WRITE_EA) can still be reopened on Windows.
+            SecureSetupFile.enforceOwnerOnly(lockPath);
         }
         if (!SecureSetupFile.existsInsideRootWithoutLinks(root, lockPath)
                 || !SecureSetupFile.isOwnerOnlyRegularFile(lockPath)) {
@@ -124,10 +130,52 @@ public final class StandaloneDeploymentOwner implements 
AutoCloseable {
         SecureSetupFile.forceParentDirectoryIfSupported(root, lockPath);
     }
 
+    /**
+     * Windows Defender / Search Indexer can briefly hold a deny-share handle 
on newly-created files
+     * while they scan or index them, which makes the immediate reopen in 
{@link #acquire} fail with
+     * {@link AccessDeniedException}. Retry the reopen with backoff before 
giving up.
+     */
+    private static FileChannel openLockChannelWithRetry(Path lockPath) throws 
IOException {
+        Set<OpenOption> options = Set.of(
+                StandardOpenOption.READ, StandardOpenOption.WRITE, 
LinkOption.NOFOLLOW_LINKS);
+        long[] delaysMillis = {0L, 250L, 750L, 1500L, 3000L, 5000L, 8000L};
+        AccessDeniedException lastFailure = null;
+        for (int attempt = 0; attempt < delaysMillis.length; attempt++) {
+            long delay = delaysMillis[attempt];
+            if (delay > 0L) {
+                try {
+                    Thread.sleep(delay);
+                } catch (InterruptedException interrupted) {
+                    Thread.currentThread().interrupt();
+                    if (lastFailure != null) {
+                        throw lastFailure;
+                    }
+                    throw new IOException("Interrupted while retrying lock 
channel open", interrupted);
+                }
+            }
+            try {
+                FileChannel channel = FileChannel.open(lockPath, options);
+                if (attempt > 0) {
+                    System.err.println("[StandaloneDeploymentOwner] lock 
channel reopen succeeded after "
+                            + (attempt + 1) + " attempt(s); Windows 
scanner/indexer released the deny-share handle.");
+                }
+                return channel;
+            } catch (AccessDeniedException denied) {
+                lastFailure = denied;
+                System.err.println("[StandaloneDeploymentOwner] lock channel 
reopen attempt "
+                        + (attempt + 1) + "/" + delaysMillis.length
+                        + " denied; sleeping " + delay + "ms before next try 
(likely Windows Defender / Search Indexer).");
+            }
+        }
+        throw lastFailure;
+    }
+
     private static Object fileKey(Path path) throws IOException {
         Object key = Files.readAttributes(path, BasicFileAttributes.class, 
LinkOption.NOFOLLOW_LINKS).fileKey();
         if (key == null) {
-            throw new IOException("Standalone deployment owner identity is 
unavailable");
+            // BasicFileAttributes.fileKey() always returns null on Windows 
(JDK platform limitation).
+            // Fall back to a canonical-path identity so standalone startup 
remains usable there.
+            return path.toRealPath();
         }
         return key;
     }
diff --git 
a/hertzbeat-startup/src/main/java/org/apache/hertzbeat/startup/runtime/StartupFailureReporter.java
 
b/hertzbeat-startup/src/main/java/org/apache/hertzbeat/startup/runtime/StartupFailureReporter.java
index 2c4677d495..a1f461ef48 100644
--- 
a/hertzbeat-startup/src/main/java/org/apache/hertzbeat/startup/runtime/StartupFailureReporter.java
+++ 
b/hertzbeat-startup/src/main/java/org/apache/hertzbeat/startup/runtime/StartupFailureReporter.java
@@ -22,15 +22,19 @@ import org.apache.hertzbeat.common.runtime.RuntimeMode;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-/** Emits startup diagnostics without retaining exception messages or causes. 
*/
+/**
+ * Emits startup diagnostics without retaining exception messages.
+ * Cause class names are included so operators can distinguish wrapped 
failures without leaking secrets.
+ */
 final class StartupFailureReporter {
 
     private static final Logger LOGGER = 
LoggerFactory.getLogger(StartupFailureReporter.class);
     private final DiagnosticSink sink;
 
     StartupFailureReporter() {
-        this((stage, mode, exceptionClass) -> LOGGER.warn(
-                "Startup failure stage={} mode={} exception={}", stage, mode, 
exceptionClass));
+        this((stage, mode, exceptionClass, causeClass) -> LOGGER.warn(
+                "Startup failure stage={} mode={} exception={} cause={}",
+                stage, mode, exceptionClass, causeClass));
     }
 
     StartupFailureReporter(DiagnosticSink sink) {
@@ -39,12 +43,17 @@ final class StartupFailureReporter {
 
     void report(Stage stage, RuntimeMode mode, RuntimeException failure) {
         try {
-            sink.report(stage.value(), safeMode(mode), 
failure.getClass().getName());
+            sink.report(stage.value(), safeMode(mode), 
failure.getClass().getName(), causeClass(failure));
         } catch (RuntimeException ignored) {
             // Diagnostics are best-effort and must never change startup 
recovery control flow.
         }
     }
 
+    private static String causeClass(Throwable failure) {
+        Throwable cause = failure.getCause();
+        return cause == null ? "none" : cause.getClass().getName();
+    }
+
     private static String safeMode(RuntimeMode mode) {
         return switch (mode) {
             case SETUP_ONLY -> "setup_only";
@@ -57,7 +66,7 @@ final class StartupFailureReporter {
     @FunctionalInterface
     interface DiagnosticSink {
 
-        void report(String stage, String mode, String exceptionClass);
+        void report(String stage, String mode, String exceptionClass, String 
causeClass);
     }
 
     enum Stage {
diff --git 
a/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/runtime/StandaloneDeploymentOwnerTest.java
 
b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/runtime/StandaloneDeploymentOwnerTest.java
index 93eb7ae272..445737bfaf 100644
--- 
a/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/runtime/StandaloneDeploymentOwnerTest.java
+++ 
b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/runtime/StandaloneDeploymentOwnerTest.java
@@ -86,6 +86,20 @@ class StandaloneDeploymentOwnerTest {
         afterCrash.release();
     }
 
+    @Test
+    void ownershipFailurePreservesUnderlyingCause() throws Exception {
+        Path root = temporaryDirectory.resolve("cause");
+        Files.createDirectories(root.resolve("data/config"));
+        Path lock = root.resolve(StandaloneDeploymentOwner.LOCK_PATH);
+        Files.createDirectories(lock);
+
+        assertThatThrownBy(() -> 
StandaloneDeploymentOwner.acquire(resolve(root)))
+                .isInstanceOf(StandaloneDeploymentOwnerException.class)
+                .hasCauseInstanceOf(Exception.class)
+                .cause()
+                .isNotInstanceOf(StandaloneDeploymentOwnerException.class);
+    }
+
     @Test
     void migrationLeaseCloseDoesNotReleaseProcessOwner() throws Exception {
         Path root = temporaryDirectory.resolve("lease");
diff --git 
a/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/runtime/StartupFailureReporterTest.java
 
b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/runtime/StartupFailureReporterTest.java
index 8a8d50a81b..8484a4443c 100644
--- 
a/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/runtime/StartupFailureReporterTest.java
+++ 
b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/runtime/StartupFailureReporterTest.java
@@ -74,6 +74,23 @@ class StartupFailureReporterTest {
         assertSafeDiagnostic(appender.list.getFirst(), "startup-probe", 
RuntimeMode.RECOVERY, probeFailure);
     }
 
+    @Test
+    void reportsCauseClassWithoutLeakingCauseMessage() {
+        RuntimeException probeFailure = new IllegalStateException("outer");
+        probeFailure.initCause(new IllegalArgumentException(SECRET + " " + 
JDBC_URL));
+        RecordingLauncher launcher = new RecordingLauncher();
+        HertzBeatStartupCoordinator coordinator = new 
HertzBeatStartupCoordinator(
+                ignored -> {
+                    throw probeFailure;
+                }, launcher);
+
+        RunningApplicationContext context = coordinator.start(new 
String[]{CLI_SECRET});
+
+        assertEquals(RuntimeMode.RECOVERY, context.mode());
+        assertSafeDiagnostic(appender.list.getFirst(), "startup-probe", 
RuntimeMode.RECOVERY, probeFailure);
+        assertEquals(IllegalArgumentException.class.getName(), 
appender.list.getFirst().getArgumentArray()[3]);
+    }
+
     @ParameterizedTest
     @EnumSource(value = RuntimeMode.class, names = {"NORMAL", 
"FULL_SETUP_GATED"})
     void contextFailureRetainsSafeDiagnosticWhenRecoverySucceeds(RuntimeMode 
failedMode) {
@@ -114,7 +131,7 @@ class StartupFailureReporterTest {
     @Test
     void diagnosticSinkFailureCannotPreventFailClosedRecovery() {
         RecordingLauncher launcher = new RecordingLauncher();
-        StartupFailureReporter reporter = new StartupFailureReporter((stage, 
mode, exceptionClass) -> {
+        StartupFailureReporter reporter = new StartupFailureReporter((stage, 
mode, exceptionClass, causeClass) -> {
             throw new IllegalStateException("diagnostic sink unavailable " + 
SECRET);
         });
         HertzBeatStartupCoordinator coordinator = new 
HertzBeatStartupCoordinator(
@@ -150,15 +167,19 @@ class StartupFailureReporterTest {
 
     private static void assertSafeDiagnostic(
             ILoggingEvent event, String stage, RuntimeMode mode, 
RuntimeException originalFailure) {
+        String causeClass = originalFailure.getCause() == null
+                ? "none" : originalFailure.getCause().getClass().getName();
         assertEquals("Startup failure stage=" + stage + " mode=" + 
mode.value() + " exception="
-                + originalFailure.getClass().getName(), 
event.getFormattedMessage());
+                + originalFailure.getClass().getName() + " cause=" + 
causeClass,
+                event.getFormattedMessage());
         assertFalse(event.getFormattedMessage().contains(SECRET));
         assertFalse(event.getFormattedMessage().contains(JDBC_URL));
         assertFalse(event.getFormattedMessage().contains(CLI_SECRET));
-        assertEquals(3, event.getArgumentArray().length);
+        assertEquals(4, event.getArgumentArray().length);
         assertEquals(stage, event.getArgumentArray()[0]);
         assertEquals(mode.value(), event.getArgumentArray()[1]);
         assertEquals(originalFailure.getClass().getName(), 
event.getArgumentArray()[2]);
+        assertEquals(causeClass, event.getArgumentArray()[3]);
         assertNull(event.getThrowableProxy());
     }
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to