From 2e7f0aa0e176d5ed6a9e6258caa9744739504288 Mon Sep 17 00:00:00 2001 From: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> Date: Wed, 10 Jun 2026 09:21:05 +0900 Subject: [PATCH 1/2] Run a single hello command in MongoDB health indicators The MongoDB health indicators ran the `hello` command against every database returned by `listDatabaseNames()`. For the reactive indicator these commands were issued in parallel via `flatMap`, so a deployment with many databases per connection could open a connection per database on each health check, dramatically increasing connection usage (a regression from the Spring Data based implementation used previously). Since `hello` is a server-level command whose result is independent of the target database, run it only once against the `admin` database. The `databases` and `maxWireVersion` details are preserved. See gh-50727 Signed-off-by: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> --- .../mongodb/health/MongoHealthIndicator.java | 18 +++++------ .../health/MongoReactiveHealthIndicator.java | 31 ++++++------------- .../health/MongoHealthIndicatorTests.java | 15 ++++++--- .../MongoReactiveHealthIndicatorTests.java | 22 +++++++++---- 4 files changed, 44 insertions(+), 42 deletions(-) diff --git a/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoHealthIndicator.java b/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoHealthIndicator.java index 541a9a996f5..7ff5f350ecd 100644 --- a/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoHealthIndicator.java +++ b/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoHealthIndicator.java @@ -17,9 +17,7 @@ package org.springframework.boot.mongodb.health; import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import com.mongodb.client.MongoClient; import org.bson.Document; @@ -34,10 +32,13 @@ * MongoDB. * * @author Christian Dupuis + * @author Seonwoo Jung * @since 4.0.0 */ public class MongoHealthIndicator extends AbstractHealthIndicator { + private static final String ADMIN_DATABASE = "admin"; + private static final Document HELLO_COMMAND = Document.parse("{ hello: 1 }"); private final MongoClient mongoClient; @@ -50,15 +51,12 @@ public MongoHealthIndicator(MongoClient mongoClient) { @Override protected void doHealthCheck(Health.Builder builder) throws Exception { - Map details = new LinkedHashMap<>(); List databases = new ArrayList<>(); - details.put("databases", databases); - this.mongoClient.listDatabaseNames().forEach((database) -> { - Document result = this.mongoClient.getDatabase(database).runCommand(HELLO_COMMAND); - databases.add(database); - details.putIfAbsent("maxWireVersion", result.getInteger("maxWireVersion")); - }); - builder.up().withDetails(details); + this.mongoClient.listDatabaseNames().forEach(databases::add); + Document result = this.mongoClient.getDatabase(ADMIN_DATABASE).runCommand(HELLO_COMMAND); + builder.up() + .withDetail("databases", databases) + .withDetail("maxWireVersion", result.getInteger("maxWireVersion")); } } diff --git a/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicator.java b/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicator.java index c73cdd50e6f..2444005efcc 100644 --- a/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicator.java +++ b/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicator.java @@ -16,10 +16,7 @@ package org.springframework.boot.mongodb.health; -import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import com.mongodb.reactivestreams.client.MongoClient; import org.bson.Document; @@ -35,10 +32,13 @@ * A {@link ReactiveHealthIndicator} for Mongo. * * @author Yulin Qin + * @author Seonwoo Jung * @since 4.0.0 */ public class MongoReactiveHealthIndicator extends AbstractReactiveHealthIndicator { + private static final String ADMIN_DATABASE = "admin"; + private static final Document HELLO_COMMAND = Document.parse("{ hello: 1 }"); private final MongoClient mongoClient; @@ -51,24 +51,13 @@ public MongoReactiveHealthIndicator(MongoClient mongoClient) { @Override protected Mono doHealthCheck(Health.Builder builder) { - Mono> healthDetails = Flux.from(this.mongoClient.listDatabaseNames()) - .flatMap((database) -> Mono.from(this.mongoClient.getDatabase(database).runCommand(HELLO_COMMAND)) - .map((document) -> new HelloResponse(database, document))) - .collectList() - .map((responses) -> { - Map databaseDetails = new LinkedHashMap<>(); - List databases = new ArrayList<>(); - databaseDetails.put("databases", databases); - for (HelloResponse response : responses) { - databases.add(response.database()); - databaseDetails.putIfAbsent("maxWireVersion", response.document().getInteger("maxWireVersion")); - } - return databaseDetails; - }); - return healthDetails.map((details) -> builder.up().withDetails(details).build()); - } - - private record HelloResponse(String database, Document document) { + Mono hello = Mono.from(this.mongoClient.getDatabase(ADMIN_DATABASE).runCommand(HELLO_COMMAND)); + Mono> databases = Flux.from(this.mongoClient.listDatabaseNames()).collectList(); + return hello.zipWith(databases) + .map((result) -> builder.up() + .withDetail("databases", result.getT2()) + .withDetail("maxWireVersion", result.getT1().getInteger("maxWireVersion")) + .build()); } } diff --git a/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoHealthIndicatorTests.java b/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoHealthIndicatorTests.java index 27d1f4f2a96..ab1d8f112c3 100644 --- a/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoHealthIndicatorTests.java +++ b/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoHealthIndicatorTests.java @@ -35,12 +35,14 @@ import static org.mockito.BDDMockito.then; import static org.mockito.BDDMockito.willAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; /** * Tests for {@link MongoHealthIndicator}. * * @author Christian Dupuis * @author Andy Wilkinson + * @author Seonwoo Jung */ class MongoHealthIndicatorTests { @@ -52,19 +54,22 @@ void mongoIsUp() { MongoClient mongoClient = mock(MongoClient.class); MongoIterable databaseNames = mock(MongoIterable.class); willAnswer((invocation) -> { - ((Consumer) invocation.getArgument(0)).accept("db"); + ((Consumer) invocation.getArgument(0)).accept("test"); + ((Consumer) invocation.getArgument(0)).accept("admin"); return null; }).given(databaseNames).forEach(any()); given(mongoClient.listDatabaseNames()).willReturn(databaseNames); - MongoDatabase mongoDatabase = mock(MongoDatabase.class); - given(mongoClient.getDatabase("db")).willReturn(mongoDatabase); - given(mongoDatabase.runCommand(Document.parse("{ hello: 1 }"))).willReturn(commandResult); + MongoDatabase adminDatabase = mock(MongoDatabase.class); + given(mongoClient.getDatabase("admin")).willReturn(adminDatabase); + given(adminDatabase.runCommand(Document.parse("{ hello: 1 }"))).willReturn(commandResult); MongoHealthIndicator healthIndicator = new MongoHealthIndicator(mongoClient); Health health = healthIndicator.health(); assertThat(health.getStatus()).isEqualTo(Status.UP); assertThat(health.getDetails()).containsEntry("maxWireVersion", 10); - assertThat(health.getDetails()).containsEntry("databases", List.of("db")); + assertThat(health.getDetails()).containsEntry("databases", List.of("test", "admin")); then(commandResult).should().getInteger("maxWireVersion"); + // the hello command must only be run once, never per listed database + then(mongoClient).should(never()).getDatabase("test"); } @Test diff --git a/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicatorTests.java b/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicatorTests.java index f6f115e40a4..64211435204 100644 --- a/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicatorTests.java +++ b/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicatorTests.java @@ -24,6 +24,7 @@ import com.mongodb.reactivestreams.client.MongoDatabase; import org.bson.Document; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -32,23 +33,26 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; /** * Tests for {@link MongoReactiveHealthIndicator}. * * @author Yulin Qin + * @author Seonwoo Jung */ class MongoReactiveHealthIndicatorTests { @Test void mongoIsUp() { MongoClient mongoClient = mock(MongoClient.class); - given(mongoClient.listDatabaseNames()).willReturn(Mono.just("db")); - MongoDatabase mongoDatabase = mock(MongoDatabase.class); - given(mongoClient.getDatabase("db")).willReturn(mongoDatabase); + given(mongoClient.listDatabaseNames()).willReturn(Flux.just("test", "admin")); + MongoDatabase adminDatabase = mock(MongoDatabase.class); + given(mongoClient.getDatabase("admin")).willReturn(adminDatabase); Document commandResult = mock(Document.class); - given(mongoDatabase.runCommand(Document.parse("{ hello: 1 }"))).willReturn(Mono.just(commandResult)); + given(adminDatabase.runCommand(Document.parse("{ hello: 1 }"))).willReturn(Mono.just(commandResult)); given(commandResult.getInteger("maxWireVersion")).willReturn(10); MongoReactiveHealthIndicator mongoReactiveHealthIndicator = new MongoReactiveHealthIndicator(mongoClient); Mono health = mongoReactiveHealthIndicator.health(); @@ -56,14 +60,20 @@ void mongoIsUp() { assertThat(h.getStatus()).isEqualTo(Status.UP); assertThat(h.getDetails()).containsOnlyKeys("maxWireVersion", "databases"); assertThat(h.getDetails()).containsEntry("maxWireVersion", 10); - assertThat(h.getDetails()).containsEntry("databases", List.of("db")); + assertThat(h.getDetails()).containsEntry("databases", List.of("test", "admin")); }).expectComplete().verify(Duration.ofSeconds(30)); + // the hello command must only be run once, never per listed database + then(mongoClient).should(never()).getDatabase("test"); } @Test void mongoIsDown() { MongoClient mongoClient = mock(MongoClient.class); - given(mongoClient.listDatabaseNames()).willThrow(new MongoException("Connection failed")); + given(mongoClient.listDatabaseNames()).willReturn(Flux.just("test")); + MongoDatabase adminDatabase = mock(MongoDatabase.class); + given(mongoClient.getDatabase("admin")).willReturn(adminDatabase); + given(adminDatabase.runCommand(Document.parse("{ hello: 1 }"))) + .willReturn(Mono.error(new MongoException("Connection failed"))); MongoReactiveHealthIndicator mongoReactiveHealthIndicator = new MongoReactiveHealthIndicator(mongoClient); Mono health = mongoReactiveHealthIndicator.health(); StepVerifier.create(health).consumeNextWith((h) -> { From bc374ae0bb089c5373f140ba9eb3e4116092084f Mon Sep 17 00:00:00 2001 From: seonwooj0810 Date: Wed, 10 Jun 2026 17:24:05 +0900 Subject: [PATCH 2/2] Use visible Mongo database for health check Signed-off-by: seonwooj0810 --- .../mongodb/health/MongoHealthIndicator.java | 9 +++++++- .../health/MongoReactiveHealthIndicator.java | 17 ++++++++++---- .../health/MongoHealthIndicatorTests.java | 23 +++++++++++++++++++ .../MongoReactiveHealthIndicatorTests.java | 22 +++++++++++++++++- 4 files changed, 64 insertions(+), 7 deletions(-) diff --git a/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoHealthIndicator.java b/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoHealthIndicator.java index 7ff5f350ecd..afb9e07db21 100644 --- a/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoHealthIndicator.java +++ b/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoHealthIndicator.java @@ -53,10 +53,17 @@ public MongoHealthIndicator(MongoClient mongoClient) { protected void doHealthCheck(Health.Builder builder) throws Exception { List databases = new ArrayList<>(); this.mongoClient.listDatabaseNames().forEach(databases::add); - Document result = this.mongoClient.getDatabase(ADMIN_DATABASE).runCommand(HELLO_COMMAND); + Document result = this.mongoClient.getDatabase(getDatabaseName(databases)).runCommand(HELLO_COMMAND); builder.up() .withDetail("databases", databases) .withDetail("maxWireVersion", result.getInteger("maxWireVersion")); } + private static String getDatabaseName(List databases) { + if (databases.contains(ADMIN_DATABASE)) { + return ADMIN_DATABASE; + } + return (!databases.isEmpty()) ? databases.get(0) : ADMIN_DATABASE; + } + } diff --git a/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicator.java b/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicator.java index 2444005efcc..cc4e60356bb 100644 --- a/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicator.java +++ b/module/spring-boot-mongodb/src/main/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicator.java @@ -51,13 +51,20 @@ public MongoReactiveHealthIndicator(MongoClient mongoClient) { @Override protected Mono doHealthCheck(Health.Builder builder) { - Mono hello = Mono.from(this.mongoClient.getDatabase(ADMIN_DATABASE).runCommand(HELLO_COMMAND)); Mono> databases = Flux.from(this.mongoClient.listDatabaseNames()).collectList(); - return hello.zipWith(databases) + return databases.flatMap((databaseNames) -> Mono + .from(this.mongoClient.getDatabase(getDatabaseName(databaseNames)).runCommand(HELLO_COMMAND)) .map((result) -> builder.up() - .withDetail("databases", result.getT2()) - .withDetail("maxWireVersion", result.getT1().getInteger("maxWireVersion")) - .build()); + .withDetail("databases", databaseNames) + .withDetail("maxWireVersion", result.getInteger("maxWireVersion")) + .build())); + } + + private static String getDatabaseName(List databases) { + if (databases.contains(ADMIN_DATABASE)) { + return ADMIN_DATABASE; + } + return (!databases.isEmpty()) ? databases.get(0) : ADMIN_DATABASE; } } diff --git a/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoHealthIndicatorTests.java b/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoHealthIndicatorTests.java index ab1d8f112c3..7973cb146d2 100644 --- a/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoHealthIndicatorTests.java +++ b/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoHealthIndicatorTests.java @@ -72,6 +72,29 @@ void mongoIsUp() { then(mongoClient).should(never()).getDatabase("test"); } + @Test + @SuppressWarnings("unchecked") + void mongoUsesFirstDatabaseWhenAdminIsNotVisible() { + Document commandResult = mock(Document.class); + given(commandResult.getInteger("maxWireVersion")).willReturn(10); + MongoClient mongoClient = mock(MongoClient.class); + MongoIterable databaseNames = mock(MongoIterable.class); + willAnswer((invocation) -> { + ((Consumer) invocation.getArgument(0)).accept("test"); + return null; + }).given(databaseNames).forEach(any()); + given(mongoClient.listDatabaseNames()).willReturn(databaseNames); + MongoDatabase database = mock(MongoDatabase.class); + given(mongoClient.getDatabase("test")).willReturn(database); + given(database.runCommand(Document.parse("{ hello: 1 }"))).willReturn(commandResult); + MongoHealthIndicator healthIndicator = new MongoHealthIndicator(mongoClient); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails()).containsEntry("maxWireVersion", 10); + assertThat(health.getDetails()).containsEntry("databases", List.of("test")); + then(mongoClient).should(never()).getDatabase("admin"); + } + @Test void mongoIsDown() { MongoClient mongoClient = mock(MongoClient.class); diff --git a/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicatorTests.java b/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicatorTests.java index 64211435204..8fd46cfeda6 100644 --- a/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicatorTests.java +++ b/module/spring-boot-mongodb/src/test/java/org/springframework/boot/mongodb/health/MongoReactiveHealthIndicatorTests.java @@ -67,9 +67,29 @@ void mongoIsUp() { } @Test - void mongoIsDown() { + void mongoUsesFirstDatabaseWhenAdminIsNotVisible() { MongoClient mongoClient = mock(MongoClient.class); given(mongoClient.listDatabaseNames()).willReturn(Flux.just("test")); + MongoDatabase database = mock(MongoDatabase.class); + given(mongoClient.getDatabase("test")).willReturn(database); + Document commandResult = mock(Document.class); + given(database.runCommand(Document.parse("{ hello: 1 }"))).willReturn(Mono.just(commandResult)); + given(commandResult.getInteger("maxWireVersion")).willReturn(10); + MongoReactiveHealthIndicator mongoReactiveHealthIndicator = new MongoReactiveHealthIndicator(mongoClient); + Mono health = mongoReactiveHealthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> { + assertThat(h.getStatus()).isEqualTo(Status.UP); + assertThat(h.getDetails()).containsOnlyKeys("maxWireVersion", "databases"); + assertThat(h.getDetails()).containsEntry("maxWireVersion", 10); + assertThat(h.getDetails()).containsEntry("databases", List.of("test")); + }).expectComplete().verify(Duration.ofSeconds(30)); + then(mongoClient).should(never()).getDatabase("admin"); + } + + @Test + void mongoIsDown() { + MongoClient mongoClient = mock(MongoClient.class); + given(mongoClient.listDatabaseNames()).willReturn(Flux.just("admin")); MongoDatabase adminDatabase = mock(MongoDatabase.class); given(mongoClient.getDatabase("admin")).willReturn(adminDatabase); given(adminDatabase.runCommand(Document.parse("{ hello: 1 }")))