From 6e53e0e223bdfe7b358d07634312969c77f20816 Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Sat, 21 Feb 2026 13:40:57 +0530 Subject: [PATCH 1/4] feat(health,version): add health and version endpoints --- .../iemr/tm/service/health/HealthService.java | 239 +++++++----------- .../tm/utils/http/HTTPRequestInterceptor.java | 11 +- 2 files changed, 91 insertions(+), 159 deletions(-) diff --git a/src/main/java/com/iemr/tm/service/health/HealthService.java b/src/main/java/com/iemr/tm/service/health/HealthService.java index edf475e..1c8c13d 100644 --- a/src/main/java/com/iemr/tm/service/health/HealthService.java +++ b/src/main/java/com/iemr/tm/service/health/HealthService.java @@ -29,13 +29,11 @@ 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; @@ -67,21 +65,14 @@ public class HealthService { private static final String ERROR_KEY = "error"; private static final String MESSAGE_KEY = "message"; private static final String RESPONSE_TIME_KEY = "responseTimeMs"; - - // Component names - private static final String MYSQL_COMPONENT = "MySQL"; - private static final String REDIS_COMPONENT = "Redis"; - - // Timeouts (in seconds) private static final long MYSQL_TIMEOUT_SECONDS = 3; private static final long REDIS_TIMEOUT_SECONDS = 3; - // Advanced checks configuration - private static final long ADVANCED_CHECKS_TIMEOUT_MS = 500L; // ms — enforced below 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: {}"; @@ -89,26 +80,20 @@ public class HealthService { private final DataSource dataSource; private final RedisTemplate redisTemplate; private final ExecutorService executorService; - private final ExecutorService advancedCheckExecutor; private volatile long lastAdvancedCheckTime = 0; private volatile AdvancedCheckResult cachedAdvancedCheckResult = null; private final ReentrantReadWriteLock advancedCheckLock = new ReentrantReadWriteLock(); - private final AtomicBoolean advancedCheckInProgress = new AtomicBoolean(false); - // Advanced checks always enabled + private volatile boolean deadlockCheckDisabled = false; + private static final boolean ADVANCED_HEALTH_CHECKS_ENABLED = true; public HealthService(DataSource dataSource, @Autowired(required = false) RedisTemplate redisTemplate) { this.dataSource = dataSource; this.redisTemplate = redisTemplate; - this.executorService = Executors.newFixedThreadPool(6); - this.advancedCheckExecutor = Executors.newSingleThreadExecutor(r -> { - Thread t = new Thread(r, "health-advanced-check"); - t.setDaemon(true); - return t; - }); + this.executorService = Executors.newFixedThreadPool(2); } @PreDestroy @@ -126,9 +111,6 @@ public void shutdown() { logger.warn("ExecutorService shutdown interrupted", e); } } - if (advancedCheckExecutor != null && !advancedCheckExecutor.isShutdown()) { - advancedCheckExecutor.shutdownNow(); - } } public Map checkHealth() { @@ -138,67 +120,47 @@ public Map checkHealth() { Map mysqlStatus = new ConcurrentHashMap<>(); Map redisStatus = new ConcurrentHashMap<>(); - if (!executorService.isShutdown()) { - performHealthChecks(mysqlStatus, redisStatus); - } - - ensurePopulated(mysqlStatus, MYSQL_COMPONENT); - ensurePopulated(redisStatus, REDIS_COMPONENT); - - Map> components = new LinkedHashMap<>(); - components.put("mysql", mysqlStatus); - components.put("redis", redisStatus); - - response.put("components", components); - response.put(STATUS_KEY, computeOverallStatus(components)); + Future mysqlFuture = executorService.submit( + () -> performHealthCheck("MySQL", mysqlStatus, this::checkMySQLHealthSync)); + Future redisFuture = executorService.submit( + () -> performHealthCheck("Redis", redisStatus, this::checkRedisHealthSync)); - return response; - } - - private void performHealthChecks(Map mysqlStatus, Map redisStatus) { - Future mysqlFuture = null; - Future redisFuture = null; + long maxTimeout = Math.max(MYSQL_TIMEOUT_SECONDS, REDIS_TIMEOUT_SECONDS) + 1; + long deadlineNs = System.nanoTime() + TimeUnit.SECONDS.toNanos(maxTimeout); try { - mysqlFuture = executorService.submit( - () -> performHealthCheck(MYSQL_COMPONENT, mysqlStatus, this::checkMySQLHealthSync)); - redisFuture = executorService.submit( - () -> performHealthCheck(REDIS_COMPONENT, redisStatus, this::checkRedisHealthSync)); - - awaitHealthChecks(mysqlFuture, redisFuture); + 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", getMaxTimeout()); - cancelFutures(mysqlFuture, redisFuture); + 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"); - cancelFutures(mysqlFuture, redisFuture); + mysqlFuture.cancel(true); + redisFuture.cancel(true); } catch (Exception e) { logger.warn("Health check execution error: {}", e.getMessage()); - cancelFutures(mysqlFuture, redisFuture); } - } - - 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(); + ensurePopulated(mysqlStatus, "MySQL"); + ensurePopulated(redisStatus, "Redis"); - 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); + 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; } private void ensurePopulated(Map status, String componentName) { @@ -210,24 +172,24 @@ private void ensurePopulated(Map status, String componentName) { } private HealthCheckResult checkMySQLHealthSync() { - boolean basicPassed = false; 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()) { - basicPassed = rs.next(); + 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); } - if (!basicPassed) { - return new HealthCheckResult(false, "No result from health check query", false); - } - boolean isDegraded = performAdvancedMySQLChecksWithThrottle(); - return new HealthCheckResult(true, null, isDegraded); } private HealthCheckResult checkRedisHealthSync() { @@ -342,92 +304,42 @@ private String computeOverallStatus(Map> components) return STATUS_UP; } - // Internal advanced health checks for MySQL - do not expose details in responses - private boolean performAdvancedMySQLChecksWithThrottle() { + private boolean performAdvancedMySQLChecksWithThrottle(Connection connection) { if (!ADVANCED_HEALTH_CHECKS_ENABLED) { - return false; // Advanced checks disabled + return false; } long currentTime = System.currentTimeMillis(); - // Check throttle window - use read lock first for fast path advancedCheckLock.readLock().lock(); try { if (cachedAdvancedCheckResult != null && (currentTime - lastAdvancedCheckTime) < ADVANCED_CHECKS_THROTTLE_SECONDS * 1000) { - // Return cached result - within throttle window return cachedAdvancedCheckResult.isDegraded; } } finally { advancedCheckLock.readLock().unlock(); } - // 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(); - } - } - + advancedCheckLock.writeLock().lock(); try { - // Outside throttle window - acquire write lock and run checks - Future future = advancedCheckExecutor.submit(this::performAdvancedMySQLChecks); - AdvancedCheckResult result = handleAdvancedChecksFuture(future); - - // 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(); + 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 { - advancedCheckInProgress.set(false); - } - } - - private AdvancedCheckResult handleAdvancedChecksFuture(Future future) { - try { - return future.get(ADVANCED_CHECKS_TIMEOUT_MS, TimeUnit.MILLISECONDS); - } catch (TimeoutException ex) { - logger.debug("Advanced MySQL checks timed out after {}ms", ADVANCED_CHECKS_TIMEOUT_MS); - future.cancel(true); - return new AdvancedCheckResult(true); // treat timeout as degraded - } catch (ExecutionException ex) { - future.cancel(true); - if (ex.getCause() instanceof InterruptedException) { - logger.debug("Advanced MySQL checks were interrupted"); - } else { - logger.debug("Advanced MySQL checks failed: {}", ex.getCause() != null ? ex.getCause().getMessage() : ex.getMessage()); - } - return new AdvancedCheckResult(true); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - logger.debug("Advanced MySQL checks interrupted"); - future.cancel(true); - return new AdvancedCheckResult(true); - } catch (Exception ex) { - logger.debug("Advanced MySQL checks failed: {}", ex.getMessage()); - future.cancel(true); - return new AdvancedCheckResult(true); - } - } - - private AdvancedCheckResult performAdvancedMySQLChecks() { - try (Connection connection = dataSource.getConnection()) { - return performAdvancedCheckLogic(connection); - } catch (Exception e) { - logger.debug("Advanced MySQL checks could not obtain connection: {}", e.getMessage()); - return new AdvancedCheckResult(true); + advancedCheckLock.writeLock().unlock(); } } - private AdvancedCheckResult performAdvancedCheckLogic(Connection connection) { + private AdvancedCheckResult performAdvancedMySQLChecks(Connection connection) { try { boolean hasIssues = false; @@ -436,6 +348,11 @@ private AdvancedCheckResult performAdvancedCheckLogic(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); hasIssues = true; @@ -459,7 +376,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 = SUBSTRING_INDEX(USER(), '@', 1)")) { + "AND user = USER()")) { stmt.setQueryTimeout(2); try (ResultSet rs = stmt.executeQuery()) { if (rs.next()) { @@ -473,6 +390,31 @@ 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( @@ -518,21 +460,16 @@ private boolean checkPoolMetricsViaJMX() { ObjectName objectName = new ObjectName("com.zaxxer.hikari:type=Pool (*)"); var mBeans = mBeanServer.queryMBeans(objectName, null); - if (mBeans.isEmpty()) { - logger.debug("Pool exhaustion check disabled: HikariCP metrics unavailable via JMX"); - return false; - } - for (var mBean : mBeans) { if (evaluatePoolMetrics(mBeanServer, mBean.getObjectName())) { return true; } } - return false; } catch (Exception e) { logger.debug("Could not access HikariCP pool metrics via JMX"); } + logger.debug("Pool exhaustion check disabled: HikariCP metrics unavailable"); return false; } diff --git a/src/main/java/com/iemr/tm/utils/http/HTTPRequestInterceptor.java b/src/main/java/com/iemr/tm/utils/http/HTTPRequestInterceptor.java index 6a6d804..4a6e523 100644 --- a/src/main/java/com/iemr/tm/utils/http/HTTPRequestInterceptor.java +++ b/src/main/java/com/iemr/tm/utils/http/HTTPRequestInterceptor.java @@ -59,14 +59,9 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons else authorization = preAuth; if (authorization == null || authorization.isEmpty()) { - String uri = request.getRequestURI().toLowerCase(); - if (uri.endsWith("/health") || uri.endsWith("/version")) { - logger.debug("Public endpoint {}; skipping auth check", uri); - return true; - } - logger.info("Authorization header is null or empty for endpoint: {}", request.getRequestURI()); - return false; // Reject unauthenticated requests to protected endpoints - } + logger.info("Authorization header is null or empty. Skipping HTTPRequestInterceptor."); + return true; // Allow the request to proceed without validation + } if (!request.getMethod().equalsIgnoreCase("OPTIONS")) { try { String[] requestURIParts = request.getRequestURI().split("/"); From 0c094ffa25c7af87dc55f4363a15193c3a5bd1e3 Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Sun, 22 Feb 2026 22:21:15 +0530 Subject: [PATCH 2/4] fix(health): cancel in-flight futures on generic failure --- .../iemr/tm/service/health/HealthService.java | 233 +++++++++++------- 1 file changed, 146 insertions(+), 87 deletions(-) diff --git a/src/main/java/com/iemr/tm/service/health/HealthService.java b/src/main/java/com/iemr/tm/service/health/HealthService.java index 1c8c13d..a28b5bb 100644 --- a/src/main/java/com/iemr/tm/service/health/HealthService.java +++ b/src/main/java/com/iemr/tm/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; @@ -65,14 +67,21 @@ public class HealthService { private static final String ERROR_KEY = "error"; private static final String MESSAGE_KEY = "message"; private static final String RESPONSE_TIME_KEY = "responseTimeMs"; + + // Component names + private static final String MYSQL_COMPONENT = "MySQL"; + private static final String REDIS_COMPONENT = "Redis"; + + // Timeouts (in seconds) private static final long MYSQL_TIMEOUT_SECONDS = 3; private static final long REDIS_TIMEOUT_SECONDS = 3; + // Advanced checks configuration + private static final long ADVANCED_CHECKS_TIMEOUT_MS = 500L; // ms — enforced below 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: {}"; @@ -80,20 +89,26 @@ public class HealthService { private final DataSource dataSource; private final RedisTemplate redisTemplate; private final ExecutorService executorService; + private final ExecutorService advancedCheckExecutor; private volatile long lastAdvancedCheckTime = 0; private volatile AdvancedCheckResult cachedAdvancedCheckResult = null; private final ReentrantReadWriteLock advancedCheckLock = new ReentrantReadWriteLock(); + private final AtomicBoolean advancedCheckInProgress = new AtomicBoolean(false); - private volatile boolean deadlockCheckDisabled = false; - + // Advanced checks always enabled private static final boolean ADVANCED_HEALTH_CHECKS_ENABLED = true; 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); + this.advancedCheckExecutor = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "health-advanced-check"); + t.setDaemon(true); + return t; + }); } @PreDestroy @@ -111,6 +126,9 @@ public void shutdown() { logger.warn("ExecutorService shutdown interrupted", e); } } + if (advancedCheckExecutor != null && !advancedCheckExecutor.isShutdown()) { + advancedCheckExecutor.shutdownNow(); + } } public Map checkHealth() { @@ -120,47 +138,67 @@ 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)); + if (!executorService.isShutdown()) { + performHealthChecks(mysqlStatus, redisStatus); + } - long maxTimeout = Math.max(MYSQL_TIMEOUT_SECONDS, REDIS_TIMEOUT_SECONDS) + 1; - long deadlineNs = System.nanoTime() + TimeUnit.SECONDS.toNanos(maxTimeout); + ensurePopulated(mysqlStatus, MYSQL_COMPONENT); + ensurePopulated(redisStatus, REDIS_COMPONENT); + + Map> components = new LinkedHashMap<>(); + components.put("mysql", mysqlStatus); + components.put("redis", redisStatus); + + response.put("components", components); + response.put(STATUS_KEY, computeOverallStatus(components)); + + return response; + } + + private void performHealthChecks(Map mysqlStatus, Map redisStatus) { + Future mysqlFuture = null; + Future redisFuture = null; try { - mysqlFuture.get(maxTimeout, TimeUnit.SECONDS); - long remainingNs = deadlineNs - System.nanoTime(); - if (remainingNs > 0) { - redisFuture.get(remainingNs, TimeUnit.NANOSECONDS); - } else { - redisFuture.cancel(true); - } + mysqlFuture = executorService.submit( + () -> performHealthCheck(MYSQL_COMPONENT, mysqlStatus, this::checkMySQLHealthSync)); + redisFuture = executorService.submit( + () -> performHealthCheck(REDIS_COMPONENT, redisStatus, this::checkRedisHealthSync)); + + awaitHealthChecks(mysqlFuture, redisFuture); } catch (TimeoutException e) { - logger.warn("Health check aggregate timeout after {} seconds", maxTimeout); - mysqlFuture.cancel(true); - redisFuture.cancel(true); + logger.warn("Health check aggregate timeout after {} seconds", getMaxTimeout()); + cancelFutures(mysqlFuture, redisFuture); } catch (InterruptedException e) { Thread.currentThread().interrupt(); logger.warn("Health check was interrupted"); - mysqlFuture.cancel(true); - redisFuture.cancel(true); + cancelFutures(mysqlFuture, redisFuture); } catch (Exception e) { logger.warn("Health check execution error: {}", e.getMessage()); + cancelFutures(mysqlFuture, redisFuture); } + } + + private void awaitHealthChecks(Future mysqlFuture, Future redisFuture) throws TimeoutException, InterruptedException, ExecutionException { + long maxTimeout = getMaxTimeout(); + long deadlineNs = System.nanoTime() + TimeUnit.SECONDS.toNanos(maxTimeout); - 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); + mysqlFuture.get(maxTimeout, TimeUnit.SECONDS); + long remainingNs = deadlineNs - System.nanoTime(); - return response; + 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) { @@ -172,24 +210,24 @@ private void ensurePopulated(Map status, String componentName) { } private HealthCheckResult checkMySQLHealthSync() { + boolean basicPassed = false; 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); - } + basicPassed = rs.next(); } - - 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); } + if (!basicPassed) { + return new HealthCheckResult(false, "No result from health check query", false); + } + boolean isDegraded = performAdvancedMySQLChecksWithThrottle(); + return new HealthCheckResult(true, null, isDegraded); } private HealthCheckResult checkRedisHealthSync() { @@ -304,42 +342,93 @@ private String computeOverallStatus(Map> components) return STATUS_UP; } - private boolean performAdvancedMySQLChecksWithThrottle(Connection connection) { + // Internal advanced health checks for MySQL - do not expose details in responses + private boolean performAdvancedMySQLChecksWithThrottle() { if (!ADVANCED_HEALTH_CHECKS_ENABLED) { - return false; + return false; // Advanced checks disabled } long currentTime = System.currentTimeMillis(); + // Check throttle window - use read lock first for fast path advancedCheckLock.readLock().lock(); try { if (cachedAdvancedCheckResult != null && (currentTime - lastAdvancedCheckTime) < ADVANCED_CHECKS_THROTTLE_SECONDS * 1000) { + // Return cached result - within throttle window return cachedAdvancedCheckResult.isDegraded; } } finally { advancedCheckLock.readLock().unlock(); } - advancedCheckLock.writeLock().lock(); - try { - if (cachedAdvancedCheckResult != null && - (currentTime - lastAdvancedCheckTime) < ADVANCED_CHECKS_THROTTLE_SECONDS * 1000) { - return cachedAdvancedCheckResult.isDegraded; + // 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 { + // Outside throttle window - acquire write lock and run checks + Future future = advancedCheckExecutor.submit(this::performAdvancedMySQLChecks); + AdvancedCheckResult result = handleAdvancedChecksFuture(future); - AdvancedCheckResult result = performAdvancedMySQLChecks(connection); - - lastAdvancedCheckTime = currentTime; - cachedAdvancedCheckResult = result; - - return result.isDegraded; + // 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(); + } } finally { - advancedCheckLock.writeLock().unlock(); + advancedCheckInProgress.set(false); + } + } + + private AdvancedCheckResult handleAdvancedChecksFuture(Future future) { + try { + return future.get(ADVANCED_CHECKS_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (TimeoutException ex) { + logger.debug("Advanced MySQL checks timed out after {}ms", ADVANCED_CHECKS_TIMEOUT_MS); + future.cancel(true); + return new AdvancedCheckResult(true); // treat timeout as degraded + } catch (ExecutionException ex) { + future.cancel(true); + if (ex.getCause() instanceof InterruptedException) { + Thread.currentThread().interrupt(); + logger.debug("Advanced MySQL checks were interrupted"); + } else { + logger.debug("Advanced MySQL checks failed: {}", ex.getCause() != null ? ex.getCause().getMessage() : ex.getMessage()); + } + return new AdvancedCheckResult(true); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + logger.debug("Advanced MySQL checks interrupted"); + future.cancel(true); + return new AdvancedCheckResult(true); + } catch (Exception ex) { + logger.debug("Advanced MySQL checks failed: {}", ex.getMessage()); + future.cancel(true); + return new AdvancedCheckResult(true); + } + } + + private AdvancedCheckResult performAdvancedMySQLChecks() { + try (Connection connection = dataSource.getConnection()) { + return performAdvancedCheckLogic(connection); + } catch (Exception e) { + logger.debug("Advanced MySQL checks could not obtain connection: {}", e.getMessage()); + return new AdvancedCheckResult(true); } } - private AdvancedCheckResult performAdvancedMySQLChecks(Connection connection) { + private AdvancedCheckResult performAdvancedCheckLogic(Connection connection) { try { boolean hasIssues = false; @@ -348,11 +437,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); hasIssues = true; @@ -376,7 +460,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()) { @@ -390,31 +474,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( From 5f243bbd87534a4d6e9fb9679f5f97c6b74d79fa Mon Sep 17 00:00:00 2001 From: DurgaPrasad-54 Date: Mon, 23 Feb 2026 00:08:56 +0530 Subject: [PATCH 3/4] Fix: Check if URI ends with /health or /version before allowing --- .../com/iemr/tm/service/health/HealthService.java | 1 - .../iemr/tm/utils/http/HTTPRequestInterceptor.java | 11 ++++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/iemr/tm/service/health/HealthService.java b/src/main/java/com/iemr/tm/service/health/HealthService.java index a28b5bb..58af864 100644 --- a/src/main/java/com/iemr/tm/service/health/HealthService.java +++ b/src/main/java/com/iemr/tm/service/health/HealthService.java @@ -401,7 +401,6 @@ private AdvancedCheckResult handleAdvancedChecksFuture(Future Date: Mon, 23 Feb 2026 00:21:12 +0530 Subject: [PATCH 4/4] fix:Added if (mBeans.isEmpty()) check to detect when metrics are actually unavailable --- .../java/com/iemr/tm/service/health/HealthService.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/tm/service/health/HealthService.java b/src/main/java/com/iemr/tm/service/health/HealthService.java index 58af864..edf475e 100644 --- a/src/main/java/com/iemr/tm/service/health/HealthService.java +++ b/src/main/java/com/iemr/tm/service/health/HealthService.java @@ -518,16 +518,21 @@ private boolean checkPoolMetricsViaJMX() { ObjectName objectName = new ObjectName("com.zaxxer.hikari:type=Pool (*)"); var mBeans = mBeanServer.queryMBeans(objectName, null); + if (mBeans.isEmpty()) { + logger.debug("Pool exhaustion check disabled: HikariCP metrics unavailable via JMX"); + return false; + } + for (var mBean : mBeans) { if (evaluatePoolMetrics(mBeanServer, mBean.getObjectName())) { return true; } } + return false; } catch (Exception e) { logger.debug("Could not access HikariCP pool metrics via JMX"); } - logger.debug("Pool exhaustion check disabled: HikariCP metrics unavailable"); return false; }