From ac0568dedb481f2a15d9b2bcd82339b18d4471f7 Mon Sep 17 00:00:00 2001 From: Tommy Karlsson Date: Mon, 18 Aug 2025 15:40:14 +0200 Subject: [PATCH] Update Hazelcast health indicator to verify that Hazelcast is running This is needed because when the default Out-Of-Memory handler in Hazelcast detects a OOM error, and terminates Hazelcast, it does so on the _node_ but not via the Hazelcast lifecycle manager - and the results is that it is still possible to successfully execute transactions in Hazelcast after this termination. The health indicator should not report UP after Hazelcast has detected an OOM and terminated itself. Signed-off-by: Tommy Karlsson --- .../health/HazelcastHealthIndicator.java | 5 ++ .../health/HazelcastHealthIndicatorTests.java | 59 ++++++++++++++----- 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/module/spring-boot-hazelcast/src/main/java/org/springframework/boot/hazelcast/health/HazelcastHealthIndicator.java b/module/spring-boot-hazelcast/src/main/java/org/springframework/boot/hazelcast/health/HazelcastHealthIndicator.java index a415259cafa1..bc91f0ea2090 100644 --- a/module/spring-boot-hazelcast/src/main/java/org/springframework/boot/hazelcast/health/HazelcastHealthIndicator.java +++ b/module/spring-boot-hazelcast/src/main/java/org/springframework/boot/hazelcast/health/HazelcastHealthIndicator.java @@ -28,6 +28,7 @@ * * @author Dmytro Nosan * @author Stephane Nicoll + * @author Tommy Karlsson * @since 4.0.0 */ public class HazelcastHealthIndicator extends AbstractHealthIndicator { @@ -42,6 +43,10 @@ public HazelcastHealthIndicator(HazelcastInstance hazelcast) { @Override protected void doHealthCheck(Health.Builder builder) { + if (!this.hazelcast.getLifecycleService().isRunning()) { + builder.down(); + return; + } this.hazelcast.executeTransaction((context) -> { String uuid = this.hazelcast.getLocalEndpoint().getUuid().toString(); builder.up().withDetail("name", this.hazelcast.getName()).withDetail("uuid", uuid); diff --git a/module/spring-boot-hazelcast/src/test/java/org/springframework/boot/hazelcast/health/HazelcastHealthIndicatorTests.java b/module/spring-boot-hazelcast/src/test/java/org/springframework/boot/hazelcast/health/HazelcastHealthIndicatorTests.java index 5e370f0c1f0d..4044fc69fc24 100644 --- a/module/spring-boot-hazelcast/src/test/java/org/springframework/boot/hazelcast/health/HazelcastHealthIndicatorTests.java +++ b/module/spring-boot-hazelcast/src/test/java/org/springframework/boot/hazelcast/health/HazelcastHealthIndicatorTests.java @@ -18,6 +18,8 @@ import com.hazelcast.core.HazelcastException; import com.hazelcast.core.HazelcastInstance; +import com.hazelcast.instance.impl.HazelcastInstanceProxy; +import com.hazelcast.instance.impl.OutOfMemoryHandlerHelper; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -37,25 +39,26 @@ * * @author Dmytro Nosan * @author Stephane Nicoll + * @author Tommy Karlsson */ +@WithResource(name = "hazelcast.xml", content = """ + + actuator-hazelcast + + + + + + + + + """) class HazelcastHealthIndicatorTests { @Test - @WithResource(name = "hazelcast.xml", content = """ - - actuator-hazelcast - - - - - - - - - """) void hazelcastUp() { new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(HazelcastAutoConfiguration.class)) .withPropertyValues("spring.hazelcast.config=hazelcast.xml") @@ -69,6 +72,32 @@ void hazelcastUp() { }); } + @Test + void hazelcastShutdown() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(HazelcastAutoConfiguration.class)) + .withPropertyValues("spring.hazelcast.config=hazelcast.xml") + .run((context) -> { + HazelcastInstance hazelcast = context.getBean(HazelcastInstance.class); + hazelcast.shutdown(); + Health health = new HazelcastHealthIndicator(hazelcast).health(); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + }); + } + + @Test + void hazelcastOOMShutdown() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(HazelcastAutoConfiguration.class)) + .withPropertyValues("spring.hazelcast.config=hazelcast.xml") + .run((context) -> { + HazelcastInstance hazelcast = context.getBean(HazelcastInstance.class); + HazelcastInstance original = ((HazelcastInstanceProxy) hazelcast).getOriginal(); + OutOfMemoryHandlerHelper.tryCloseConnections(original); + OutOfMemoryHandlerHelper.tryShutdown(original); + Health health = new HazelcastHealthIndicator(hazelcast).health(); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + }); + } + @Test void hazelcastDown() { HazelcastInstance hazelcast = mock(HazelcastInstance.class);