From 7e0044b6bec90c56c3830fbec84d6176f3f1649c Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Wed, 12 Aug 2026 17:18:24 +0530 Subject: [PATCH 1/2] fix: remove hardcoded schema and update the health api to show the warning on ben id availability --- src/main/environment/bengen_docker.properties | 3 + .../environment/bengen_example.properties | 3 + .../common/bengen/repo/BeneficiaryIdRepo.java | 2 +- .../service/GenerateBeneficiaryService.java | 8 +- .../bengen/service/health/HealthService.java | 172 +++++++++++++++--- .../GenerateBeneficiaryServiceTest.java | 2 +- 6 files changed, 163 insertions(+), 27 deletions(-) diff --git a/src/main/environment/bengen_docker.properties b/src/main/environment/bengen_docker.properties index b4b6ad1..bb6fe16 100644 --- a/src/main/environment/bengen_docker.properties +++ b/src/main/environment/bengen_docker.properties @@ -21,6 +21,9 @@ no-of-benID-to-be-generate=25000 ### Total available Ben should not be less than this lower-limit-of-beneficiary=20000 +### Health API warns (status DEGRADED) when available Ben IDs fall below this +health.min-available-beneficiary-ids=5000 + ### Redis IP spring.redis.host=${REDIS_HOST} diff --git a/src/main/environment/bengen_example.properties b/src/main/environment/bengen_example.properties index 1c7a79c..a835201 100644 --- a/src/main/environment/bengen_example.properties +++ b/src/main/environment/bengen_example.properties @@ -22,6 +22,9 @@ no-of-benID-to-be-generate=25000 ### Total available Ben should not be less than this lower-limit-of-beneficiary=20000 +### Health API warns (status DEGRADED) when available Ben IDs fall below this +health.min-available-beneficiary-ids=5000 + ### Redis IP spring.redis.host=localhost jwt.secret=my-32-character-ultra-secure-and-ultra-long-secret diff --git a/src/main/java/com/iemr/common/bengen/repo/BeneficiaryIdRepo.java b/src/main/java/com/iemr/common/bengen/repo/BeneficiaryIdRepo.java index 1a4a594..aa5bc98 100644 --- a/src/main/java/com/iemr/common/bengen/repo/BeneficiaryIdRepo.java +++ b/src/main/java/com/iemr/common/bengen/repo/BeneficiaryIdRepo.java @@ -116,7 +116,7 @@ Long countByReservedForPSMapIdAndProvisionedAndReserved( @Query(nativeQuery = true, value = "Select benregMap.benRegId, benregMap.beneficiaryId, " + "benregMap.CreatedDate " - + "from db_identity.m_beneficiaryregidmapping benregMap " + + "from m_beneficiaryregidmapping benregMap " + "where benregMap.provisioned =false and benregMap.reserved =true " + "and benregMap.vanID=:vanID order by benregMap.benRegId desc limit :num ") List getBenIDGenerated(@Param("vanID") Integer vanID, @Param("num") Long num); diff --git a/src/main/java/com/iemr/common/bengen/service/GenerateBeneficiaryService.java b/src/main/java/com/iemr/common/bengen/service/GenerateBeneficiaryService.java index ddb42fb..28bc8b2 100644 --- a/src/main/java/com/iemr/common/bengen/service/GenerateBeneficiaryService.java +++ b/src/main/java/com/iemr/common/bengen/service/GenerateBeneficiaryService.java @@ -103,11 +103,11 @@ public StringBuffer createQuery(Integer num) { Generator g = new Generator(); StringBuffer sb = new StringBuffer( - "INSERT INTO `db_identity`.`m_beneficiaryregidmapping` " + + "INSERT INTO `m_beneficiaryregidmapping` " + "(`BeneficiaryID`,`Provisioned`,`Deleted`," + "`CreatedDate`,`CreatedBy`) VALUES "); - // INSERT INTO `db_identity`.`m_beneficiaryregidmapping` + // INSERT INTO `m_beneficiaryregidmapping` // (`BeneficiaryID`,`Provisioned`,`Deleted`,`CreatedDate`,`CreatedBy`) VALUES // (<{BeneficiaryID: }>, <{Provisioned: b'0'}>, <{Deleted: b'0'}>, // <{CreatedDate: CURRENT_TIMESTAMP}>, <{CreatedBy: }>); @@ -143,7 +143,7 @@ public StringBuffer createQuery(Integer num) { public void testLoopGenr() { List strList = new ArrayList(); StringBuffer sb = new StringBuffer( - "INSERT INTO `db_identity`.`m_beneficiaryregidmapping` " + + "INSERT INTO `m_beneficiaryregidmapping` " + "(`BeneficiaryID`,`Provisioned`,`Deleted`,`CreatedDate`," + "`CreatedBy`) VALUES "); Timestamp ts = Timestamp.from(Instant.now()); @@ -167,7 +167,7 @@ public List getBeneficiaryIDs(Long num, Integer vanID long strt = System.currentTimeMillis(); Generator g = new Generator(); StringBuffer sb = new StringBuffer( - "INSERT INTO `db_identity`.`m_beneficiaryregidmapping` " + + "INSERT INTO `m_beneficiaryregidmapping` " + "(`BeneficiaryID`,`Provisioned`,`Deleted`,`Reserved`," + "`CreatedDate`,`CreatedBy`,`VanID`) VALUES "); Timestamp ts = Timestamp.from(Instant.now()); diff --git a/src/main/java/com/iemr/common/bengen/service/health/HealthService.java b/src/main/java/com/iemr/common/bengen/service/health/HealthService.java index 2aca8c1..dea23d7 100644 --- a/src/main/java/com/iemr/common/bengen/service/health/HealthService.java +++ b/src/main/java/com/iemr/common/bengen/service/health/HealthService.java @@ -24,6 +24,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; @@ -47,8 +48,16 @@ public class HealthService { private static final String RESPONSE_TIME_KEY = "responseTimeMs"; private static final long MYSQL_TIMEOUT_SECONDS = 3; private static final long REDIS_TIMEOUT_SECONDS = 3; + private static final long BENEFICIARY_POOL_TIMEOUT_SECONDS = 3; private static final long ADVANCED_CHECKS_THROTTLE_SECONDS = 30; + private static final long POOL_CHECK_THROTTLE_SECONDS = 30; private static final long RESPONSE_TIME_THRESHOLD_MS = 2000; + private static final String BENEFICIARY_POOL_KEY = "beneficiaryIdPool"; + private static final String AVAILABLE_IDS_KEY = "availableIds"; + private static final String THRESHOLD_KEY = "threshold"; + // Mirrors BeneficiaryIdRepo.countBenID(): unprovisioned and unreserved IDs are the usable pool. + private static final String COUNT_AVAILABLE_IDS_SQL = + "SELECT COUNT(*) FROM m_beneficiaryregidmapping WHERE Provisioned = 0 AND Reserved = 0"; private static final String DIAGNOSTIC_LOCK_WAIT = "MYSQL_LOCK_WAIT"; private static final String DIAGNOSTIC_SLOW_QUERIES = "MYSQL_SLOW_QUERIES"; private static final String DIAGNOSTIC_POOL_EXHAUSTED = "MYSQL_POOL_EXHAUSTED"; @@ -61,14 +70,24 @@ public class HealthService { private volatile long lastAdvancedCheckTime = 0; private volatile AdvancedCheckResult cachedAdvancedCheckResult = null; private final ReentrantReadWriteLock advancedCheckLock = new ReentrantReadWriteLock(); - + + // Warn when the usable beneficiary ID pool falls below this count + private final int minAvailableBeneficiaryIds; + + // Cached pool count so frequent health polls do not repeatedly scan the table. + // A benign race here only costs one extra COUNT query, so no lock is needed. + private volatile long lastPoolCheckTime = 0; + private volatile Long cachedAvailableIdCount = null; + // Advanced checks always enabled private static final boolean ADVANCED_HEALTH_CHECKS_ENABLED = true; public HealthService(DataSource dataSource, - @Autowired(required = false) RedisTemplate redisTemplate) { + @Autowired(required = false) RedisTemplate redisTemplate, + @Value("${health.min-available-beneficiary-ids:5000}") int minAvailableBeneficiaryIds) { this.dataSource = dataSource; this.redisTemplate = redisTemplate; + this.minAvailableBeneficiaryIds = minAvailableBeneficiaryIds; this.executorService = Executors.newFixedThreadPool(6); } @@ -92,34 +111,39 @@ public void shutdown() { public Map checkHealth() { Map mysqlStatus = new ConcurrentHashMap<>(); Map redisStatus = new ConcurrentHashMap<>(); + Map beneficiaryPoolStatus = new ConcurrentHashMap<>(); Future mysqlFuture = executorService.submit( () -> performHealthCheck("MySQL", mysqlStatus, this::checkMySQLHealthSync)); Future redisFuture = executorService.submit( () -> performHealthCheck("Redis", redisStatus, this::checkRedisHealthSync)); + Future beneficiaryPoolFuture = executorService.submit( + () -> checkBeneficiaryIdPool(beneficiaryPoolStatus)); - // Wait for both checks to complete with combined timeout (shared deadline) - long maxTimeout = Math.max(MYSQL_TIMEOUT_SECONDS, REDIS_TIMEOUT_SECONDS) + 1; - awaitHealthChecks(mysqlFuture, redisFuture, maxTimeout); + // Wait for all checks to complete with combined timeout (shared deadline) + long maxTimeout = Math.max(Math.max(MYSQL_TIMEOUT_SECONDS, REDIS_TIMEOUT_SECONDS), + BENEFICIARY_POOL_TIMEOUT_SECONDS) + 1; + awaitHealthChecks(maxTimeout, mysqlFuture, redisFuture, beneficiaryPoolFuture); // Ensure timed-out or unfinished components are marked DOWN ensurePopulated(mysqlStatus, "MySQL"); ensurePopulated(redisStatus, "Redis"); + if (!beneficiaryPoolStatus.containsKey(STATUS_KEY)) { + markPoolUnknown(beneficiaryPoolStatus, "Beneficiary ID pool check did not complete in time"); + } // Build response in the standardized AMRIT shape (aligned with Common-API): // top-level status + checkedAt, then per-service status/severity summaries. Map response = new LinkedHashMap<>(); - String mysqlOverall = (String) mysqlStatus.get(STATUS_KEY); - String redisOverall = (String) redisStatus.get(STATUS_KEY); - boolean overallUp = !STATUS_DOWN.equals(mysqlOverall) && !STATUS_DOWN.equals(redisOverall); - - response.put(STATUS_KEY, overallUp ? STATUS_UP : STATUS_DOWN); + response.put(STATUS_KEY, computeOverallStatus(mysqlStatus, redisStatus, beneficiaryPoolStatus)); response.put("checkedAt", Instant.now().toString()); // Expose only status and severity; keep diagnostics (responseTime, messages) internal response.put("mysql", summarize(mysqlStatus)); response.put("redis", summarize(redisStatus)); + // The pool also reports its count and threshold, which operators need to act on the warning + response.put(BENEFICIARY_POOL_KEY, summarizeBeneficiaryPool(beneficiaryPoolStatus)); return response; } @@ -131,30 +155,136 @@ private Map summarize(Map componentStatus) { return summary; } - private void awaitHealthChecks(Future mysqlFuture, Future redisFuture, long maxTimeoutSeconds) { + private Map summarizeBeneficiaryPool(Map componentStatus) { + Map summary = summarize(componentStatus); + if (componentStatus.containsKey(AVAILABLE_IDS_KEY)) { + summary.put(AVAILABLE_IDS_KEY, componentStatus.get(AVAILABLE_IDS_KEY)); + } + summary.put(THRESHOLD_KEY, minAvailableBeneficiaryIds); + if (componentStatus.containsKey(MESSAGE_KEY)) { + summary.put(MESSAGE_KEY, componentStatus.get(MESSAGE_KEY)); + } + if (componentStatus.containsKey(ERROR_KEY)) { + summary.put(ERROR_KEY, componentStatus.get(ERROR_KEY)); + } + return summary; + } + + /** + * Overall status is DOWN if any component is DOWN, DEGRADED if any component + * reports a warning (for example a low beneficiary ID pool), otherwise UP. + */ + @SafeVarargs + private final String computeOverallStatus(Map... componentStatuses) { + boolean hasDegraded = false; + for (Map componentStatus : componentStatuses) { + String status = (String) componentStatus.get(STATUS_KEY); + String severity = (String) componentStatus.get(SEVERITY_KEY); + if (STATUS_DOWN.equals(status) || SEVERITY_CRITICAL.equals(severity)) { + return STATUS_DOWN; + } + if (STATUS_DEGRADED.equals(status) || SEVERITY_WARNING.equals(severity)) { + hasDegraded = true; + } + } + return hasDegraded ? STATUS_DEGRADED : STATUS_UP; + } + + private void awaitHealthChecks(long maxTimeoutSeconds, Future... futures) { long deadlineNs = System.nanoTime() + TimeUnit.SECONDS.toNanos(maxTimeoutSeconds); try { - mysqlFuture.get(maxTimeoutSeconds, TimeUnit.SECONDS); - long remainingNs = deadlineNs - System.nanoTime(); - if (remainingNs > 0) { - redisFuture.get(remainingNs, TimeUnit.NANOSECONDS); - } else { - redisFuture.cancel(true); + for (Future future : futures) { + long remainingNs = deadlineNs - System.nanoTime(); + if (remainingNs <= 0) { + future.cancel(true); + continue; + } + future.get(remainingNs, TimeUnit.NANOSECONDS); } } catch (TimeoutException e) { logger.warn("Health check aggregate timeout after {} seconds", maxTimeoutSeconds); - mysqlFuture.cancel(true); - redisFuture.cancel(true); + cancelAll(futures); } catch (InterruptedException e) { Thread.currentThread().interrupt(); logger.warn("Health check was interrupted"); - mysqlFuture.cancel(true); - redisFuture.cancel(true); + cancelAll(futures); } catch (Exception e) { logger.warn("Health check execution error: {}", e.getMessage()); } } + private void cancelAll(Future... futures) { + for (Future future : futures) { + future.cancel(true); + } + } + + /** + * Flags a WARNING when the usable beneficiary ID pool drops below the configured + * threshold, so a draining pool is visible before registration stops entirely. + * Reported as DEGRADED rather than DOWN, so the endpoint still returns HTTP 200. + */ + private void checkBeneficiaryIdPool(Map status) { + long startTime = System.currentTimeMillis(); + try { + long availableIds = countAvailableBeneficiaryIdsWithThrottle(); + status.put(RESPONSE_TIME_KEY, System.currentTimeMillis() - startTime); + status.put(AVAILABLE_IDS_KEY, availableIds); + + if (availableIds < minAvailableBeneficiaryIds) { + status.put(STATUS_KEY, STATUS_DEGRADED); + status.put(SEVERITY_KEY, SEVERITY_WARNING); + status.put(MESSAGE_KEY, "Available beneficiary ID pool is below the configured threshold"); + logger.warn("Beneficiary ID pool low: {} available, warning threshold is {}", + availableIds, minAvailableBeneficiaryIds); + } else { + status.put(STATUS_KEY, STATUS_UP); + status.put(SEVERITY_KEY, SEVERITY_OK); + } + } catch (Exception e) { + logger.warn("Beneficiary ID pool check failed: {}", e.getMessage(), e); + status.put(RESPONSE_TIME_KEY, System.currentTimeMillis() - startTime); + // A failed count is not an outage: MySQL itself is checked separately, so this + // stays DEGRADED to avoid a slow count returning 503 and dropping the instance + // out of load-balancer rotation. + markPoolUnknown(status, "Beneficiary ID pool count could not be determined"); + } + } + + private void markPoolUnknown(Map status, String error) { + status.put(STATUS_KEY, STATUS_DEGRADED); + status.put(SEVERITY_KEY, SEVERITY_WARNING); + status.put(ERROR_KEY, error); + } + + private long countAvailableBeneficiaryIdsWithThrottle() throws Exception { + long currentTime = System.currentTimeMillis(); + Long cached = cachedAvailableIdCount; + if (cached != null && (currentTime - lastPoolCheckTime) < POOL_CHECK_THROTTLE_SECONDS * 1000) { + return cached; + } + + long availableIds = countAvailableBeneficiaryIds(); + lastPoolCheckTime = currentTime; + cachedAvailableIdCount = availableIds; + return availableIds; + } + + private long countAvailableBeneficiaryIds() throws Exception { + try (Connection connection = dataSource.getConnection(); + PreparedStatement stmt = connection.prepareStatement(COUNT_AVAILABLE_IDS_SQL)) { + + stmt.setQueryTimeout((int) BENEFICIARY_POOL_TIMEOUT_SECONDS); + + try (ResultSet rs = stmt.executeQuery()) { + if (!rs.next()) { + throw new IllegalStateException("No result from beneficiary ID pool count query"); + } + return rs.getLong(1); + } + } + } + private void ensurePopulated(Map status, String componentName) { if (!status.containsKey(STATUS_KEY)) { status.put(STATUS_KEY, STATUS_DOWN); diff --git a/src/test/java/com/iemr/common/bengen/service/GenerateBeneficiaryServiceTest.java b/src/test/java/com/iemr/common/bengen/service/GenerateBeneficiaryServiceTest.java index 6d38253..df240a0 100644 --- a/src/test/java/com/iemr/common/bengen/service/GenerateBeneficiaryServiceTest.java +++ b/src/test/java/com/iemr/common/bengen/service/GenerateBeneficiaryServiceTest.java @@ -73,7 +73,7 @@ class GenerateBeneficiaryServiceTest { Path tempDir; private static final BigInteger MOCKED_BENEFICIARY_ID = new BigInteger("12345678901"); - private static final String EXPECTED_TABLE_NAME = "`db_identity`.`m_beneficiaryregidmapping`"; + private static final String EXPECTED_TABLE_NAME = "`m_beneficiaryregidmapping`"; private static final String EXPECTED_CREATOR = "admin-batch"; @BeforeEach From a61b82ce5479379385afd3aed91e65b7a4b6c1c3 Mon Sep 17 00:00:00 2001 From: vanitha1822 Date: Wed, 12 Aug 2026 17:21:14 +0530 Subject: [PATCH 2/2] fix: update the cron to run daily --- src/main/environment/bengen_example.properties | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/environment/bengen_example.properties b/src/main/environment/bengen_example.properties index a835201..bd3742c 100644 --- a/src/main/environment/bengen_example.properties +++ b/src/main/environment/bengen_example.properties @@ -13,6 +13,9 @@ start-bengen-scheduler=true # To Run scheduler Every MON,WED,FRI cron-scheduler-bengen=0 1 0 ? * MON,WED,FRI,SUN * +# To Run scheduler Every Day +# cron-scheduler-bengen=0 1 0 * * ? * + # To Run scheduler Every Minute #cron-scheduler-bengen=0 0/1 * * * ? *