Skip to content

feat(health,version): add health and version endpoints#66

Merged
drtechie merged 2 commits intoPSMRI:mainfrom
DurgaPrasad-54:feat/health-version
Mar 2, 2026
Merged

feat(health,version): add health and version endpoints#66
drtechie merged 2 commits intoPSMRI:mainfrom
DurgaPrasad-54:feat/health-version

Conversation

@DurgaPrasad-54
Copy link
Contributor

@DurgaPrasad-54 DurgaPrasad-54 commented Feb 21, 2026

📋 Description

JIRA ID:

This PR implements /health and /version endpoints in the Helpline104-API as part of Issue PSMRI/AMRIT#102

What’s changed

  • Add /health endpoint
  • Add /version endpoint

Summary by CodeRabbit

  • New Features

    • Added a /health endpoint that reports service and dependency status with diagnostics.
  • Improvements

    • Version endpoint now returns structured JSON (build timestamp, version, branch, commit).
    • Build now embeds Git revision metadata for richer version info.
    • /health and /version endpoints are exempt from JWT validation to allow unauthenticated access.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 21, 2026

📝 Walkthrough

Walkthrough

Adds health monitoring: a new HealthService (concurrent MySQL and Redis checks, diagnostics, throttling) and HealthController GET /health; refactors VersionController to return JSON from git.properties; exempts /health and /version from JWT filter; adds git-commit-id-maven-plugin entries to pom.xml (duplicated).

Changes

Cohort / File(s) Summary
Build Configuration
pom.xml
Added io.github.git-commit-id:git-commit-id-maven-plugin:5.0.0 with revision goal bound to initialize (failOnNoGitDirectory=false, abbrevLength=7, prefix=git, generateGitPropertiesFile=true). Plugin block was inserted twice (duplicate entries).
Health Monitoring
src/main/java/com/iemr/helpline104/controller/health/HealthController.java, src/main/java/com/iemr/helpline104/service/health/HealthService.java
New HealthController exposing GET /health and new HealthService performing concurrent MySQL and Redis checks, advanced diagnostics, throttled caching, pool/exhaustion detection, and aggregated health payloads.
Version Endpoint
src/main/java/com/iemr/helpline104/controller/version/VersionController.java
Refactored to return structured JSON via ResponseEntity<Map<String,String>>, added UNKNOWN_VALUE fallback and loadGitProperties() to read git.properties; improved error handling and logging.
Security Filter
src/main/java/com/iemr/helpline104/utils/JwtUserIdValidationFilter.java
Added /health and /version to JWT validation exemption list so those endpoints bypass JWT checks.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant HC as HealthController
    participant HS as HealthService
    participant EX as ExecutorService
    participant DB as MySQL
    participant RS as Redis

    Client->>HC: GET /health
    HC->>HS: checkHealth()
    HS->>EX: submit MySQL check
    HS->>EX: submit Redis check
    par Parallel checks
        EX->>DB: basic connectivity (SELECT 1)
        DB-->>EX: response
        EX->>DB: advanced diagnostics (locks, slow queries, pool)
        DB-->>EX: diagnostics
        EX->>RS: PING
        RS-->>EX: PONG
    end
    EX-->>HS: component results
    HS->>HS: aggregate statuses (UP/DEGRADED/DOWN)
    HS-->>HC: health map
    HC-->>Client: ResponseEntity (200 or 503)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I nibble logs and chase each trace,
I ping the DB and hop to Redis' place,
I bring you health and commit-time cheer,
Two version endpoints now crystal clear,
Bounce past JWT gates — the rabbit's here! 🎋

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'feat(health,version): add health and version endpoints' directly and clearly summarizes the main changes, covering both new endpoints introduced in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 21, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@DurgaPrasad-54
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 21, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
src/main/java/com/iemr/helpline104/controller/health/HealthController.java (1)

60-83: Consider reducing log level for the health check entry point.

