Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/main/environment/common_ci.properties
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,25 @@ benGenUrlCentral=@env.COMMON_API_OPEN_SYNC@beneficiary/generateBeneficiaryIDs

## benImportUrl, identity, local
benImportUrlLocal=@env.IDENTITY_API_BASE_URL@id/saveGeneratedBenIDToLocalServer

## benCheckUrl, identity, local - counts genuinely claimable BenIDs left on this device
benCheckUrlLocal=@env.IDENTITY_API_BASE_URL@id/checkAvailablBenIDLocalServer

## BenID pool auto top-up scheduler - reuses the same stoptb.enforce.vanid /
## stoptb.van.id properties already used by Identity-API/FLW-API/TM-API on
## this same laptop, rather than a separate name for the same concept.
## This device is a van/laptop only if stoptb.enforce.vanid=true.
stoptb.enforce.vanid=@env.STOPTB_ENFORCE_VANID@
stoptb.van.id=@env.STOPTB_VAN_ID@
benid.pool.lowerLimit=@env.BENID_POOL_LOWER_LIMIT@
benid.pool.batchSize=@env.BENID_POOL_BATCH_SIZE@
benid.pool.bootstrapSize=@env.BENID_POOL_BOOTSTRAP_SIZE@
## retry window - top-up retries only fire between these hours (24h clock),
## confined to overnight so retries never compete with active camp-hour usage
benid.pool.retryWindow.startHour=@env.BENID_POOL_RETRY_WINDOW_START_HOUR@
benid.pool.retryWindow.endHour=@env.BENID_POOL_RETRY_WINDOW_END_HOUR@
## static server-to-server credential for the scheduler's own calls (no user session exists)
benGenSchedulerCredential=@env.BENGEN_SCHEDULER_CREDENTIAL@
mmucentralserver = @env.MMU_CENTRAL_SERVER@common/get/Case-sheet/centralServerTMCaseSheet

tmCentralServer = @env.TM_CENTRAL_SERVER@common/get/Case-sheet/printData
Expand Down
19 changes: 19 additions & 0 deletions src/main/environment/common_docker.properties
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,25 @@ benGenUrlCentral=${COMMON_API_OPEN_SYNC}beneficiary/generateBeneficiaryIDs

## benImportUrl, identity, local
benImportUrlLocal=${IDENTITY_API_BASE_URL}id/saveGeneratedBenIDToLocalServer

## benCheckUrl, identity, local - counts genuinely claimable BenIDs left on this device
benCheckUrlLocal=${IDENTITY_API_BASE_URL}id/checkAvailablBenIDLocalServer

## BenID pool auto top-up scheduler - reuses the same stoptb.enforce.vanid /
## stoptb.van.id properties already used by Identity-API/FLW-API/TM-API on
## this same laptop, rather than a separate name for the same concept.
## This device is a van/laptop only if stoptb.enforce.vanid=true.
stoptb.enforce.vanid=${STOPTB_ENFORCE_VANID}
stoptb.van.id=${STOPTB_VAN_ID}
benid.pool.lowerLimit=${BENID_POOL_LOWER_LIMIT}
benid.pool.batchSize=${BENID_POOL_BATCH_SIZE}
benid.pool.bootstrapSize=${BENID_POOL_BOOTSTRAP_SIZE}
## retry window - top-up retries only fire between these hours (24h clock),
## confined to overnight so retries never compete with active camp-hour usage
benid.pool.retryWindow.startHour=${BENID_POOL_RETRY_WINDOW_START_HOUR}
benid.pool.retryWindow.endHour=${BENID_POOL_RETRY_WINDOW_END_HOUR}
## static server-to-server credential for the scheduler's own calls (no user session exists)
benGenSchedulerCredential=${BENGEN_SCHEDULER_CREDENTIAL}
mmucentralserver = ${MMU_CENTRAL_SERVER}common/get/Case-sheet/centralServerTMCaseSheet

tmCentralServer = ${TM_CENTRAL_SERVER}common/get/Case-sheet/printData
Expand Down
19 changes: 19 additions & 0 deletions src/main/environment/common_example.properties
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,25 @@ benGenUrlCentral=http://10.208.122.38:8080/common-api/beneficiary/generateBenefi
## benImportUrl, identity, local
benImportUrlLocal=http://localhost:8094/id/saveGeneratedBenIDToLocalServer

## benCheckUrl, identity, local - counts genuinely claimable BenIDs left on this device
benCheckUrlLocal=http://localhost:8094/id/checkAvailablBenIDLocalServer

