Skip to content

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

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

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

Conversation

@DurgaPrasad-54
Copy link
Contributor

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

📋 Description

JIRA ID:

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

What’s changed

  • Add /health endpoint
  • Add /version endpoint

Summary by CodeRabbit

  • New Features

    • Health endpoint: concurrent per-component checks (MySQL, Redis when configured) reporting status, response times, severities and overall health (HTTP 200/503).
    • Version endpoint: returns structured JSON with build timestamp, version, branch and commit hash.
    • Health and Version endpoints bypass authentication/filtering for direct access.
  • Chores

    • Build now generates git metadata (git.properties) with selected repository fields.

@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds build-time git metadata generation, a concurrent HealthService performing MySQL (+ optional Redis) checks, a /health REST controller mapping status to HTTP 200/503, refactors /version to read git.properties and return JSON, and excludes /health and /version from the JWT filter.

Changes

Cohort / File(s) Summary
Build configuration
pom.xml
Added io.github.git-commit-id:git-commit-id-maven-plugin (v9.0.2) to generate ${project.build.outputDirectory}/git.properties with selected properties; non-fatal when git data unavailable.
Health service & controller
src/main/java/com/iemr/inventory/service/health/HealthService.java, src/main/java/com/iemr/inventory/controller/health/HealthController.java
New concurrent HealthService performing MySQL and optional Redis checks (timeouts, per-component latency, severity, aggregation to UP/DEGRADED/DOWN, executor shutdown). New HealthController exposes GET /health, delegates to service, returns 200 or 503 based on aggregated status and handles errors.
Version endpoint
src/main/java/com/iemr/inventory/controller/version/VersionController.java
Refactored /version to ResponseEntity<Map<String,String>>, loads git.properties (buildTimestamp, version, branch, commitHash), defaults to "unknown" on failure, and improves logging and response structure.
Auth filter
src/main/java/com/iemr/inventory/utils/JwtUserIdValidationFilter.java
Extended skip list to bypass JWT validation for /health and /version endpoints.

Sequence Diagram

sequenceDiagram
    participant Client
    participant HealthController
    participant HealthService
    participant ThreadPool
    participant MySQL
    participant Redis

    Client->>HealthController: GET /health
    HealthController->>HealthService: checkHealth()
    HealthService->>ThreadPool: submit MySQL check
    HealthService->>ThreadPool: submit Redis check (if configured)

    par Parallel
        ThreadPool->>MySQL: SELECT 1 (with timeout)
        ThreadPool->>Redis: PING (with timeout)
    end

    MySQL-->>ThreadPool: result / error / timeout
    Redis-->>ThreadPool: result / error / timeout
    ThreadPool-->>HealthService: per-component results
    HealthService->>HealthService: aggregate statuses → UP/DEGRADED/DOWN
    HealthService-->>HealthController: health map (components, statuses, timestamp)

    alt Overall UP
        HealthController-->>Client: HTTP 200 + health JSON
    else
        HealthController-->>Client: HTTP 503 + health JSON
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Poem

🐰 I hopped through branches, sniffed commits anew,

I pinged the DB and bounced a Redis cue,
I stitched timestamps, hashes, branch and name,
Returned a health map so services proclaim,
A tiny rabbit patch to know what's true.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.55% 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 title clearly summarizes the main changes: adding health and version endpoints, which are the primary objectives reflected in all file modifications.

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

✨ Finishing Touches
🧪 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.

Copy link

@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: 1

🧹 Nitpick comments (2)
src/main/java/com/iemr/inventory/service/health/HealthService.java (2)

96-119: Health checks run sequentially; consider parallelizing.

checkHealth() invokes MySQL and Redis checks one after the other. In the worst case (both time out), this endpoint blocks for up to 6 seconds (3s + 3s). Since you already have a 2-thread pool and CompletableFuture inside each check, you could launch both checks concurrently and join them, halving the worst-case latency. This matters for load-balancer health probes that typically expect sub-second responses.