Line 61 logs at INFO on every health check call. Health endpoints are often polled every few seconds by load balancers and monitoring systems, which will generate significant log noise. Consider using DEBUG for the entry log and keeping INFO/WARN for anomalous results only.

Suggested change
-        logger.info("Health check endpoint called");
+        logger.debug("Health check endpoint called");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/iemr/helpline104/controller/health/HealthController.java`
around lines 60 - 83, The health endpoint logs each invocation at INFO in
HealthController.checkHealth which creates noisy logs; change the initial
logger.info("Health check endpoint called") to logger.debug(...) so routine
polls are logged at DEBUG level, and retain the existing logger.info/warn/error
usage for anomalous results or exceptions (i.e., keep the debug entry log but
leave the status-based logging and exception logging as-is).
src/main/java/com/iemr/helpline104/controller/version/VersionController.java (1)

69-78: Consider caching git properties at construction time.

git.properties is static build-time data. Loading it from the classpath on every request is wasteful (albeit harmless for a low-traffic endpoint). You could load once in the constructor or in a @PostConstruct and store the result.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/iemr/helpline104/controller/version/VersionController.java`
around lines 69 - 78, The loadGitProperties method currently reads
git.properties on each call; instead, add a private Properties field (e.g.,
gitProperties) to VersionController and initialize it once at construction time
(either in the class constructor or a `@PostConstruct` method) by calling
loadGitProperties or inlining its logic; handle IOException during init (log or
rethrow as an unchecked exception) so subsequent requests use the cached
gitProperties field rather than reloading from the classpath each time, and
update any methods that call loadGitProperties to use the cached gitProperties
field.
src/main/java/com/iemr/helpline104/service/health/HealthService.java (3)

193-213: Advanced checks reuse the health-check connection — be mindful of timeout budget.

performAdvancedMySQLChecksWithThrottle runs on the same Connection obtained in checkMySQLHealthSync, within the outer try-with-resources. The SELECT 1 has a 3-second query timeout, but the advanced checks each set their own 2-second timeouts. If the advanced checks aren't throttled (i.e., cache miss), the total connection hold time can exceed the 3-second MYSQL_TIMEOUT_SECONDS, potentially causing the outer checkHealth() deadline to expire. This is mitigated by the 30-second throttle cache but worth noting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/iemr/helpline104/service/health/HealthService.java` around
lines 193 - 213, checkMySQLHealthSync currently passes the same Connection to
performAdvancedMySQLChecksWithThrottle which can exceed the outer SELECT 1's
MYSQL_TIMEOUT_SECONDS; change the implementation so that
performAdvancedMySQLChecksWithThrottle uses its own DB Connection (i.e., open a
new connection inside that method or have it obtain one from the DataSource) so
the outer try-with-resources' short query timeout isn't consumed by the advanced
checks, and ensure the new connection is properly closed; alternatively, if you
prefer to keep a single call site, increase the outer MYSQL_TIMEOUT_SECONDS
before invoking advanced checks and restore it afterwards—but prefer the
separate-connection approach for isolation and predictable timeouts.

107-112: Fixed thread pool of size 2 may bottleneck under concurrent health check requests.