## BenID pool auto top-up scheduler - reuses the same stoptb.enforce.vanid /
## stoptb.van.id properties already used by Identity-API/FLW-API/TM-API on
## this same laptop, rather than a separate name for the same concept.
## This device is a van/laptop only if stoptb.enforce.vanid=true.
stoptb.enforce.vanid=false
stoptb.van.id=0
benid.pool.lowerLimit=2000
benid.pool.batchSize=1000
benid.pool.bootstrapSize=10000
## retry window - top-up retries only fire between these hours (24h clock),
## confined to overnight so retries never compete with active camp-hour usage
benid.pool.retryWindow.startHour=18
benid.pool.retryWindow.endHour=6
## static server-to-server credential for the scheduler's own calls (no user session exists)
benGenSchedulerCredential=mmu-scheduler

## inventory transaction download URL, central, mmu
dataSyncTransactionDownloadUrl=http://10.208.122.38:8080/mmu-api/dataSync/server-to-van-transactional

Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/iemr/mmu/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scheduling.annotation.EnableScheduling;

import com.iemr.mmu.data.login.Users;
import com.iemr.mmu.utils.IEMRApplBeans;

@SpringBootApplication
@EnableScheduling
public class Application {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
/*
* AMRIT – Accessible Medical Records via Integrated Technology
* Integrated EHR (Electronic Health Records) Solution
*
* Copyright (C) "Piramal Swasthya Management and Research Institute"
*
* This file is part of AMRIT.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package com.iemr.mmu.service.dataSyncActivity;

import java.time.LocalTime;
import java.util.concurrent.atomic.AtomicBoolean;

import org.json.JSONObject;
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.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import jakarta.annotation.PostConstruct;

/**
* Automatically keeps this device's local BenID pool topped up, replacing the
* manual "Check BenID" / "Generate BenID" click on the Data Sync screen.
*
* Only runs on a van/laptop deployment (stoptb.enforce.vanid=true). MMU-API
* is also deployed on the central server for the receiving side of data sync
* (com.iemr.mmu.controller.dataSyncLayerCentral) — that deployment sets
* stoptb.enforce.vanid=false, and every method here exits immediately in
* that case. Central's own BenID pool is topped up separately by
* BeneficiaryID-Generation-API's own scheduled job — unrelated, untouched.
*
* Triggers:
* - On startup: bootstrap fetch if the local pool is empty (first-time setup).
* - Hourly, but ONLY within a configurable overnight window (default 6 PM -
* 6 AM, via benid.pool.retryWindow.startHour/endHour): top up if the pool
* has dropped below the low-watermark. Confined to overnight on purpose -
* retrying during active camp hours would add doomed network attempts on
* an already-strained connection right when the app is actually in use.
*
* Safety rules:
* - Never two downloads running at once (inProgress guard).
* - A dropped/slow connection times out rather than hanging the job thread.
* - A partial/garbled response is never trusted as success.
* - Every attempt requests a fresh batch, so a failed/lost attempt only
* wastes that batch — it can never produce a duplicate BenID.
*/
@Component
public class BenIDPoolTopUpScheduler {

private final Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName());

private static final int CONNECT_TIMEOUT_MS = 15_000;
private static final int READ_TIMEOUT_MS = 30_000;

@Autowired

Check warning on line 77 in src/main/java/com/iemr/mmu/service/dataSyncActivity/BenIDPoolTopUpScheduler.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this field injection and use constructor injection instead.

See more on https://sonarcloud.io/project/issues?id=PSMRI_MMU-API&issues=AaCQV-slLkljabW4nbQ-&open=AaCQV-slLkljabW4nbQ-&pullRequest=186
private DownloadDataFromServerImpl downloadDataFromServerImpl;

// Same stoptb.enforce.vanid / stoptb.van.id properties already used by
// Identity-API/FLW-API/TM-API on this same laptop - not a separate name
// for the same concept. No inline default on the enforce flag: every
// properties file must set it explicitly, so a forgotten config fails
// loudly at startup instead of silently running fail-open.
@Value("${stoptb.enforce.vanid}")
private boolean enforceVanID;

@Value("${stoptb.van.id}")
private int vanID;

@Value("${benid.pool.lowerLimit:2000}")
private long lowerLimit;

@Value("${benid.pool.batchSize:1000}")
private long batchSize;

@Value("${benid.pool.bootstrapSize:10000}")
private long bootstrapSize;

@Value("${benCheckUrlLocal}")
private String benCheckUrlLocal;

@Value("${benGenSchedulerCredential:mmu-scheduler}")
private String schedulerCredential;

// Retry window - confines every retry attempt (after the one-time startup
// bootstrap) to overnight hours, configurable per deployment. Default
// 18 (6 PM) to 6 (6 AM); startHour > endHour is treated as wrapping past
// midnight, which is the normal case.
@Value("${benid.pool.retryWindow.startHour:18}")
private int retryWindowStartHour;

@Value("${benid.pool.retryWindow.endHour:6}")
private int retryWindowEndHour;

private final AtomicBoolean inProgress = new AtomicBoolean(false);
private RestTemplate restTemplate;

@PostConstruct
public void init() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(CONNECT_TIMEOUT_MS);
factory.setReadTimeout(READ_TIMEOUT_MS);
this.restTemplate = new RestTemplate(factory);

// Bootstrap: brand-new laptop, pool is empty - fetch immediately if
// internet happens to be available right now at setup/startup.
attemptTopUp(bootstrapSize, true);
}

