From 0b6305f49924c2812a51cb9c2acf7d2d6547a59b Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Sat, 21 Feb 2026 12:59:22 +0530 Subject: [PATCH 1/8] feat(health,version): add health and version endponts --- pom.xml | 21 +- .../controller/health/HealthController.java | 56 +- .../controller/version/VersionController.java | 58 +- .../admin/service/health/HealthService.java | 495 ++++++++++++++++-- .../utils/JwtUserIdValidationFilter.java | 4 +- 5 files changed, 542 insertions(+), 92 deletions(-) diff --git a/pom.xml b/pom.xml index f0f714e..9e53417 100644 --- a/pom.xml +++ b/pom.xml @@ -298,9 +298,14 @@ - pl.project13.maven - git-commit-id-plugin - 4.9.10 + org.apache.maven.plugins + maven-jar-plugin + 3.0.2 + + + io.github.git-commit-id + git-commit-id-maven-plugin + 9.0.2 get-the-git-infos @@ -314,11 +319,13 @@ true ${project.build.outputDirectory}/git.properties - git.commit.id - git.build.time + ^git.branch$ + ^git.commit.id.abbrev$ + ^git.build.version$ + ^git.build.time$ - full - properties + false + false diff --git a/src/main/java/com/iemr/admin/controller/health/HealthController.java b/src/main/java/com/iemr/admin/controller/health/HealthController.java index f01d279..d1a9e80 100644 --- a/src/main/java/com/iemr/admin/controller/health/HealthController.java +++ b/src/main/java/com/iemr/admin/controller/health/HealthController.java @@ -21,40 +21,66 @@ */ package com.iemr.admin.controller.health; +import java.time.Instant; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.iemr.admin.service.health.HealthService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; @RestController +@RequestMapping("/health") +@Tag(name = "Health Check", description = "APIs for checking infrastructure health status") public class HealthController { private static final Logger logger = LoggerFactory.getLogger(HealthController.class); - @Autowired - private HealthService healthService; + private final HealthService healthService; + + public HealthController(HealthService healthService) { + this.healthService = healthService; + } - @Operation(summary = "Health check endpoint") - @GetMapping("/health") - public ResponseEntity> health() { - logger.info("Health check endpoint called"); - - Map healthStatus = healthService.checkHealth(); + @GetMapping + @Operation(summary = "Check infrastructure health", + description = "Returns the health status of MySQL, Redis, and other configured services") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Services are UP or DEGRADED (operational with warnings)"), + @ApiResponse(responseCode = "503", description = "One or more critical services are DOWN") + }) + public ResponseEntity> checkHealth() { + logger.debug("Health check endpoint called"); - // Return 503 if any service is down, 200 if all are up - String status = (String) healthStatus.get("status"); - HttpStatus httpStatus = "UP".equals(status) ? HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE; - - logger.info("Health check completed with status: {}", status); - return ResponseEntity.status(httpStatus).body(healthStatus); + try { + Map healthStatus = healthService.checkHealth(); + String overallStatus = (String) healthStatus.get("status"); + + HttpStatus httpStatus = "DOWN".equals(overallStatus) ? HttpStatus.SERVICE_UNAVAILABLE : HttpStatus.OK; + + logger.debug("Health check completed with status: {}", overallStatus); + return new ResponseEntity<>(healthStatus, httpStatus); + + } catch (Exception e) { + logger.error("Unexpected error during health check", e); + + Map errorResponse = Map.of( + "status", "DOWN", + "timestamp", Instant.now().toString() + ); + + return new ResponseEntity<>(errorResponse, HttpStatus.SERVICE_UNAVAILABLE); + } } } + diff --git a/src/main/java/com/iemr/admin/controller/version/VersionController.java b/src/main/java/com/iemr/admin/controller/version/VersionController.java index 67f7c74..27679a6 100644 --- a/src/main/java/com/iemr/admin/controller/version/VersionController.java +++ b/src/main/java/com/iemr/admin/controller/version/VersionController.java @@ -19,35 +19,59 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ + package com.iemr.admin.controller.version; import java.util.Map; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; - -import com.iemr.admin.service.version.VersionService; - import io.swagger.v3.oas.annotations.Operation; +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedHashMap; +import java.util.Properties; +import org.springframework.http.MediaType; @RestController public class VersionController { - private static final Logger logger = LoggerFactory.getLogger(VersionController.class); + private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); + + private static final String UNKNOWN_VALUE = "unknown"; - @Autowired - private VersionService versionService; + @Operation(summary = "Get version information") + @GetMapping(value = "/version", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> versionInformation() { + Map response = new LinkedHashMap<>(); + try { + logger.info("version Controller Start"); + Properties gitProperties = loadGitProperties(); + response.put("buildTimestamp", gitProperties.getProperty("git.build.time", UNKNOWN_VALUE)); + response.put("version", gitProperties.getProperty("git.build.version", UNKNOWN_VALUE)); + response.put("branch", gitProperties.getProperty("git.branch", UNKNOWN_VALUE)); + response.put("commitHash", gitProperties.getProperty("git.commit.id.abbrev", UNKNOWN_VALUE)); + } catch (Exception e) { + logger.error("Failed to load version information", e); + response.put("buildTimestamp", UNKNOWN_VALUE); + response.put("version", UNKNOWN_VALUE); + response.put("branch", UNKNOWN_VALUE); + response.put("commitHash", UNKNOWN_VALUE); + } + logger.info("version Controller End"); + return ResponseEntity.ok(response); + } - @Operation(summary = "Version information") - @GetMapping(value = "/version", produces = MediaType.APPLICATION_JSON_VALUE) - public Map versionInformation() { - logger.info("version Controller Start"); - Map versionInfo = versionService.getVersionInfo(); - logger.info("version Controller End"); - return versionInfo; - } + private Properties loadGitProperties() throws IOException { + Properties properties = new Properties(); + try (InputStream input = getClass().getClassLoader() + .getResourceAsStream("git.properties")) { + if (input != null) { + properties.load(input); + } + } + return properties; + } } diff --git a/src/main/java/com/iemr/admin/service/health/HealthService.java b/src/main/java/com/iemr/admin/service/health/HealthService.java index ae41b8e..f80d846 100644 --- a/src/main/java/com/iemr/admin/service/health/HealthService.java +++ b/src/main/java/com/iemr/admin/service/health/HealthService.java @@ -19,20 +19,33 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ + package com.iemr.admin.service.health; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; -import java.util.HashMap; +import java.time.Instant; +import java.util.LinkedHashMap; import java.util.Map; - +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; +import jakarta.annotation.PreDestroy; import javax.sql.DataSource; - +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.HikariPoolMXBean; +import java.lang.management.ManagementFactory; +import javax.management.MBeanServer; +import javax.management.ObjectName; +import java.util.concurrent.locks.ReentrantReadWriteLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; @@ -40,80 +53,458 @@ public class HealthService { private static final Logger logger = LoggerFactory.getLogger(HealthService.class); - private static final String DB_HEALTH_CHECK_QUERY = "SELECT 1 as health_check"; - @Autowired - private DataSource dataSource; + private static final String STATUS_KEY = "status"; + private static final String STATUS_UP = "UP"; + private static final String STATUS_DOWN = "DOWN"; + private static final String STATUS_DEGRADED = "DEGRADED"; + private static final String SEVERITY_KEY = "severity"; + private static final String SEVERITY_OK = "OK"; + private static final String SEVERITY_WARNING = "WARNING"; + private static final String SEVERITY_CRITICAL = "CRITICAL"; + private static final String ERROR_KEY = "error"; + private static final String MESSAGE_KEY = "message"; + private static final String RESPONSE_TIME_KEY = "responseTimeMs"; + private static final long MYSQL_TIMEOUT_SECONDS = 3; + private static final long REDIS_TIMEOUT_SECONDS = 3; + + private static final long ADVANCED_CHECKS_THROTTLE_SECONDS = 30; + private static final long RESPONSE_TIME_THRESHOLD_MS = 2000; + + private static final String DIAGNOSTIC_LOCK_WAIT = "MYSQL_LOCK_WAIT"; + private static final String DIAGNOSTIC_DEADLOCK = "MYSQL_DEADLOCK"; + private static final String DIAGNOSTIC_SLOW_QUERIES = "MYSQL_SLOW_QUERIES"; + private static final String DIAGNOSTIC_POOL_EXHAUSTED = "MYSQL_POOL_EXHAUSTED"; + private static final String DIAGNOSTIC_LOG_TEMPLATE = "Diagnostic: {}"; - @Autowired(required = false) - private RedisTemplate redisTemplate; + private final DataSource dataSource; + private final RedisTemplate redisTemplate; + private final ExecutorService executorService; + + private volatile long lastAdvancedCheckTime = 0; + private volatile AdvancedCheckResult cachedAdvancedCheckResult = null; + private final ReentrantReadWriteLock advancedCheckLock = new ReentrantReadWriteLock(); + + private volatile boolean deadlockCheckDisabled = false; + + private static final boolean ADVANCED_HEALTH_CHECKS_ENABLED = true; - public Map checkHealth() { - Map healthStatus = new HashMap<>(); - boolean overallHealth = true; + public HealthService(DataSource dataSource, + @Autowired(required = false) RedisTemplate redisTemplate) { + this.dataSource = dataSource; + this.redisTemplate = redisTemplate; + this.executorService = Executors.newFixedThreadPool(2); + } - // Check database connectivity (details logged internally, not exposed) - boolean dbHealthy = checkDatabaseHealthInternal(); - if (!dbHealthy) { - overallHealth = false; + @PreDestroy + public void shutdown() { + if (executorService != null && !executorService.isShutdown()) { + try { + executorService.shutdown(); + if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { + executorService.shutdownNow(); + logger.warn("ExecutorService did not terminate gracefully"); + } + } catch (InterruptedException e) { + executorService.shutdownNow(); + Thread.currentThread().interrupt(); + logger.warn("ExecutorService shutdown interrupted", e); + } } + } - // Check Redis connectivity if configured (details logged internally) - if (redisTemplate != null) { - boolean redisHealthy = checkRedisHealthInternal(); - if (!redisHealthy) { - overallHealth = false; + public Map checkHealth() { + Map response = new LinkedHashMap<>(); + response.put("timestamp", Instant.now().toString()); + + Map mysqlStatus = new ConcurrentHashMap<>(); + Map redisStatus = new ConcurrentHashMap<>(); + + Future mysqlFuture = executorService.submit( + () -> performHealthCheck("MySQL", mysqlStatus, this::checkMySQLHealthSync)); + Future redisFuture = executorService.submit( + () -> performHealthCheck("Redis", redisStatus, this::checkRedisHealthSync)); + + long maxTimeout = Math.max(MYSQL_TIMEOUT_SECONDS, REDIS_TIMEOUT_SECONDS) + 1; + long deadlineNs = System.nanoTime() + TimeUnit.SECONDS.toNanos(maxTimeout); + try { + mysqlFuture.get(maxTimeout, TimeUnit.SECONDS); + long remainingNs = deadlineNs - System.nanoTime(); + if (remainingNs > 0) { + redisFuture.get(remainingNs, TimeUnit.NANOSECONDS); + } else { + redisFuture.cancel(true); } + } catch (TimeoutException e) { + logger.warn("Health check aggregate timeout after {} seconds", maxTimeout); + mysqlFuture.cancel(true); + redisFuture.cancel(true); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.warn("Health check was interrupted"); + mysqlFuture.cancel(true); + redisFuture.cancel(true); + } catch (Exception e) { + logger.warn("Health check execution error: {}", e.getMessage()); } + + ensurePopulated(mysqlStatus, "MySQL"); + ensurePopulated(redisStatus, "Redis"); + + Map> components = new LinkedHashMap<>(); + components.put("mysql", mysqlStatus); + components.put("redis", redisStatus); + + response.put("components", components); + + String overallStatus = computeOverallStatus(components); + response.put(STATUS_KEY, overallStatus); + + return response; + } - healthStatus.put("status", overallHealth ? "UP" : "DOWN"); + private void ensurePopulated(Map status, String componentName) { + if (!status.containsKey(STATUS_KEY)) { + status.put(STATUS_KEY, STATUS_DOWN); + status.put(SEVERITY_KEY, SEVERITY_CRITICAL); + status.put(ERROR_KEY, componentName + " health check did not complete in time"); + } + } - logger.info("Health check completed - Overall status: {}", overallHealth ? "UP" : "DOWN"); - return healthStatus; + private HealthCheckResult checkMySQLHealthSync() { + try (Connection connection = dataSource.getConnection(); + PreparedStatement stmt = connection.prepareStatement("SELECT 1 as health_check")) { + + stmt.setQueryTimeout((int) MYSQL_TIMEOUT_SECONDS); + + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + boolean isDegraded = performAdvancedMySQLChecksWithThrottle(connection); + return new HealthCheckResult(true, null, isDegraded); + } + } + + return new HealthCheckResult(false, "No result from health check query", false); + + } catch (Exception e) { + logger.warn("MySQL health check failed: {}", e.getMessage(), e); + return new HealthCheckResult(false, "MySQL connection failed", false); + } } - private boolean checkDatabaseHealthInternal() { - long startTime = System.currentTimeMillis(); + private HealthCheckResult checkRedisHealthSync() { + if (redisTemplate == null) { + return new HealthCheckResult(true, "Redis not configured — skipped", false); + } - try (Connection connection = dataSource.getConnection()) { - boolean isConnectionValid = connection.isValid(2); // 2 second timeout per best practices - - if (isConnectionValid) { - try (PreparedStatement stmt = connection.prepareStatement(DB_HEALTH_CHECK_QUERY)) { - stmt.setQueryTimeout(3); // 3 second query timeout - try (ResultSet rs = stmt.executeQuery()) { - if (rs.next() && rs.getInt(1) == 1) { - long responseTime = System.currentTimeMillis() - startTime; - logger.debug("Database health check: UP ({}ms)", responseTime); - return true; - } - } - } + try { + String pong = redisTemplate.execute((org.springframework.data.redis.core.RedisCallback) (connection) -> connection.ping()); + + if ("PONG".equals(pong)) { + return new HealthCheckResult(true, null, false); } - logger.warn("Database health check: Connection not valid"); - return false; + + return new HealthCheckResult(false, "Redis PING failed", false); + } catch (Exception e) { - logger.error("Database health check failed: {}", e.getMessage()); - return false; + logger.warn("Redis health check failed: {}", e.getMessage(), e); + return new HealthCheckResult(false, "Redis connection failed", false); } } - private boolean checkRedisHealthInternal() { + private Map performHealthCheck(String componentName, + Map status, + Supplier checker) { long startTime = System.currentTimeMillis(); try { - String pong = redisTemplate.execute((RedisCallback) connection -> connection.ping()); + HealthCheckResult result = checker.get(); + long responseTime = System.currentTimeMillis() - startTime; - if ("PONG".equals(pong)) { - long responseTime = System.currentTimeMillis() - startTime; - logger.debug("Redis health check: UP ({}ms)", responseTime); - return true; + String componentStatus; + if (!result.isHealthy) { + componentStatus = STATUS_DOWN; + } else if (result.isDegraded) { + componentStatus = STATUS_DEGRADED; + } else { + componentStatus = STATUS_UP; + } + status.put(STATUS_KEY, componentStatus); + + status.put(RESPONSE_TIME_KEY, responseTime); + + String severity = determineSeverity(result.isHealthy, responseTime, result.isDegraded); + status.put(SEVERITY_KEY, severity); + + if (result.error != null) { + String fieldKey = result.isHealthy ? MESSAGE_KEY : ERROR_KEY; + status.put(fieldKey, result.error); + } + + return status; + + } catch (Exception e) { + long responseTime = System.currentTimeMillis() - startTime; + logger.error("{} health check failed with exception: {}", componentName, e.getMessage(), e); + + status.put(STATUS_KEY, STATUS_DOWN); + status.put(RESPONSE_TIME_KEY, responseTime); + status.put(SEVERITY_KEY, SEVERITY_CRITICAL); + status.put(ERROR_KEY, "Health check failed with an unexpected error"); + + return status; + } + } + + private String determineSeverity(boolean isHealthy, long responseTimeMs, boolean isDegraded) { + if (!isHealthy) { + return SEVERITY_CRITICAL; + } + + if (isDegraded) { + return SEVERITY_WARNING; + } + + if (responseTimeMs > RESPONSE_TIME_THRESHOLD_MS) { + return SEVERITY_WARNING; + } + + return SEVERITY_OK; + } + + private String computeOverallStatus(Map> components) { + boolean hasCritical = false; + boolean hasDegraded = false; + + for (Map componentStatus : components.values()) { + String status = (String) componentStatus.get(STATUS_KEY); + String severity = (String) componentStatus.get(SEVERITY_KEY); + + if (STATUS_DOWN.equals(status) || SEVERITY_CRITICAL.equals(severity)) { + hasCritical = true; } - logger.warn("Redis health check: Ping returned unexpected response"); + + if (STATUS_DEGRADED.equals(status)) { + hasDegraded = true; + } + + if (SEVERITY_WARNING.equals(severity)) { + hasDegraded = true; + } + } + + if (hasCritical) { + return STATUS_DOWN; + } + + if (hasDegraded) { + return STATUS_DEGRADED; + } + + return STATUS_UP; + } + + private boolean performAdvancedMySQLChecksWithThrottle(Connection connection) { + if (!ADVANCED_HEALTH_CHECKS_ENABLED) { return false; + } + + long currentTime = System.currentTimeMillis(); + + advancedCheckLock.readLock().lock(); + try { + if (cachedAdvancedCheckResult != null && + (currentTime - lastAdvancedCheckTime) < ADVANCED_CHECKS_THROTTLE_SECONDS * 1000) { + return cachedAdvancedCheckResult.isDegraded; + } + } finally { + advancedCheckLock.readLock().unlock(); + } + + advancedCheckLock.writeLock().lock(); + try { + if (cachedAdvancedCheckResult != null && + (currentTime - lastAdvancedCheckTime) < ADVANCED_CHECKS_THROTTLE_SECONDS * 1000) { + return cachedAdvancedCheckResult.isDegraded; + } + + AdvancedCheckResult result = performAdvancedMySQLChecks(connection); + + lastAdvancedCheckTime = currentTime; + cachedAdvancedCheckResult = result; + + return result.isDegraded; + } finally { + advancedCheckLock.writeLock().unlock(); + } + } + + private AdvancedCheckResult performAdvancedMySQLChecks(Connection connection) { + try { + boolean hasIssues = false; + + if (hasLockWaits(connection)) { + logger.warn(DIAGNOSTIC_LOG_TEMPLATE, DIAGNOSTIC_LOCK_WAIT); + hasIssues = true; + } + + if (hasDeadlocks(connection)) { + logger.warn(DIAGNOSTIC_LOG_TEMPLATE, DIAGNOSTIC_DEADLOCK); + hasIssues = true; + } + + if (hasSlowQueries(connection)) { + logger.warn(DIAGNOSTIC_LOG_TEMPLATE, DIAGNOSTIC_SLOW_QUERIES); + hasIssues = true; + } + + if (hasConnectionPoolExhaustion()) { + logger.warn(DIAGNOSTIC_LOG_TEMPLATE, DIAGNOSTIC_POOL_EXHAUSTED); + hasIssues = true; + } + + return new AdvancedCheckResult(hasIssues); } catch (Exception e) { - logger.error("Redis health check failed: {}", e.getMessage()); + logger.debug("Advanced MySQL checks encountered exception, marking degraded"); + return new AdvancedCheckResult(true); + } + } + + private boolean hasLockWaits(Connection connection) { + try (PreparedStatement stmt = connection.prepareStatement( + "SELECT COUNT(*) FROM INFORMATION_SCHEMA.PROCESSLIST " + + "WHERE (state = 'Waiting for table metadata lock' " + + " OR state = 'Waiting for row lock' " + + " OR state = 'Waiting for lock') " + + "AND user = USER()")) { + stmt.setQueryTimeout(2); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + int lockCount = rs.getInt(1); + return lockCount > 0; + } + } + } catch (Exception e) { + logger.debug("Could not check for lock waits"); + } + return false; + } + + private boolean hasDeadlocks(Connection connection) { + if (deadlockCheckDisabled) { return false; } + + try (PreparedStatement stmt = connection.prepareStatement("SHOW ENGINE INNODB STATUS")) { + stmt.setQueryTimeout(2); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + String innodbStatus = rs.getString(3); + return innodbStatus != null && innodbStatus.contains("LATEST DETECTED DEADLOCK"); + } + } + } catch (java.sql.SQLException e) { + if (e.getErrorCode() == 1142 || e.getErrorCode() == 1227) { + deadlockCheckDisabled = true; + logger.warn("Deadlock check disabled: Insufficient privileges"); + } else { + logger.debug("Could not check for deadlocks"); + } + } catch (Exception e) { + logger.debug("Could not check for deadlocks"); + } + return false; + } + + private boolean hasSlowQueries(Connection connection) { + try (PreparedStatement stmt = connection.prepareStatement( + "SELECT COUNT(*) FROM INFORMATION_SCHEMA.PROCESSLIST " + + "WHERE command != 'Sleep' AND time > ? AND user NOT IN ('event_scheduler', 'system user')")) { + stmt.setQueryTimeout(2); + stmt.setInt(1, 10); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + int slowQueryCount = rs.getInt(1); + return slowQueryCount > 3; + } + } + } catch (Exception e) { + logger.debug("Could not check for slow queries"); + } + return false; + } + + private boolean hasConnectionPoolExhaustion() { + if (dataSource instanceof HikariDataSource hikariDataSource) { + try { + HikariPoolMXBean poolMXBean = hikariDataSource.getHikariPoolMXBean(); + + if (poolMXBean != null) { + int activeConnections = poolMXBean.getActiveConnections(); + int maxPoolSize = hikariDataSource.getMaximumPoolSize(); + + int threshold = (int) (maxPoolSize * 0.8); + return activeConnections > threshold; + } + } catch (Exception e) { + logger.debug("Could not retrieve HikariCP pool metrics"); + } + } + + return checkPoolMetricsViaJMX(); + } + + private boolean checkPoolMetricsViaJMX() { + try { + MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); + ObjectName objectName = new ObjectName("com.zaxxer.hikari:type=Pool (*)"); + var mBeans = mBeanServer.queryMBeans(objectName, null); + + for (var mBean : mBeans) { + if (evaluatePoolMetrics(mBeanServer, mBean.getObjectName())) { + return true; + } + } + } catch (Exception e) { + logger.debug("Could not access HikariCP pool metrics via JMX"); + } + + logger.debug("Pool exhaustion check disabled: HikariCP metrics unavailable"); + return false; + } + + private boolean evaluatePoolMetrics(MBeanServer mBeanServer, ObjectName objectName) { + try { + Integer activeConnections = (Integer) mBeanServer.getAttribute(objectName, "ActiveConnections"); + Integer maximumPoolSize = (Integer) mBeanServer.getAttribute(objectName, "MaximumPoolSize"); + + if (activeConnections != null && maximumPoolSize != null) { + int threshold = (int) (maximumPoolSize * 0.8); + return activeConnections > threshold; + } + } catch (Exception e) { + // Continue to next MBean + } + return false; + } + + private static class AdvancedCheckResult { + final boolean isDegraded; + + AdvancedCheckResult(boolean isDegraded) { + this.isDegraded = isDegraded; + } + } + + private static class HealthCheckResult { + final boolean isHealthy; + final String error; + final boolean isDegraded; + + HealthCheckResult(boolean isHealthy, String error, boolean isDegraded) { + this.isHealthy = isHealthy; + this.error = error; + this.isDegraded = isDegraded; + } } } diff --git a/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java index 9619c40..b1a3595 100644 --- a/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java @@ -85,7 +85,9 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo || path.startsWith(contextPath + "/swagger-ui") || path.startsWith(contextPath + "/v3/api-docs") || path.startsWith(contextPath + "/user/refreshToken") - || path.startsWith(contextPath + "/public")) { + || path.startsWith(contextPath + "/public") + || path.equals(contextPath + "/health") + || path.equals(contextPath + "/version")) { logger.info("Skipping filter for path: " + path); filterChain.doFilter(servletRequest, servletResponse); return; From 4c93556ca14da472445b27033982bdfb49b3b980 Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Sat, 21 Feb 2026 13:16:54 +0530 Subject: [PATCH 2/8] fix(health): add constant and remove duplicates --- .../iemr/admin/utils/JwtUserIdValidationFilter.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java index b1a3595..0e6f6f8 100644 --- a/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/admin/utils/JwtUserIdValidationFilter.java @@ -19,6 +19,9 @@ public class JwtUserIdValidationFilter implements Filter { + private static final String HEALTH_ENDPOINT = "/health"; + private static final String VERSION_ENDPOINT = "/version"; + private final JwtAuthenticationUtil jwtAuthenticationUtil; private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); private final String allowedOrigins; @@ -39,8 +42,8 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo String contextPath = request.getContextPath(); // FIRST: Check for health and version endpoints - skip ALL processing - if (path.equals("/health") || path.equals("/version") || - path.equals(contextPath + "/health") || path.equals(contextPath + "/version")) { + if (path.equals(HEALTH_ENDPOINT) || path.equals(VERSION_ENDPOINT) || + path.equals(contextPath + HEALTH_ENDPOINT) || path.equals(contextPath + VERSION_ENDPOINT)) { logger.info("Skipping JWT validation for monitoring endpoint: {}", path); filterChain.doFilter(servletRequest, servletResponse); return; @@ -86,8 +89,8 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo || path.startsWith(contextPath + "/v3/api-docs") || path.startsWith(contextPath + "/user/refreshToken") || path.startsWith(contextPath + "/public") - || path.equals(contextPath + "/health") - || path.equals(contextPath + "/version")) { + || path.equals(contextPath + HEALTH_ENDPOINT) + || path.equals(contextPath + VERSION_ENDPOINT)) { logger.info("Skipping filter for path: " + path); filterChain.doFilter(servletRequest, servletResponse); return; From 593c52574b597ae54eebc97002529efda871aaa1 Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Sat, 21 Feb 2026 13:25:56 +0530 Subject: [PATCH 3/8] fix(health): avoid permanent DEGRADED from historical deadlocks --- pom.xml | 6 --- .../admin/service/health/HealthService.java | 53 +++++-------------- 2 files changed, 13 insertions(+), 46 deletions(-) diff --git a/pom.xml b/pom.xml index 9e53417..7541207 100644 --- a/pom.xml +++ b/pom.xml @@ -291,12 +291,6 @@ nvd - - org.apache.maven.plugins - maven-jar-plugin - 3.0.2 - - org.apache.maven.plugins maven-jar-plugin diff --git a/src/main/java/com/iemr/admin/service/health/HealthService.java b/src/main/java/com/iemr/admin/service/health/HealthService.java index f80d846..02aa60b 100644 --- a/src/main/java/com/iemr/admin/service/health/HealthService.java +++ b/src/main/java/com/iemr/admin/service/health/HealthService.java @@ -85,8 +85,6 @@ public class HealthService { private volatile AdvancedCheckResult cachedAdvancedCheckResult = null; private final ReentrantReadWriteLock advancedCheckLock = new ReentrantReadWriteLock(); - private volatile boolean deadlockCheckDisabled = false; - private static final boolean ADVANCED_HEALTH_CHECKS_ENABLED = true; public HealthService(DataSource dataSource, @@ -179,7 +177,7 @@ private HealthCheckResult checkMySQLHealthSync() { try (ResultSet rs = stmt.executeQuery()) { if (rs.next()) { - boolean isDegraded = performAdvancedMySQLChecksWithThrottle(connection); + boolean isDegraded = performAdvancedMySQLChecksWithThrottle(); return new HealthCheckResult(true, null, isDegraded); } } @@ -304,7 +302,7 @@ private String computeOverallStatus(Map> components) return STATUS_UP; } - private boolean performAdvancedMySQLChecksWithThrottle(Connection connection) { + private boolean performAdvancedMySQLChecksWithThrottle() { if (!ADVANCED_HEALTH_CHECKS_ENABLED) { return false; } @@ -328,12 +326,17 @@ private boolean performAdvancedMySQLChecksWithThrottle(Connection connection) { return cachedAdvancedCheckResult.isDegraded; } - AdvancedCheckResult result = performAdvancedMySQLChecks(connection); - - lastAdvancedCheckTime = currentTime; - cachedAdvancedCheckResult = result; - - return result.isDegraded; + // Acquire a fresh connection for advanced checks to avoid pool exhaustion + try (Connection connection = dataSource.getConnection()) { + AdvancedCheckResult result = performAdvancedMySQLChecks(connection); + lastAdvancedCheckTime = currentTime; + cachedAdvancedCheckResult = result; + return result.isDegraded; + } + } catch (Exception e) { + logger.debug("Failed to get connection for advanced checks: {}", e.getMessage()); + // Return cached result or false if no cache + return cachedAdvancedCheckResult != null ? cachedAdvancedCheckResult.isDegraded : false; } finally { advancedCheckLock.writeLock().unlock(); } @@ -348,10 +351,6 @@ private AdvancedCheckResult performAdvancedMySQLChecks(Connection connection) { hasIssues = true; } - if (hasDeadlocks(connection)) { - logger.warn(DIAGNOSTIC_LOG_TEMPLATE, DIAGNOSTIC_DEADLOCK); - hasIssues = true; - } if (hasSlowQueries(connection)) { logger.warn(DIAGNOSTIC_LOG_TEMPLATE, DIAGNOSTIC_SLOW_QUERIES); @@ -390,32 +389,6 @@ private boolean hasLockWaits(Connection connection) { return false; } - private boolean hasDeadlocks(Connection connection) { - if (deadlockCheckDisabled) { - return false; - } - - try (PreparedStatement stmt = connection.prepareStatement("SHOW ENGINE INNODB STATUS")) { - stmt.setQueryTimeout(2); - try (ResultSet rs = stmt.executeQuery()) { - if (rs.next()) { - String innodbStatus = rs.getString(3); - return innodbStatus != null && innodbStatus.contains("LATEST DETECTED DEADLOCK"); - } - } - } catch (java.sql.SQLException e) { - if (e.getErrorCode() == 1142 || e.getErrorCode() == 1227) { - deadlockCheckDisabled = true; - logger.warn("Deadlock check disabled: Insufficient privileges"); - } else { - logger.debug("Could not check for deadlocks"); - } - } catch (Exception e) { - logger.debug("Could not check for deadlocks"); - } - return false; - } - private boolean hasSlowQueries(Connection connection) { try (PreparedStatement stmt = connection.prepareStatement( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.PROCESSLIST " + From 6ac42c114bfc622bc09a646486d4b384c514f734 Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Sat, 21 Feb 2026 13:28:46 +0530 Subject: [PATCH 4/8] fix(health): Removed the unnecessary boolean literal --- src/main/java/com/iemr/admin/service/health/HealthService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/admin/service/health/HealthService.java b/src/main/java/com/iemr/admin/service/health/HealthService.java index 02aa60b..f651143 100644 --- a/src/main/java/com/iemr/admin/service/health/HealthService.java +++ b/src/main/java/com/iemr/admin/service/health/HealthService.java @@ -336,7 +336,7 @@ private boolean performAdvancedMySQLChecksWithThrottle() { } catch (Exception e) { logger.debug("Failed to get connection for advanced checks: {}", e.getMessage()); // Return cached result or false if no cache - return cachedAdvancedCheckResult != null ? cachedAdvancedCheckResult.isDegraded : false; + return cachedAdvancedCheckResult != null && cachedAdvancedCheckResult.isDegraded; } finally { advancedCheckLock.writeLock().unlock(); } From 5be92a54e5993fa4a127b8acf9194ee6eeecbf92 Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Sat, 21 Feb 2026 14:09:43 +0530 Subject: [PATCH 5/8] fix(health): Fixed the broken lock-wait detection --- src/main/java/com/iemr/admin/service/health/HealthService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/admin/service/health/HealthService.java b/src/main/java/com/iemr/admin/service/health/HealthService.java index f651143..1586997 100644 --- a/src/main/java/com/iemr/admin/service/health/HealthService.java +++ b/src/main/java/com/iemr/admin/service/health/HealthService.java @@ -375,7 +375,7 @@ private boolean hasLockWaits(Connection connection) { "WHERE (state = 'Waiting for table metadata lock' " + " OR state = 'Waiting for row lock' " + " OR state = 'Waiting for lock') " + - "AND user = USER()")) { + "AND user = SUBSTRING_INDEX(USER(), '@', 1)")) { stmt.setQueryTimeout(2); try (ResultSet rs = stmt.executeQuery()) { if (rs.next()) { From 5c82af190c537603b45947037ac7b4e57e6fd2fb Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Sun, 22 Feb 2026 21:37:14 +0530 Subject: [PATCH 6/8] fix(health): avoid blocking DB I/O under write lock and restore interrupt flag --- .../admin/service/health/HealthService.java | 123 +++++++++++------- 1 file changed, 79 insertions(+), 44 deletions(-) diff --git a/src/main/java/com/iemr/admin/service/health/HealthService.java b/src/main/java/com/iemr/admin/service/health/HealthService.java index 1586997..89bc923 100644 --- a/src/main/java/com/iemr/admin/service/health/HealthService.java +++ b/src/main/java/com/iemr/admin/service/health/HealthService.java @@ -29,11 +29,13 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.ExecutionException; import java.util.function.Supplier; import jakarta.annotation.PreDestroy; import javax.sql.DataSource; @@ -72,7 +74,6 @@ public class HealthService { private static final long RESPONSE_TIME_THRESHOLD_MS = 2000; private static final String DIAGNOSTIC_LOCK_WAIT = "MYSQL_LOCK_WAIT"; - private static final String DIAGNOSTIC_DEADLOCK = "MYSQL_DEADLOCK"; private static final String DIAGNOSTIC_SLOW_QUERIES = "MYSQL_SLOW_QUERIES"; private static final String DIAGNOSTIC_POOL_EXHAUSTED = "MYSQL_POOL_EXHAUSTED"; private static final String DIAGNOSTIC_LOG_TEMPLATE = "Diagnostic: {}"; @@ -84,6 +85,7 @@ public class HealthService { private volatile long lastAdvancedCheckTime = 0; private volatile AdvancedCheckResult cachedAdvancedCheckResult = null; private final ReentrantReadWriteLock advancedCheckLock = new ReentrantReadWriteLock(); + private final AtomicBoolean advancedCheckInProgress = new AtomicBoolean(false); private static final boolean ADVANCED_HEALTH_CHECKS_ENABLED = true; @@ -91,7 +93,7 @@ public HealthService(DataSource dataSource, @Autowired(required = false) RedisTemplate redisTemplate) { this.dataSource = dataSource; this.redisTemplate = redisTemplate; - this.executorService = Executors.newFixedThreadPool(2); + this.executorService = Executors.newFixedThreadPool(6); } @PreDestroy @@ -118,32 +120,8 @@ public Map checkHealth() { Map mysqlStatus = new ConcurrentHashMap<>(); Map redisStatus = new ConcurrentHashMap<>(); - Future mysqlFuture = executorService.submit( - () -> performHealthCheck("MySQL", mysqlStatus, this::checkMySQLHealthSync)); - Future redisFuture = executorService.submit( - () -> performHealthCheck("Redis", redisStatus, this::checkRedisHealthSync)); - - long maxTimeout = Math.max(MYSQL_TIMEOUT_SECONDS, REDIS_TIMEOUT_SECONDS) + 1; - long deadlineNs = System.nanoTime() + TimeUnit.SECONDS.toNanos(maxTimeout); - try { - mysqlFuture.get(maxTimeout, TimeUnit.SECONDS); - long remainingNs = deadlineNs - System.nanoTime(); - if (remainingNs > 0) { - redisFuture.get(remainingNs, TimeUnit.NANOSECONDS); - } else { - redisFuture.cancel(true); - } - } catch (TimeoutException e) { - logger.warn("Health check aggregate timeout after {} seconds", maxTimeout); - mysqlFuture.cancel(true); - redisFuture.cancel(true); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - logger.warn("Health check was interrupted"); - mysqlFuture.cancel(true); - redisFuture.cancel(true); - } catch (Exception e) { - logger.warn("Health check execution error: {}", e.getMessage()); + if (!executorService.isShutdown()) { + performHealthChecks(mysqlStatus, redisStatus); } ensurePopulated(mysqlStatus, "MySQL"); @@ -154,13 +132,56 @@ public Map checkHealth() { components.put("redis", redisStatus); response.put("components", components); - - String overallStatus = computeOverallStatus(components); - response.put(STATUS_KEY, overallStatus); + response.put(STATUS_KEY, computeOverallStatus(components)); return response; } + private void performHealthChecks(Map mysqlStatus, Map redisStatus) { + Future mysqlFuture = null; + Future redisFuture = null; + try { + mysqlFuture = executorService.submit( + () -> performHealthCheck("MySQL", mysqlStatus, this::checkMySQLHealthSync)); + redisFuture = executorService.submit( + () -> performHealthCheck("Redis", redisStatus, this::checkRedisHealthSync)); + + awaitHealthChecks(mysqlFuture, redisFuture); + } catch (TimeoutException e) { + logger.warn("Health check aggregate timeout after {} seconds", getMaxTimeout()); + cancelFutures(mysqlFuture, redisFuture); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.warn("Health check was interrupted"); + cancelFutures(mysqlFuture, redisFuture); + } catch (Exception e) { + logger.warn("Health check execution error: {}", e.getMessage()); + } + } + + private void awaitHealthChecks(Future mysqlFuture, Future redisFuture) throws TimeoutException, InterruptedException, ExecutionException { + long maxTimeout = getMaxTimeout(); + long deadlineNs = System.nanoTime() + TimeUnit.SECONDS.toNanos(maxTimeout); + + mysqlFuture.get(maxTimeout, TimeUnit.SECONDS); + long remainingNs = deadlineNs - System.nanoTime(); + + if (remainingNs > 0) { + redisFuture.get(remainingNs, TimeUnit.NANOSECONDS); + } else { + redisFuture.cancel(true); + } + } + + private long getMaxTimeout() { + return Math.max(MYSQL_TIMEOUT_SECONDS, REDIS_TIMEOUT_SECONDS) + 1; + } + + private void cancelFutures(Future mysqlFuture, Future redisFuture) { + if (mysqlFuture != null) mysqlFuture.cancel(true); + if (redisFuture != null) redisFuture.cancel(true); + } + private void ensurePopulated(Map status, String componentName) { if (!status.containsKey(STATUS_KEY)) { status.put(STATUS_KEY, STATUS_DOWN); @@ -319,26 +340,40 @@ private boolean performAdvancedMySQLChecksWithThrottle() { advancedCheckLock.readLock().unlock(); } - advancedCheckLock.writeLock().lock(); + // Only one thread may submit; others fall back to the (stale) cache + if (!advancedCheckInProgress.compareAndSet(false, true)) { + advancedCheckLock.readLock().lock(); + try { + return cachedAdvancedCheckResult != null && cachedAdvancedCheckResult.isDegraded; + } finally { + advancedCheckLock.readLock().unlock(); + } + } + try { - if (cachedAdvancedCheckResult != null && - (currentTime - lastAdvancedCheckTime) < ADVANCED_CHECKS_THROTTLE_SECONDS * 1000) { - return cachedAdvancedCheckResult.isDegraded; + // Perform DB I/O outside the write lock to avoid lock contention + AdvancedCheckResult result; + try (Connection connection = dataSource.getConnection()) { + result = performAdvancedMySQLChecks(connection); + } catch (Exception e) { + if (e.getCause() instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + logger.debug("Failed to get connection for advanced checks: {}", e.getMessage()); + result = new AdvancedCheckResult(false); } - // Acquire a fresh connection for advanced checks to avoid pool exhaustion - try (Connection connection = dataSource.getConnection()) { - AdvancedCheckResult result = performAdvancedMySQLChecks(connection); + // Re-acquire write lock only to update the cache atomically + advancedCheckLock.writeLock().lock(); + try { lastAdvancedCheckTime = currentTime; cachedAdvancedCheckResult = result; return result.isDegraded; + } finally { + advancedCheckLock.writeLock().unlock(); } - } catch (Exception e) { - logger.debug("Failed to get connection for advanced checks: {}", e.getMessage()); - // Return cached result or false if no cache - return cachedAdvancedCheckResult != null && cachedAdvancedCheckResult.isDegraded; } finally { - advancedCheckLock.writeLock().unlock(); + advancedCheckInProgress.set(false); } } @@ -392,7 +427,7 @@ private boolean hasLockWaits(Connection connection) { private boolean hasSlowQueries(Connection connection) { try (PreparedStatement stmt = connection.prepareStatement( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.PROCESSLIST " + - "WHERE command != 'Sleep' AND time > ? AND user NOT IN ('event_scheduler', 'system user')")) { + "WHERE command != 'Sleep' AND time > ? AND user = SUBSTRING_INDEX(USER(), '@', 1)")) { stmt.setQueryTimeout(2); stmt.setInt(1, 10); try (ResultSet rs = stmt.executeQuery()) { From 64d7f3164d268ec3cfacaea0a36e38697dafc23f Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Sun, 22 Feb 2026 21:47:46 +0530 Subject: [PATCH 7/8] fix(health): add cancelFutures in healthservice --- src/main/java/com/iemr/admin/service/health/HealthService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/iemr/admin/service/health/HealthService.java b/src/main/java/com/iemr/admin/service/health/HealthService.java index 89bc923..10c9037 100644 --- a/src/main/java/com/iemr/admin/service/health/HealthService.java +++ b/src/main/java/com/iemr/admin/service/health/HealthService.java @@ -156,6 +156,7 @@ private void performHealthChecks(Map mysqlStatus, Map Date: Tue, 24 Feb 2026 11:10:35 +0530 Subject: [PATCH 8/8] fix(health): close basic DB connection before advanced checks and remove shared-map race --- .../admin/service/health/HealthService.java | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/iemr/admin/service/health/HealthService.java b/src/main/java/com/iemr/admin/service/health/HealthService.java index 10c9037..e6a0432 100644 --- a/src/main/java/com/iemr/admin/service/health/HealthService.java +++ b/src/main/java/com/iemr/admin/service/health/HealthService.java @@ -48,9 +48,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; +import java.util.concurrent.atomic.AtomicInteger; + @Service public class HealthService { @@ -87,13 +90,23 @@ public class HealthService { private final ReentrantReadWriteLock advancedCheckLock = new ReentrantReadWriteLock(); private final AtomicBoolean advancedCheckInProgress = new AtomicBoolean(false); - private static final boolean ADVANCED_HEALTH_CHECKS_ENABLED = true; + @Value("${health.advanced.checks.enabled:true}") + private boolean advancedHealthChecksEnabled; public HealthService(DataSource dataSource, @Autowired(required = false) RedisTemplate redisTemplate) { this.dataSource = dataSource; this.redisTemplate = redisTemplate; - this.executorService = Executors.newFixedThreadPool(6); + this.executorService = Executors.newFixedThreadPool(2, new java.util.concurrent.ThreadFactory() { + private final AtomicInteger threadCounter = new AtomicInteger(0); + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "health-check-" + threadCounter.incrementAndGet()); + t.setDaemon(true); + return t; + } + }); } @PreDestroy @@ -184,11 +197,10 @@ private void cancelFutures(Future mysqlFuture, Future redisFuture) { } private void ensurePopulated(Map status, String componentName) { - if (!status.containsKey(STATUS_KEY)) { - status.put(STATUS_KEY, STATUS_DOWN); - status.put(SEVERITY_KEY, SEVERITY_CRITICAL); - status.put(ERROR_KEY, componentName + " health check did not complete in time"); - } + + status.putIfAbsent(STATUS_KEY, STATUS_DOWN); + status.putIfAbsent(SEVERITY_KEY, SEVERITY_CRITICAL); + status.putIfAbsent(ERROR_KEY, componentName + " health check did not complete in time"); } private HealthCheckResult checkMySQLHealthSync() { @@ -199,17 +211,19 @@ private HealthCheckResult checkMySQLHealthSync() { try (ResultSet rs = stmt.executeQuery()) { if (rs.next()) { - boolean isDegraded = performAdvancedMySQLChecksWithThrottle(); - return new HealthCheckResult(true, null, isDegraded); + // Connection is auto-closed by try-with-resources here + // Advanced checks will open a separate connection if needed } } - return new HealthCheckResult(false, "No result from health check query", false); - } catch (Exception e) { logger.warn("MySQL health check failed: {}", e.getMessage(), e); return new HealthCheckResult(false, "MySQL connection failed", false); } + + + boolean isDegraded = performAdvancedMySQLChecksWithThrottle(); + return new HealthCheckResult(true, null, isDegraded); } private HealthCheckResult checkRedisHealthSync() { @@ -325,7 +339,7 @@ private String computeOverallStatus(Map> components) } private boolean performAdvancedMySQLChecksWithThrottle() { - if (!ADVANCED_HEALTH_CHECKS_ENABLED) { + if (!advancedHealthChecksEnabled) { return false; }