Since checkHealth() submits exactly 2 tasks per invocation, concurrent callers will queue behind each other on this pool. Under aggressive polling (e.g., multiple load balancers), this could cause cascading timeouts. Consider using a cached thread pool or a slightly larger pool, or — preferably — adding rate-limiting/caching at the controller level.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/iemr/helpline104/service/health/HealthService.java` around
lines 107 - 112, The fixed-size executor in the HealthService constructor
(executorService = Executors.newFixedThreadPool(2)) will bottleneck because
checkHealth() submits two tasks per call; change this to a scalable executor
(for example use Executors.newCachedThreadPool() or create a configurable pool
via a constructor parameter/DI so pool size can be tuned) and/or accept an
ExecutorService in the HealthService constructor so callers can provide a
shared, appropriately sized executor; update the constructor signature and
initialization (or configuration binding) and ensure existing checkHealth()
continues to submit tasks to the new executorService.

334-372: Throttle uses currentTime captured before acquiring the write lock — minor staleness.

currentTime is captured at line 339 before the read lock, but is reused at line 365 (lastAdvancedCheckTime = currentTime) after the write lock is acquired. If there's contention, this timestamp could be stale by the time the write lock is obtained, slightly shortening the effective throttle window. This is a minor timing inaccuracy that's unlikely to cause issues in practice.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/iemr/helpline104/service/health/HealthService.java` around
lines 334 - 372, The throttle timestamp is captured in
performAdvancedMySQLChecksWithThrottle as currentTime before locking and then
reused after acquiring the write lock, which can make the cached timestamp
slightly stale; fix by re-reading System.currentTimeMillis() inside the write
lock (e.g., update currentTime after acquiring advancedCheckLock.writeLock())
and use that refreshed value for the double-check comparison and when assigning
lastAdvancedCheckTime, keeping references to cachedAdvancedCheckResult,
ADVANCED_CHECKS_THROTTLE_SECONDS, and advancedCheckLock unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pom.xml`:
- Around line 390-408: The git-commit-id-maven-plugin configuration is missing
generateGitPropertiesFile, so git.properties is not produced and
VersionController.loadGitProperties() ends up returning "unknown"; update the
plugin block for artifactId git-commit-id-maven-plugin (version 5.0.0) to
include <generateGitPropertiesFile>true</generateGitPropertiesFile> in its
<configuration> so the git.properties file is generated onto the classpath and
VersionController.loadGitProperties() can read the git metadata.

In `@src/main/java/com/iemr/helpline104/service/health/HealthService.java`:
- Around line 425-454: The hasDeadlocks method currently treats any presence of
"LATEST DETECTED DEADLOCK" in innodbStatus as a live deadlock; instead parse the
timestamp from the InnoDB status text (variable innodbStatus) within
hasDeadlocks and only return true if the parsed deadlock time is within a recent
window (e.g., last 5 minutes). Use a regex to extract the timestamp line that
accompanies the LATEST DETECTED DEADLOCK block, normalize the parsed time to the
server timezone or assume UTC, compare against Instant.now() (or
System.currentTimeMillis()) with a configurable threshold, and only set a
degraded result when within that threshold; keep the existing
permission-fallback behavior (deadlockCheckDisabled) and ensure any parsing
failures fall back to treating it as non-degraded (return false) while logging a
debug message.
- Around line 155-166: The generic exception catch in HealthService currently
logs the error but fails to cancel redisFuture, leaving an orphaned task; update
the catch (Exception e) block (where mysqlFuture and redisFuture are referenced)
to cancel redisFuture as well (call redisFuture.cancel(true)) — mirror the
cancellation behavior used in the TimeoutException/InterruptedException handlers
so both mysqlFuture and redisFuture are cancelled when any exception (e.g.,
ExecutionException) occurs; keep the existing logging of the error message and
ensure ensurePopulated behavior remains unchanged.

---

Nitpick comments:
In `@src/main/java/com/iemr/helpline104/controller/health/HealthController.java`:
- Around line 60-83: The health endpoint logs each invocation at INFO in
HealthController.checkHealth which creates noisy logs; change the initial
logger.info("Health check endpoint called") to logger.debug(...) so routine
polls are logged at DEBUG level, and retain the existing logger.info/warn/error
usage for anomalous results or exceptions (i.e., keep the debug entry log but
leave the status-based logging and exception logging as-is).

In
`@src/main/java/com/iemr/helpline104/controller/version/VersionController.java`:
- Around line 69-78: The loadGitProperties method currently reads git.properties
on each call; instead, add a private Properties field (e.g., gitProperties) to
VersionController and initialize it once at construction time (either in the
class constructor or a `@PostConstruct` method) by calling loadGitProperties or
inlining its logic; handle IOException during init (log or rethrow as an
unchecked exception) so subsequent requests use the cached gitProperties field
rather than reloading from the classpath each time, and update any methods that
call loadGitProperties to use the cached gitProperties field.

In `@src/main/java/com/iemr/helpline104/service/health/HealthService.java`:
- Around line 193-213: checkMySQLHealthSync currently passes the same Connection
to performAdvancedMySQLChecksWithThrottle which can exceed the outer SELECT 1's
MYSQL_TIMEOUT_SECONDS; change the implementation so that
performAdvancedMySQLChecksWithThrottle uses its own DB Connection (i.e., open a
new connection inside that method or have it obtain one from the DataSource) so
the outer try-with-resources' short query timeout isn't consumed by the advanced
checks, and ensure the new connection is properly closed; alternatively, if you
prefer to keep a single call site, increase the outer MYSQL_TIMEOUT_SECONDS
before invoking advanced checks and restore it afterwards—but prefer the
separate-connection approach for isolation and predictable timeouts.
- Around line 107-112: The fixed-size executor in the HealthService constructor
(executorService = Executors.newFixedThreadPool(2)) will bottleneck because
checkHealth() submits two tasks per call; change this to a scalable executor
(for example use Executors.newCachedThreadPool() or create a configurable pool
via a constructor parameter/DI so pool size can be tuned) and/or accept an
ExecutorService in the HealthService constructor so callers can provide a
shared, appropriately sized executor; update the constructor signature and
initialization (or configuration binding) and ensure existing checkHealth()
continues to submit tasks to the new executorService.
- Around line 334-372: The throttle timestamp is captured in
performAdvancedMySQLChecksWithThrottle as currentTime before locking and then
reused after acquiring the write lock, which can make the cached timestamp
slightly stale; fix by re-reading System.currentTimeMillis() inside the write
lock (e.g., update currentTime after acquiring advancedCheckLock.writeLock())
and use that refreshed value for the double-check comparison and when assigning
lastAdvancedCheckTime, keeping references to cachedAdvancedCheckResult,
ADVANCED_CHECKS_THROTTLE_SECONDS, and advancedCheckLock unchanged.

@sonarqubecloud
Copy link

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (5)
src/main/java/com/iemr/helpline104/service/health/HealthService.java (4)

99-104: Thread pool size is hardcoded.

Executors.newFixedThreadPool(6) is hardcoded, but the pool only ever runs two concurrent tasks (MySQL + Redis checks). A pool of 2 would suffice and avoid holding unnecessary threads. If more components are added later, it can be bumped.

Suggested change
-        this.executorService = Executors.newFixedThreadPool(6);
+        this.executorService = Executors.newFixedThreadPool(2);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/iemr/helpline104/service/health/HealthService.java` around
lines 99 - 104, The constructor in HealthService is creating an executorService
with a hardcoded size of 6 (Executors.newFixedThreadPool(6)) even though only
two concurrent tasks are used; change this to a pool size of 2 or make it
configurable. Update the HealthService constructor to initialize executorService
with Executors.newFixedThreadPool(2) or fetch the size from a
configuration/constant (e.g., THREAD_POOL_SIZE) so the pool is small by default
but can be increased later without code changes; reference the HealthService
constructor and executorService/Executors.newFixedThreadPool(...) when making
the change.