Sketch of parallel execution
     public Map<String, Object> checkHealth() {
         Map<String, Object> response = new LinkedHashMap<>();
         response.put("timestamp", Instant.now().toString());
         
-        Map<String, Map<String, Object>> components = new LinkedHashMap<>();
-        
-        // Check MySQL
-        Map<String, Object> mysqlStatus = new LinkedHashMap<>();
-        performHealthCheck("MySQL", mysqlStatus, this::checkMySQLHealth);
-        components.put("mysql", mysqlStatus);
-        
-        // Check Redis
-        Map<String, Object> redisStatus = new LinkedHashMap<>();
-        performHealthCheck("Redis", redisStatus, this::checkRedisHealth);
-        components.put("redis", redisStatus);
+        Map<String, Object> mysqlStatus = new LinkedHashMap<>();
+        Map<String, Object> redisStatus = new LinkedHashMap<>();
+        
+        CompletableFuture<Void> mysqlFuture = CompletableFuture.runAsync(
+            () -> performHealthCheck("MySQL", mysqlStatus, this::checkMySQLHealth), executorService);
+        CompletableFuture<Void> redisFuture = CompletableFuture.runAsync(
+            () -> performHealthCheck("Redis", redisStatus, this::checkRedisHealth), executorService);
+        
+        CompletableFuture.allOf(mysqlFuture, redisFuture).join();
+        
+        Map<String, Map<String, Object>> components = new LinkedHashMap<>();
+        components.put("mysql", mysqlStatus);
+        components.put("redis", redisStatus);
         
         response.put("components", components);

Note: if you do this, the inner checkMySQLHealth/checkRedisHealth methods should no longer submit to executorService (to avoid thread-pool starvation with a pool of 2). Run them directly instead.

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

In `@src/main/java/com/iemr/inventory/service/health/HealthService.java` around
lines 96 - 119, checkHealth currently runs performHealthCheck for MySQL and
Redis sequentially which doubles worst-case latency; modify checkHealth to
launch both component checks concurrently using CompletableFuture.supplyAsync
(or similar) for performHealthCheck invocations for "MySQL" and "Redis", then
join both futures and collect their results into the components map before
computing overall status with computeOverallStatus; also update checkMySQLHealth
and checkRedisHealth to execute synchronously (remove their internal
executorService submission) so they don't submit additional tasks and risk
thread-pool starvation.

158-161: Redis "not configured" uses the error field for a non-error message.

When redisTemplate is null, the result is isHealthy=true with a message in the error field (line 218–220 populates "error" from result.error). This produces a confusing response where status=UP but an error key is present. Consider using a separate "message" or "info" field for informational notes to avoid confusion with actual errors.

Proposed approach

Add a message field to HealthCheckResult for informational messages, keeping error for actual failures:

     private static class HealthCheckResult {
         final boolean isHealthy;
         final String error;
+        final String message;

-        HealthCheckResult(boolean isHealthy, String error) {
+        HealthCheckResult(boolean isHealthy, String error, String message) {
             this.isHealthy = isHealthy;
             this.error = error;
+            this.message = message;
         }
     }

Then in checkRedisHealth:

     if (redisTemplate == null) {
-        return new HealthCheckResult(true, "Redis not configured — skipped");
+        return new HealthCheckResult(true, null, "Redis not configured — skipped");
     }

And in performHealthCheck, populate "message" when result.message != null.

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

In `@src/main/java/com/iemr/inventory/service/health/HealthService.java` around
lines 158 - 161, Health check currently writes the "Redis not configured" text
into HealthCheckResult.error, which is confusing; add a separate informational
field (e.g., String message) to the HealthCheckResult class and use it for
non-error notes, update checkRedisHealth() to return new HealthCheckResult(true,
null, "Redis not configured — skipped") (or equivalent constructor/setter) when
redisTemplate is null, ensure actual failures still populate error, and modify
performHealthCheck() to populate the response "message" (from
result.getMessage()) instead of placing that text into the "error" field so
error remains reserved for real failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/com/iemr/inventory/controller/health/HealthController.java`:
- Around line 65-70: The current HealthController uses overallStatus (from
HealthService.computeOverallStatus()) to map any non-"UP" value to
HttpStatus.SERVICE_UNAVAILABLE; change the mapping so that both "UP" and
"DEGRADED" map to HttpStatus.OK and only "DOWN" maps to
HttpStatus.SERVICE_UNAVAILABLE. Locate the code that reads overallStatus and
constructs the ResponseEntity (the block using variables overallStatus,
httpStatus and returning new ResponseEntity<>(healthStatus, httpStatus)) and
update the conditional to explicitly check for "UP" or "DEGRADED" before
assigning HttpStatus.OK, leaving SERVICE_UNAVAILABLE only for "DOWN" (or other
explicit failure states).

---

Nitpick comments:
In `@src/main/java/com/iemr/inventory/service/health/HealthService.java`:
- Around line 96-119: checkHealth currently runs performHealthCheck for MySQL
and Redis sequentially which doubles worst-case latency; modify checkHealth to
launch both component checks concurrently using CompletableFuture.supplyAsync
(or similar) for performHealthCheck invocations for "MySQL" and "Redis", then
join both futures and collect their results into the components map before
computing overall status with computeOverallStatus; also update checkMySQLHealth
and checkRedisHealth to execute synchronously (remove their internal
executorService submission) so they don't submit additional tasks and risk
thread-pool starvation.
- Around line 158-161: Health check currently writes the "Redis not configured"
text into HealthCheckResult.error, which is confusing; add a separate
informational field (e.g., String message) to the HealthCheckResult class and
use it for non-error notes, update checkRedisHealth() to return new
HealthCheckResult(true, null, "Redis not configured — skipped") (or equivalent
constructor/setter) when redisTemplate is null, ensure actual failures still
populate error, and modify performHealthCheck() to populate the response
"message" (from result.getMessage()) instead of placing that text into the
"error" field so error remains reserved for real failures.

Copy link

@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: 5

🧹 Nitpick comments (1)
src/main/java/com/iemr/inventory/service/health/HealthService.java (1)

199-201: performHealthCheck has an unused return value — consider making it void.

The method signature declares Map<String, Object> as the return type and returns the status parameter, but both callers (Lines 105, 110) ignore it entirely, relying on in-place mutation of the status map. The dual contract (mutate-in-place + return-same-reference) is confusing. Changing the signature to void removes the ambiguity.

♻️ Proposed refactor
-    private Map<String, Object> performHealthCheck(String componentName,
-                                                    Map<String, Object> status,
-                                                    Supplier<HealthCheckResult> checker) {
+    private void performHealthCheck(String componentName,
+                                    Map<String, Object> status,
+                                    Supplier<HealthCheckResult> checker) {
         ...
-        return status;
         ...
-        return status;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/iemr/inventory/service/health/HealthService.java` around
lines 199 - 201, The performHealthCheck method currently returns the status Map
but callers ignore that return; change the signature of
performHealthCheck(String componentName, Map<String, Object> status,
Supplier<HealthCheckResult> checker) to return void and keep the existing
in-place mutation of the status map (using HealthCheckResult from the checker).
Update all call sites that currently call performHealthCheck(...) (they
currently ignore the return) to call it as a void method (remove any
assignment/expectation of a returned Map). Ensure imports/types (Supplier,
HealthCheckResult) remain unchanged and tests/usage still rely on the mutated
status map.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/com/iemr/inventory/service/health/HealthService.java`:
- Around line 125-127: The JDBC query timeout and the outer future.get() use the
same MYSQL_TIMEOUT_SECONDS causing a race; in HealthService (the method that
creates the PreparedStatement and later calls future.get(...)), change the outer
future.get timeout to use MYSQL_TIMEOUT_SECONDS + 1 (or otherwise add a 1-second
buffer) so the statement's stmt.setQueryTimeout(MYSQL_TIMEOUT_SECONDS) can
trigger first and clean up before the future.get times out.
- Around line 159-162: The informational Redis skip response is being stored
under the "error" key; update the health model and mapping so informational text
is stored under "message" not "error". Add a message field to HealthCheckResult
(alongside existing ok/error), adjust checkRedisHealth() to return the message
when redisTemplate==null (e.g., new HealthCheckResult(true, null, "Redis not
configured — skipped")), and modify performHealthCheck() to put result.message
into status.put("message", ...) when result.ok is true and result.error remains
null, while keeping error handling unchanged for failing checks.
- Around line 189-192: In checkRedisHealth, the InterruptedException handler
logs before restoring the interrupt flag; change the handler to first call
Thread.currentThread().interrupt() to restore the interrupt status and then
perform logger.warn(...) and return the HealthCheckResult(false, "Redis health
check was interrupted") so the interrupt flag isn't lost (mirror the order used
in checkMySQLHealth).
- Around line 97-119: checkHealth currently invokes performHealthCheck
sequentially leading to up-to-6s blocking and false timeouts; make the component
checks run concurrently and ensure the overall timeout is applied across both.
Change checkMySQLHealth and checkRedisHealth to be synchronous helpers (remove
internal CompletableFuture/submit-to-executorService), then in checkHealth
submit both performHealthCheck tasks to executorService as Callables (reference:
performHealthCheck, checkHealth, checkMySQLHealth, checkRedisHealth,
executorService) and wait for both results using invokeAll or by submitting two
Futures and waiting with a single shared timeout (e.g., 3s) so you join both
within the same deadline; keep computeOverallStatus usage the same and populate
mysql/redis maps from the concurrent results, marking timed-out or failed tasks
as DOWN.
- Around line 143-148: The health check uses CompletableFuture and calls
future.cancel(true) in HealthService (MySQL check around the get with
MYSQL_TIMEOUT_SECONDS and similarly the Redis check) but
CompletableFuture.cancel(true) does not interrupt the underlying JDBC/Redis
task; change the implementation to submit the blocking DB/Redis call via
ExecutorService.submit(Callable) so you get a cancellable Future (FutureTask)
and keep that Future reference to call cancel(true) to interrupt the thread, or
alternatively ensure the JDBC/Redis command-level timeout is shorter than
MYSQL_TIMEOUT_SECONDS so the task self-terminates before cancellation; update
both the MySQL health-check path and the Redis health-check path accordingly and
remove reliance on CompletableFuture.cancel(true).

---

Nitpick comments:
In `@src/main/java/com/iemr/inventory/service/health/HealthService.java`:
- Around line 199-201: The performHealthCheck method currently returns the
status Map but callers ignore that return; change the signature of
performHealthCheck(String componentName, Map<String, Object> status,
Supplier<HealthCheckResult> checker) to return void and keep the existing
in-place mutation of the status map (using HealthCheckResult from the checker).
Update all call sites that currently call performHealthCheck(...) (they
currently ignore the return) to call it as a void method (remove any
assignment/expectation of a returned Map). Ensure imports/types (Supplier,
HealthCheckResult) remain unchanged and tests/usage still rely on the mutated
status map.

Copy link

@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: 1

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

60-70: Health check logging at INFO level will flood logs in production.

Load balancers and orchestrators typically poll /health every few seconds. Logging at INFO on Line 61 will generate significant noise. Demote to DEBUG; keep the completion log on Line 69 at DEBUG as well.

Proposed fix
-        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/inventory/controller/health/HealthController.java`
around lines 60 - 70, In HealthController.checkHealth(), change the initial
logger call from logger.info("Health check endpoint called") to
logger.debug(...) to avoid INFO-level noise from frequent health probes; keep
the existing completion log (logger.debug("Health check completed with status:
{}"...)) as-is so both entry and exit are at DEBUG level and the method behavior
and returned ResponseEntity remain unchanged.

72-81: Error response omits component details, making it hard to distinguish from a healthy-but-DOWN response.

The catch-all error response contains only "status" and "timestamp", while the happy-path response includes "components". Consumers parsing /health may expect a stable schema. Consider adding a minimal "error" field with a sanitized message so the caller can distinguish an internal failure from a component-level DOWN.

Proposed fix
             Map<String, Object> errorResponse = Map.of(
                 "status", "DOWN",
-                "timestamp", Instant.now().toString()
+                "timestamp", Instant.now().toString(),
+                "error", "Unexpected internal error during health check"
             );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/iemr/inventory/controller/health/HealthController.java`
around lines 72 - 81, The catch block in HealthController returns a minimal
errorResponse missing "components" and an "error" detail; update the
ResponseEntity returned in the catch of the health-check method to include a
components object (e.g., an empty or minimal Map under "components") and an
"error" field with a sanitized message (not stack trace) alongside "status" and
"timestamp" so the response schema matches the happy path; adjust the Map
construction used for errorResponse and keep the logger.error("Unexpected error
during health check", e) as-is while returning ResponseEntity<>(errorResponse,
HttpStatus.SERVICE_UNAVAILABLE).
src/main/java/com/iemr/inventory/service/health/HealthService.java (3)

73-77: Thread pool creates non-daemon threads — consider a named thread factory.

Executors.newFixedThreadPool(2) creates non-daemon threads with default names like pool-N-thread-M. If @PreDestroy doesn't fire (e.g., abnormal shutdown), these threads will prevent JVM exit. A custom ThreadFactory with daemon threads and a descriptive name (e.g., health-check-) aids debugging and ensures clean shutdown.

Proposed fix
-        this.executorService = Executors.newFixedThreadPool(2);
+        this.executorService = Executors.newFixedThreadPool(2, r -> {
+            Thread t = new Thread(r, "health-check-" + System.nanoTime());
+            t.setDaemon(true);
+            return t;
+        });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/iemr/inventory/service/health/HealthService.java` around
lines 73 - 77, The constructor in HealthService currently uses
Executors.newFixedThreadPool(2) which creates non‑daemon, generically named
threads; replace that with a fixed thread pool constructed with a custom
ThreadFactory that sets threads as daemon and gives them a descriptive name
prefix (e.g., "health-check-") so they don't block JVM exit if `@PreDestroy` isn't
invoked and are easier to identify in logs; update the HealthService constructor
to build that ThreadFactory and pass it to Executors.newFixedThreadPool, keeping
executorService as the same field.

239-266: performHealthCheck returns the same map reference it mutates — return type is misleading.

The method accepts Map<String, Object> status, mutates it in place, and returns it. Callers at Lines 106/108 discard the return value (used with runAsync + Runnable). Either change the return type to void (cleaner contract) or return a new map (functional style). Mutating a shared reference with an unused return is a footgun.

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

In `@src/main/java/com/iemr/inventory/service/health/HealthService.java` around
lines 239 - 266, performHealthCheck currently mutates the passed-in Map and
returns it, which is misleading because callers (used with runAsync/Runnable)
ignore the return; change the method signature from Map<String,Object>
performHealthCheck(...) to void performHealthCheck(...), remove the return
statement, and keep the existing in-place updates (STATUS_KEY, "responseTimeMs",
SEVERITY_KEY, error/message handling, and determineSeverity/HealthCheckResult
usage) so callers that pass the status map continue to see the updates without a
returned value.

189-237: Remove dead deprecated private methods.

checkMySQLHealth() and checkRedisHealth() are private and @Deprecated(forRemoval = true). Private methods cannot have external callers, so @Deprecated annotations serve no purpose — they only add confusion. Since checkHealth() now calls the sync variants directly, these methods are dead code.

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

In `@src/main/java/com/iemr/inventory/service/health/HealthService.java` around
lines 189 - 237, Remove the dead, private deprecated wrappers checkMySQLHealth()
and checkRedisHealth(): they wrap async CompletableFuture calls to
checkMySQLHealthSync and checkRedisHealthSync but are unused since checkHealth()
now calls the sync variants directly; delete both methods (including their
CompletableFuture usage and related timeout/exception handling) and ensure no
remaining references to checkMySQLHealth or checkRedisHealth exist (leave
checkMySQLHealthSync, checkRedisHealthSync, MYSQL_TIMEOUT_SECONDS,
REDIS_TIMEOUT_SECONDS, and executorService intact).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/com/iemr/inventory/service/health/HealthService.java`:
- Around line 97-139: checkHealth currently races and treats timed-out
components as UP because cancelled futures may leave mysqlStatus/redisStatus
empty; after the CompletableFuture.allOf(...) join/cancel block, add logic to
detect unpopulated component maps and mark them DOWN (use
STATUS_KEY/SEVERITY_KEY with STATUS_DOWN/SEVERITY_CRITICAL and an "error"
message referencing the component name) — implement this as a helper like
ensurePopulated(Map<String,Object> status, String componentName) and call it for
mysqlStatus and redisStatus before building components and calling
computeOverallStatus; also avoid the data race by switching
mysqlStatus/redisStatus from LinkedHashMap to thread-safe maps (e.g.,
ConcurrentHashMap) or by copying their contents into new maps after the
join/cancel block before reading them.

---

Duplicate comments:
In `@src/main/java/com/iemr/inventory/controller/health/HealthController.java`:
- Around line 65-67: The controller now maps overallStatus to HTTP 503 only when
overallStatus equals "DOWN" (using "DOWN".equals(overallStatus)) which is
correct; ensure this remains and add a null-safe check by keeping the
literal-first equals usage for overallStatus in HealthController (reference
overallStatus and healthStatus) and add unit tests that assert responses for
"UP", "DEGRADED", and "DOWN" map to HttpStatus.OK and
HttpStatus.SERVICE_UNAVAILABLE respectively so the behavior is covered by tests.

In `@src/main/java/com/iemr/inventory/service/health/HealthService.java`:
- Around line 115-123: The health check cancels mysqlFuture/redisFuture but
CompletableFuture.cancel(true) doesn't interrupt the pool threads, so the Redis
PING (referenced at the Redis PING call and redisFuture in HealthService) can
block pool threads; to fix this, enforce a real timeout for the Redis operation
instead of relying on cancel: either configure the Redis client command/socket
timeout (spring.data.redis/Lettuce/Jedis) so the PING returns within a bounded
time, or wrap the Redis PING in a cancellable task that uses a dedicated
timeout-aware client call or explicit timeout logic (e.g., use a client API with
command timeout or run the ping via an executor and use future.get(timeout) and
ensure the Redis client supports aborting the operation); keep the existing
stmt.setQueryTimeout usage for MySQL and apply the same explicit timeout
strategy to the Redis check so redisFuture does not occupy pool threads
indefinitely.

---

Nitpick comments:
In `@src/main/java/com/iemr/inventory/controller/health/HealthController.java`:
- Around line 60-70: In HealthController.checkHealth(), change the initial
logger call from logger.info("Health check endpoint called") to
logger.debug(...) to avoid INFO-level noise from frequent health probes; keep
the existing completion log (logger.debug("Health check completed with status:
{}"...)) as-is so both entry and exit are at DEBUG level and the method behavior
and returned ResponseEntity remain unchanged.
- Around line 72-81: The catch block in HealthController returns a minimal
errorResponse missing "components" and an "error" detail; update the
ResponseEntity returned in the catch of the health-check method to include a
components object (e.g., an empty or minimal Map under "components") and an
"error" field with a sanitized message (not stack trace) alongside "status" and
"timestamp" so the response schema matches the happy path; adjust the Map
construction used for errorResponse and keep the logger.error("Unexpected error
during health check", e) as-is while returning ResponseEntity<>(errorResponse,
HttpStatus.SERVICE_UNAVAILABLE).

In `@src/main/java/com/iemr/inventory/service/health/HealthService.java`:
- Around line 73-77: The constructor in HealthService currently uses
Executors.newFixedThreadPool(2) which creates non‑daemon, generically named
threads; replace that with a fixed thread pool constructed with a custom
ThreadFactory that sets threads as daemon and gives them a descriptive name
prefix (e.g., "health-check-") so they don't block JVM exit if `@PreDestroy` isn't
invoked and are easier to identify in logs; update the HealthService constructor
to build that ThreadFactory and pass it to Executors.newFixedThreadPool, keeping
executorService as the same field.
- Around line 239-266: performHealthCheck currently mutates the passed-in Map
and returns it, which is misleading because callers (used with
runAsync/Runnable) ignore the return; change the method signature from
Map<String,Object> performHealthCheck(...) to void performHealthCheck(...),
remove the return statement, and keep the existing in-place updates (STATUS_KEY,
"responseTimeMs", SEVERITY_KEY, error/message handling, and
determineSeverity/HealthCheckResult usage) so callers that pass the status map
continue to see the updates without a returned value.
- Around line 189-237: Remove the dead, private deprecated wrappers
checkMySQLHealth() and checkRedisHealth(): they wrap async CompletableFuture
calls to checkMySQLHealthSync and checkRedisHealthSync but are unused since
checkHealth() now calls the sync variants directly; delete both methods
(including their CompletableFuture usage and related timeout/exception handling)
and ensure no remaining references to checkMySQLHealth or checkRedisHealth exist
(leave checkMySQLHealthSync, checkRedisHealthSync, MYSQL_TIMEOUT_SECONDS,
REDIS_TIMEOUT_SECONDS, and executorService intact).

Copy link

@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 (2)
src/main/java/com/iemr/inventory/service/health/HealthService.java (2)

251-253: performHealthCheck return value is never used — change return type to void.

The method mutates the status map passed by reference, and all callers invoke it via CompletableFuture.runAsync (which discards any return value). Returning status is superfluous and misleads readers into expecting a meaningful return.

♻️ Proposed refactor
-    private Map<String, Object> performHealthCheck(String componentName,
-                                                    Map<String, Object> status,
-                                                    Supplier<HealthCheckResult> checker) {
+    private void performHealthCheck(String componentName,
+                                    Map<String, Object> status,
+                                    Supplier<HealthCheckResult> checker) {

And remove both return status; and return status; (lines 278 and 289).

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

In `@src/main/java/com/iemr/inventory/service/health/HealthService.java` around
lines 251 - 253, The performHealthCheck method currently returns
Map<String,Object> but its return value is never used (callers invoke it via
CompletableFuture.runAsync); change its signature to void (private void
performHealthCheck(String componentName, Map<String,Object> status,
Supplier<HealthCheckResult> checker), remove the two "return status;" statements
inside the method, and ensure callers (which use CompletableFuture.runAsync(()
-> performHealthCheck(...))) remain unchanged; this clarifies that the method
mutates the passed-in status map instead of producing a value.

201-249: Dead deprecated private methods should be removed, not deprecated.

checkMySQLHealth() (line 202) and checkRedisHealth() (line 231) are private methods with no call sites in the file — checkHealth() now calls the *Sync variants directly. Deprecating private methods serves no purpose since they are inaccessible outside the class and there are no consumers to migrate. Keeping them adds maintenance surface and introduces a subtle risk: if either is ever invoked internally, it re-submits to the same shared 2-thread executorService, which could cause thread pool exhaustion under concurrent checkHealth() calls.

These should simply be deleted.

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

In `@src/main/java/com/iemr/inventory/service/health/HealthService.java` around
lines 201 - 249, Remove the two dead private methods checkMySQLHealth() and
checkRedisHealth() (including their `@Deprecated` annotations, javadoc blocks and
entire method bodies) since no internal or external call sites exist and they
re-submit work to the shared executorService; after removal, run a build/test
and delete any now-unused imports/fields (e.g., references only used by those
methods such as the methods' javadocs or timeout constants if they become
unused) to keep the class clean.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/main/java/com/iemr/inventory/service/health/HealthService.java`:
- Around line 117-118: The current use of redisFuture.cancel(true) (and
mysqlFuture.cancel(true)) in checkHealth() is ineffective because
CompletableFuture.cancel does not interrupt the pooled thread; fix by enforcing
a real command-level timeout and ensuring the underlying Redis connection is
closed on timeout: update the Redis health check (e.g., checkRedisHealth()/the
lambda submitted that creates redisFuture) to use the Redis client's
socket/command timeout or client API (Lettuce/Jedis) timeout configuration, or
run the Redis call via an ExecutorService.submit(...) obtaining a
java.util.concurrent.Future and call future.get(timeout, unit) so you can catch
TimeoutException and then explicitly close the Redis connection/resource; remove
reliance on CompletableFuture.cancel(true) and ensure the thread pool size and
task handling won't block subsequent checkHealth() calls.
- Around line 101-102: The mysqlStatus and redisStatus maps are plain
LinkedHashMap and can be written concurrently by performHealthCheck() while the
main thread calls ensurePopulated() and components.put(), causing a data race;
replace the maps with thread-safe implementations (e.g., use
java.util.concurrent.ConcurrentHashMap for mysqlStatus and redisStatus and
import it) so concurrent writes from performHealthCheck() won't race with reads
in ensurePopulated()/components.put(), and ensure any method signatures or local
variable types referencing these maps (mysqlStatus, redisStatus) are updated
accordingly.

---

Nitpick comments:
In `@src/main/java/com/iemr/inventory/service/health/HealthService.java`:
- Around line 251-253: The performHealthCheck method currently returns
Map<String,Object> but its return value is never used (callers invoke it via
CompletableFuture.runAsync); change its signature to void (private void
performHealthCheck(String componentName, Map<String,Object> status,
Supplier<HealthCheckResult> checker), remove the two "return status;" statements
inside the method, and ensure callers (which use CompletableFuture.runAsync(()
-> performHealthCheck(...))) remain unchanged; this clarifies that the method
mutates the passed-in status map instead of producing a value.
- Around line 201-249: Remove the two dead private methods checkMySQLHealth()
and checkRedisHealth() (including their `@Deprecated` annotations, javadoc blocks
and entire method bodies) since no internal or external call sites exist and
they re-submit work to the shared executorService; after removal, run a
build/test and delete any now-unused imports/fields (e.g., references only used
by those methods such as the methods' javadocs or timeout constants if they
become unused) to keep the class clean.

Copy link

@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: 2

🧹 Nitpick comments (1)
src/main/java/com/iemr/inventory/service/health/HealthService.java (1)

225-263: Remove unused methods checkMySQLHealth() and checkRedisHealth().

These methods (lines 225–263) are dead code—never invoked anywhere in the codebase. checkHealth() calls checkMySQLHealthSync and checkRedisHealthSync directly via performHealthCheck instead. Keeping these unused methods creates unnecessary clutter and potential confusion during maintenance.

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

In `@src/main/java/com/iemr/inventory/service/health/HealthService.java` around
lines 225 - 263, Delete the two unused wrapper methods checkMySQLHealth() and
checkRedisHealth() from the HealthService class; they are dead code (never
called) because checkHealth() uses checkMySQLHealthSync and checkRedisHealthSync
directly via performHealthCheck. Remove both methods (including their
CompletableFuture logic and associated timeout/exception handling) and ensure no
other code references checkMySQLHealth or checkRedisHealth; leave
checkMySQLHealthSync, checkRedisHealthSync and performHealthCheck intact for
actual health checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/com/iemr/inventory/service/health/HealthService.java`:
- Around line 462-475: The hasDeadlocks(Connection) method currently treats any
historical "LATEST DETECTED DEADLOCK" in the output of "SHOW ENGINE INNODB
STATUS" as active; update hasDeadlocks to parse the timestamp that precedes the
deadlock entry in the innodbStatus string and only return true if that timestamp
is within a configurable recency window (e.g., deadlockRecencySeconds),
otherwise return false; add a config/constant deadlockRecencySeconds, implement
robust parsing of the timestamp format present in the INNODB status (falling
back to previous behavior only if parsing fails), and log parse failures via
logger.debug in the hasDeadlocks method.
- Around line 393-411: The executeAdvancedChecksWithTimeout method causes nested
submissions to the same executor and uses a Connection that may be closed before
the queued task runs; remove executeAdvancedChecksWithTimeout entirely and
change the MySQL health flow to call performAdvancedMySQLChecks(connection)
directly and synchronously from within the MySQL executor task (e.g., from
checkMySQLHealthSync or performHealthCheck("MySQL", ...)), so no additional task
is submitted to executorService and the Connection is used on the same thread;
rely on the per-query JDBC timeouts (stmt.setQueryTimeout) and the existing
outer maxTimeout in checkHealth() to bound runtime and delete any timeout/cancel
logic and associated log branches in executeAdvancedChecksWithTimeout.

---

Duplicate comments:
In `@src/main/java/com/iemr/inventory/service/health/HealthService.java`:
- Around line 131-132: The LinkedHashMap instances mysqlStatus and redisStatus
are vulnerable to concurrent writes from performHealthCheck while the main
thread may call ensurePopulated after an allOf().get() timeout/cancel; replace
those LinkedHashMap constructions with thread-safe ConcurrentHashMap (or
Collections.newConcurrentMap) so concurrent updates are safe, and ensure any
iteration in ensurePopulated/response building uses the concurrent map snapshot
or safe iteration; update references to mysqlStatus and redisStatus accordingly.
- Around line 147-148: The current use of CompletableFuture.cancel(true)
(mysqlFuture and redisFuture) doesn't interrupt the underlying JDBC/Redis work
and leaves executorService threads blocked; change the async health checks to
use executorService.submit(Callable) (store the returned Future<?> e.g.,
mysqlTask and redisTask) so cancel(true) will attempt to interrupt the worker
thread, and also ensure the JDBC/Redis operations respect interrupts or have
explicit socket/query timeouts (e.g., Statement.setQueryTimeout or client
timeouts) so the interrupted thread can terminate promptly; update HealthService
to cancel those submitted Futures instead of calling cancel on the
CompletableFutures and add interrupt-aware/timeout configuration in the
JDBC/Redis check methods.

---

Nitpick comments:
In `@src/main/java/com/iemr/inventory/service/health/HealthService.java`:
- Around line 225-263: Delete the two unused wrapper methods checkMySQLHealth()
and checkRedisHealth() from the HealthService class; they are dead code (never
called) because checkHealth() uses checkMySQLHealthSync and checkRedisHealthSync
directly via performHealthCheck. Remove both methods (including their
CompletableFuture logic and associated timeout/exception handling) and ensure no
other code references checkMySQLHealth or checkRedisHealth; leave
checkMySQLHealthSync, checkRedisHealthSync and performHealthCheck intact for
actual health checks.

@sonarqubecloud
Copy link

@drtechie drtechie merged commit af64c6c 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