diff --git a/src/main/java/com/iemr/common/identity/repo/BenRegIdMappingRepo.java b/src/main/java/com/iemr/common/identity/repo/BenRegIdMappingRepo.java index 3875211b..866b2137 100644 --- a/src/main/java/com/iemr/common/identity/repo/BenRegIdMappingRepo.java +++ b/src/main/java/com/iemr/common/identity/repo/BenRegIdMappingRepo.java @@ -75,4 +75,13 @@ MBeneficiaryregidmapping getWithVanSerialNoVanID(@Param("vanSerialNo") BigIntege List findTop10000ByProvisionedAndReserved(Boolean isProvisioned,Boolean isReserved); + /** + * Atomically selects and locks the next available registration ID row. + * SKIP LOCKED ensures concurrent servers each get a distinct row without blocking each other, + * eliminating duplicate BenRegId assignments when multiple app instances share the same database. + */ + @Transactional + @Query(value = "SELECT * FROM m_beneficiaryregidmapping WHERE Provisioned = false AND Reserved = false ORDER BY BenRegId ASC LIMIT 1 FOR UPDATE SKIP LOCKED", nativeQuery = true) + MBeneficiaryregidmapping findAndLockNextAvailable(); + } diff --git a/src/main/java/com/iemr/common/identity/service/BenRegIdClaimService.java b/src/main/java/com/iemr/common/identity/service/BenRegIdClaimService.java new file mode 100644 index 00000000..ba2c32eb --- /dev/null +++ b/src/main/java/com/iemr/common/identity/service/BenRegIdClaimService.java @@ -0,0 +1,89 @@ +/* +* 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.common.identity.service; + +import java.sql.Timestamp; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import com.iemr.common.identity.domain.MBeneficiaryregidmapping; +import com.iemr.common.identity.repo.BenRegIdMappingRepo; + +/** + * Handles atomic beneficiary registration ID claiming. + * + * Uses SELECT ... FOR UPDATE SKIP LOCKED so that each application instance + * obtains a distinct, exclusive row. Multiple servers sharing the same database + * will never receive the same BenRegId, preventing + * SQLIntegrityConstraintViolationException duplicate-key errors that occurred + * with the previous in-memory ArrayDeque queue approach. + * + * REQUIRES_NEW propagation ensures the SELECT + UPDATE happens in its own + * short-lived transaction, releasing the row lock immediately after the ID is + * marked reserved — keeping lock contention to a minimum. + */ +@Service +public class BenRegIdClaimService { + + private static final Logger logger = LoggerFactory.getLogger(BenRegIdClaimService.class); + + @Autowired + private BenRegIdMappingRepo regIdRepo; + + /** + * Atomically claims the next available registration ID. + * + *
    + *
  1. Opens a brand-new transaction (REQUIRES_NEW).
  2. + *
  3. Executes SELECT … FOR UPDATE SKIP LOCKED to lock exactly one row. + * Concurrent callers on other servers/threads skip the locked row and + * get the next one — no two callers ever see the same row.
  4. + *
  5. Marks the row {@code reserved = true} and flushes it within the same + * transaction so the change is visible to other connections the moment + * this method returns.
  6. + *
+ * + * @return the reserved {@link MBeneficiaryregidmapping} with {@code reserved=true} + * @throws IllegalStateException if the ID pool is exhausted + */ + @Transactional(propagation = Propagation.REQUIRES_NEW) + public MBeneficiaryregidmapping claimNextAvailableRegId() { + MBeneficiaryregidmapping regMap = regIdRepo.findAndLockNextAvailable(); + if (regMap == null) { + throw new IllegalStateException( + "No available registration IDs in the pool. " + + "Please contact the system administrator to import more IDs."); + } + if (regMap.getCreatedDate() == null) { + regMap.setCreatedDate(new Timestamp(System.currentTimeMillis())); + } + regMap.setReserved(true); + regMap = regIdRepo.save(regMap); + logger.info("BenRegIdClaimService: claimed BenRegId={}", regMap.getBenRegId()); + return regMap; + } +} diff --git a/src/main/java/com/iemr/common/identity/service/IdentityService.java b/src/main/java/com/iemr/common/identity/service/IdentityService.java index a6655b78..8921986e 100644 --- a/src/main/java/com/iemr/common/identity/service/IdentityService.java +++ b/src/main/java/com/iemr/common/identity/service/IdentityService.java @@ -25,7 +25,6 @@ import java.math.BigInteger; import java.sql.Timestamp; import java.text.SimpleDateFormat; -import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; @@ -154,6 +153,8 @@ private JdbcTemplate getJdbcTemplate() { @Autowired BenRegIdMappingRepo regIdRepo; @Autowired + private BenRegIdClaimService benRegIdClaimService; + @Autowired BenServiceMappingRepo serviceMapRepo; @Autowired MBeneficiaryAccountRepo accountRepo; @@ -1347,34 +1348,13 @@ private MBeneficiarydetail convertIdentityEditDTOToMBeneficiarydetail(IdentityEd * @param identity * @return */ - ArrayDeque queue = new ArrayDeque<>(); - public BeneficiaryCreateResp createIdentity(IdentityDTO identity) { logger.info("IdentityService.createIdentity - start"); - List list = null; - MBeneficiaryregidmapping regMap = null; - synchronized (queue) { - if (queue.isEmpty()) { - logger.info("fetching 10000 rows"); - list = regIdRepo.findTop10000ByProvisionedAndReserved(false, false); - logger.info("Adding SynchronousQueue start-- "); - for (MBeneficiaryregidmapping map : list) { - queue.add(map); - } - logger.info("Adding SynchronousQueue end-- "); - } - regMap = queue.removeFirst(); - } - regMap.setReserved(true); - if (regMap.getCreatedDate() == null) { - SimpleDateFormat sdf = new SimpleDateFormat(CREATED_DATE_FORMAT); - String dateToStoreInDataBase = sdf.format(new Date()); - Timestamp ts = Timestamp.valueOf(dateToStoreInDataBase); - regMap.setCreatedDate(ts); - } - - regIdRepo.save(regMap); + // Atomically claim the next available ID using SELECT … FOR UPDATE SKIP LOCKED. + // This is safe across multiple app servers sharing the same database — each server + // locks and reserves a distinct row, so duplicate BenRegId inserts cannot occur. + MBeneficiaryregidmapping regMap = benRegIdClaimService.claimNextAvailableRegId(); regMap.setProvisioned(true);