359-391: Thundering herd possible when the throttle window expires.

After the write-lock double-check (line 363) releases the lock at line 368, multiple concurrent threads can each pass the stale check and all proceed to execute the expensive DB queries simultaneously. The DB I/O was intentionally moved outside the write lock (per the commit message), but this creates a burst of redundant queries.

A lightweight fix: use an AtomicBoolean as a "check-in-progress" flag so that only the first thread performs the DB I/O while others return the stale cached result.

Sketch using AtomicBoolean guard
+    private final java.util.concurrent.atomic.AtomicBoolean advancedCheckInProgress = new java.util.concurrent.atomic.AtomicBoolean(false);
+
     private boolean performAdvancedMySQLChecksWithThrottle() {
         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;
-            }
-        } finally {
-            advancedCheckLock.writeLock().unlock();
-        }
-        
-        AdvancedCheckResult result;
-        try (Connection conn = dataSource.getConnection()) {
-            result = performAdvancedMySQLChecks(conn);
-        } catch (Exception ex) {
-            if (ex.getCause() instanceof InterruptedException) {
-                Thread.currentThread().interrupt();
-            }
-            logger.debug("Could not acquire connection for advanced checks: {}", ex.getMessage());
-            result = new AdvancedCheckResult(false);
-        }
-        
-        advancedCheckLock.writeLock().lock();
-        try {
-            lastAdvancedCheckTime = currentTime;
-            cachedAdvancedCheckResult = result;
-            return result.isDegraded;
-        } finally {
-            advancedCheckLock.writeLock().unlock();
+        // Only one thread runs the expensive checks; others return stale cache
+        if (!advancedCheckInProgress.compareAndSet(false, true)) {
+            return cachedAdvancedCheckResult != null && cachedAdvancedCheckResult.isDegraded;
+        }
+        try {
+            AdvancedCheckResult result;
+            try (Connection conn = dataSource.getConnection()) {
+                result = performAdvancedMySQLChecks(conn);
+            } catch (Exception ex) {
+                if (ex.getCause() instanceof InterruptedException) {
+                    Thread.currentThread().interrupt();
+                }
+                logger.debug("Could not acquire connection for advanced checks: {}", ex.getMessage());
+                result = new AdvancedCheckResult(false);
+            }
+            advancedCheckLock.writeLock().lock();
+            try {
+                lastAdvancedCheckTime = currentTime;
+                cachedAdvancedCheckResult = result;
+            } finally {
+                advancedCheckLock.writeLock().unlock();
+            }
+            return result.isDegraded;
+        } finally {
+            advancedCheckInProgress.set(false);
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/iemr/helpline104/service/health/HealthService.java` around
lines 359 - 391, The throttle window still allows a thundering herd because
multiple threads can pass the stale double-check after
advancedCheckLock.writeLock() is released; introduce an AtomicBoolean (e.g.,
advancedCheckInProgress) and use compareAndSet(true) to make exactly one thread
perform the DB I/O in performAdvancedMySQLChecks via dataSource.getConnection(),
while other threads immediately return the current
cachedAdvancedCheckResult.isDegraded (or reload the cache if null) without
performing queries; ensure the thread that won the compareAndSet clears
advancedCheckInProgress in a finally block (and still updates
lastAdvancedCheckTime and cachedAdvancedCheckResult under
advancedCheckLock.writeLock()), and preserve existing InterruptedException
handling and logging.

517-535: Consider using Java record types for these inner value classes.

Since the project targets Java 17, these private inner classes are good candidates for records, reducing boilerplate:

Example
-    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;
-        }
-    }
+    private record AdvancedCheckResult(boolean isDegraded) {}
+
+    private record HealthCheckResult(boolean isHealthy, String error, boolean isDegraded) {}

Note: record field access uses isHealthy() instead of .isHealthy, so update all field references accordingly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/iemr/helpline104/service/health/HealthService.java` around
lines 517 - 535, Replace the private inner classes AdvancedCheckResult and
HealthCheckResult with Java records to remove boilerplate: change "private
static class AdvancedCheckResult { final boolean isDegraded; ... }" to "private
record AdvancedCheckResult(boolean isDegraded) {}" and "private static class
HealthCheckResult { final boolean isHealthy; final String error; final boolean
isDegraded; ... }" to "private record HealthCheckResult(boolean isHealthy,
String error, boolean isDegraded) {}", remove explicit constructors, and update
all usage sites to call the generated accessor methods (use isDegraded() and
isHealthy() and error() instead of field access) wherever AdvancedCheckResult
and HealthCheckResult fields were read.

201-219: Health check holds two pool connections simultaneously.

The try-with-resources at line 202 keeps the first connection open while performAdvancedMySQLChecksWithThrottle() (line 213) acquires a second connection internally (line 373). Under pool pressure — exactly the condition this check aims to detect — this doubles the footprint per health check thread.

Close the initial connection before running advanced checks:

Suggested restructure
     private HealthCheckResult checkMySQLHealthSync() {
+        // Basic connectivity check
+        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()) {
+                    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);
+        }
+
+        // Advanced checks use their own connection (acquired inside)
         try {
-            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()) {
-                        return new HealthCheckResult(false, "No result from health check query", false);
-                    }
-                }
-                boolean isDegraded = performAdvancedMySQLChecksWithThrottle();
-                return new HealthCheckResult(true, null, isDegraded);
-            }
+            boolean isDegraded = performAdvancedMySQLChecksWithThrottle();
+            return new HealthCheckResult(true, null, isDegraded);
         } catch (Exception e) {
-            logger.warn("MySQL health check failed: {}", e.getMessage(), e);
-            return new HealthCheckResult(false, "MySQL connection failed", false);
+            logger.warn("Advanced MySQL checks failed: {}", e.getMessage(), e);
+            return new HealthCheckResult(true, null, false);
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/iemr/helpline104/service/health/HealthService.java` around
lines 201 - 219, checkMySQLHealthSync currently holds a
Connection/PreparedStatement/ResultSet open while calling
performAdvancedMySQLChecksWithThrottle, causing two pooled connections to be
used; refactor so the simple "SELECT 1" query runs inside its own
try-with-resources and is fully closed (close ResultSet, PreparedStatement and
Connection) before invoking performAdvancedMySQLChecksWithThrottle; specifically
update checkMySQLHealthSync to release the initial
Connection/PreparedStatement/ResultSet after verifying rs.next() and only then
call performAdvancedMySQLChecksWithThrottle, preserving existing return behavior
and exception handling.
pom.xml (1)

321-324: Duplicate spring-boot-maven-plugin declarations.

spring-boot-maven-plugin is declared twice — once at lines 321–324 (no version, no execution config) and again at lines 422–433 (version 3.2.2, repackage goal). Having two declarations for the same plugin can cause unexpected merge behavior in Maven's effective POM. Consider consolidating into a single block.

Suggested consolidation
-			<plugin>
-				<groupId>org.springframework.boot</groupId>
-				<artifactId>spring-boot-maven-plugin</artifactId>
-			</plugin>

Keep only the explicit declaration at lines 422–433 that specifies the version and the repackage goal.

Also applies to: 422-433

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pom.xml` around lines 321 - 324, There are two declarations of the same Maven
plugin (org.springframework.boot:spring-boot-maven-plugin); remove the duplicate
by consolidating into a single plugin block: keep the explicit block that
includes the version (3.2.2) and the repackage goal (the block referenced by the
review at lines 422–433) and delete the redundant bare plugin block (the one
with groupId/org.springframework.boot and artifactId/spring-boot-maven-plugin
without version or executions), or merge any missing configuration from the bare
block into the explicit block so only one
org.springframework.boot:spring-boot-maven-plugin entry remains.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@pom.xml`:
- Around line 390-409: The git-commit-id-maven-plugin configuration is now
correct: ensure <generateGitPropertiesFile> is set to true (as seen in the
git-commit-id-maven-plugin configuration) and keep
<failOnNoGitDirectory>false</failOnNoGitDirectory> and
<abbrevLength>7</abbrevLength> so VersionController can consume the generated
git.properties; no further code changes required.

In `@src/main/java/com/iemr/helpline104/service/health/HealthService.java`:
- Around line 147-168: performHealthChecks previously risked leaving orphaned
tasks; the current change adds cancelFutures(mysqlFuture, redisFuture) in all
exception blocks—ensure the cancellation logic in cancelFutures safely handles
null and already-completed/ cancelled Future instances and that
awaitHealthChecks(mysqlFuture, redisFuture) can propagate
TimeoutException/InterruptedException appropriately; locate performHealthChecks,
awaitHealthChecks, cancelFutures, executorService.submit and the passed
checkMySQLHealthSync/checkRedisHealthSync methods to verify cancelFutures checks
for null, calls future.cancel(true) and logs outcomes, and that no other code
path can leave running tasks.
- Around line 420-457: The try/catch blocks in hasLockWaits and hasSlowQueries
swallow exceptions without including the exception details; update the catch
handlers to log the caught Exception (e) with the existing logger.debug call
(e.g., logger.debug("Could not check for lock waits", e) and logger.debug("Could
not check for slow queries", e)) so failures in those methods are recorded and
diagnosable while preserving the current behavior and timeouts.

---

Nitpick comments:
In `@pom.xml`:
- Around line 321-324: There are two declarations of the same Maven plugin
(org.springframework.boot:spring-boot-maven-plugin); remove the duplicate by
consolidating into a single plugin block: keep the explicit block that includes
the version (3.2.2) and the repackage goal (the block referenced by the review
at lines 422–433) and delete the redundant bare plugin block (the one with
groupId/org.springframework.boot and artifactId/spring-boot-maven-plugin without
version or executions), or merge any missing configuration from the bare block
into the explicit block so only one
org.springframework.boot:spring-boot-maven-plugin entry remains.

In `@src/main/java/com/iemr/helpline104/service/health/HealthService.java`:
- Around line 99-104: The constructor in HealthService is creating an
executorService with a hardcoded size of 6 (Executors.newFixedThreadPool(6))
even though only two concurrent tasks are used; change this to a pool size of 2
or make it configurable. Update the HealthService constructor to initialize
executorService with Executors.newFixedThreadPool(2) or fetch the size from a
configuration/constant (e.g., THREAD_POOL_SIZE) so the pool is small by default
but can be increased later without code changes; reference the HealthService
constructor and executorService/Executors.newFixedThreadPool(...) when making
the change.
- Around line 359-391: The throttle window still allows a thundering herd
because multiple threads can pass the stale double-check after
advancedCheckLock.writeLock() is released; introduce an AtomicBoolean (e.g.,
advancedCheckInProgress) and use compareAndSet(true) to make exactly one thread
perform the DB I/O in performAdvancedMySQLChecks via dataSource.getConnection(),
while other threads immediately return the current
cachedAdvancedCheckResult.isDegraded (or reload the cache if null) without
performing queries; ensure the thread that won the compareAndSet clears
advancedCheckInProgress in a finally block (and still updates
lastAdvancedCheckTime and cachedAdvancedCheckResult under
advancedCheckLock.writeLock()), and preserve existing InterruptedException
handling and logging.
- Around line 517-535: Replace the private inner classes AdvancedCheckResult and
HealthCheckResult with Java records to remove boilerplate: change "private
static class AdvancedCheckResult { final boolean isDegraded; ... }" to "private
record AdvancedCheckResult(boolean isDegraded) {}" and "private static class
HealthCheckResult { final boolean isHealthy; final String error; final boolean
isDegraded; ... }" to "private record HealthCheckResult(boolean isHealthy,
String error, boolean isDegraded) {}", remove explicit constructors, and update
all usage sites to call the generated accessor methods (use isDegraded() and
isHealthy() and error() instead of field access) wherever AdvancedCheckResult
and HealthCheckResult fields were read.
- Around line 201-219: checkMySQLHealthSync currently holds a
Connection/PreparedStatement/ResultSet open while calling
performAdvancedMySQLChecksWithThrottle, causing two pooled connections to be
used; refactor so the simple "SELECT 1" query runs inside its own
try-with-resources and is fully closed (close ResultSet, PreparedStatement and
Connection) before invoking performAdvancedMySQLChecksWithThrottle; specifically
update checkMySQLHealthSync to release the initial
Connection/PreparedStatement/ResultSet after verifying rs.next() and only then
call performAdvancedMySQLChecksWithThrottle, preserving existing return behavior
and exception handling.

@drtechie drtechie merged commit b9684ef into PSMRI:main Mar 2, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants