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]