/**
* Runs every hour, on the hour, every day - but only actually attempts a
* top-up when the current time falls inside the configured overnight
* window. Outside that window it's a no-op, by design: retrying during
* active camp hours would add doomed network attempts on an
* already-strained connection right when the app is in use.
*/
@Scheduled(cron = "0 0 * * * *")
public void hourlyCheckWithinRetryWindow() {
if (!isWithinRetryWindow()) {
return;
}
attemptTopUp(batchSize, false);
}

private boolean isWithinRetryWindow() {
int hour = LocalTime.now().getHour();

Check warning on line 147 in src/main/java/com/iemr/mmu/service/dataSyncActivity/BenIDPoolTopUpScheduler.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Explicitly specify the time zone by passing a ZoneId or a Clock to the .now() method.

See more on https://sonarcloud.io/project/issues?id=PSMRI_MMU-API&issues=AaCQV-slLkljabW4nbQ_&open=AaCQV-slLkljabW4nbQ_&pullRequest=186
if (retryWindowStartHour <= retryWindowEndHour) {
// Same-day window, e.g. startHour=9, endHour=17.
return hour >= retryWindowStartHour && hour < retryWindowEndHour;
}
// Wraps past midnight, e.g. startHour=18, endHour=6 (the default).
return hour >= retryWindowStartHour || hour < retryWindowEndHour;
}

private void attemptTopUp(long amount, boolean isBootstrap) {
if (!enforceVanID) {
// Central deployment of MMU-API (or a van with the feature turned
// off) - this whole mechanism is van/laptop-only. Central tops up
// its own pool via BeneficiaryID-Generation-API's own scheduler.
return;
}
if (vanID <= 0) {
logger.warn("stoptb.enforce.vanid=true but stoptb.van.id is 0 - skipping BenID auto top-up");
return;
}
if (!inProgress.compareAndSet(false, true)) {
logger.info("BenID auto top-up already in progress - skipping this trigger");
return;
}
try {
long available = checkLocalAvailability();
boolean needsTopUp = isBootstrap ? available <= 0 : available < lowerLimit;
if (!needsTopUp) {
logger.info("BenID pool healthy ({} available for vanID {}) - no top-up needed", available, vanID);
return;
}

JSONObject requestObj = new JSONObject();
requestObj.put("vanID", vanID);
requestObj.put("benIDRequired", amount);

// Static server-to-server credential in place of a live user JWT -
// there is no logged-in session in a scheduled job. Passed as both
// Authorization and ServerAuthorization, with token="datasync" so
// the central-facing leg uses the existing sentinel-token path.
int result = downloadDataFromServerImpl.callCentralAPIToGenerateBenIDAndimportToLocal(
requestObj.toString(), schedulerCredential, schedulerCredential, "datasync");

if (result == 2) {
logger.info("BenID auto top-up succeeded: requested {} IDs for vanID {}", amount, vanID);
} else {
logger.warn(
"BenID auto top-up for vanID {} did not fully complete (result code {}) - will retry later",
vanID, result);
}
} catch (Exception e) {
// No user-facing alert by design - poor-network camps would
// otherwise get spammed. Just log and let the next scheduled
// attempt retry.
logger.warn("BenID auto top-up failed for vanID {} - will retry on next scheduled attempt: {}", vanID,
e.getMessage());
} finally {
inProgress.set(false);
}
}

/**
* Checks how many BenIDs are genuinely claimable right now on this
* device. Rejects anything short of a clean, fully-parsed success -
* a truncated/garbled response from a dying connection must never be
* trusted.
*/
private long checkLocalAvailability() {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", schedulerCredential);
ResponseEntity<String> response = restTemplate.exchange(benCheckUrlLocal, HttpMethod.GET,
new HttpEntity<Void>(headers), String.class);

if (response == null || !response.hasBody()) {
throw new IllegalStateException("Empty response checking local BenID availability");
}
JSONObject obj = new JSONObject(response.getBody());
if (!obj.has("data") || !obj.has("statusCode") || obj.getInt("statusCode") != 200) {
throw new IllegalStateException("Unexpected response checking local BenID availability: "
+ response.getBody());
}
return Long.parseLong(obj.getJSONObject("data").getString("response"));
}
}
Loading