diff --git a/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/config/CommonConfig.java b/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/config/CommonConfig.java index 02590829867..e90cd809310 100644 --- a/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/config/CommonConfig.java +++ b/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/config/CommonConfig.java @@ -3,10 +3,12 @@ import jakarta.servlet.Filter; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.security.concurrent.DelegatingSecurityContextExecutorService; import org.springframework.web.filter.CommonsRequestLoggingFilter; import io.mosip.kernel.core.masterdata.util.model.Node; @@ -17,6 +19,9 @@ import io.mosip.kernel.masterdata.utils.AuditUtil; import io.mosip.kernel.masterdata.utils.DefaultSort; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + /** * Config class with beans for modelmapper and request logging * @@ -29,6 +34,12 @@ @EnableAspectJAutoProxy public class CommonConfig { + @Value("${mosip.kernel.masterdata.enrichment.executor.min-fixed-thread-pool:2}") + private int minFixedThreadPool; + + @Value("${mosip.kernel.masterdata.enrichment.executor.max-fixed-thread-pool:8}") + private int maxFixedThreadPool; + /** * Produce Request Logging bean * @@ -77,5 +88,16 @@ public DefaultSort defaultSort() { public AuditUtil auditUtil() { return new AuditUtil(); } + + @Bean(destroyMethod = "shutdown") + public ExecutorService enrichmentExecutor() { + final ExecutorService executorService = Executors.newFixedThreadPool( + Math.min( + Math.max(this.minFixedThreadPool, Runtime.getRuntime().availableProcessors()), + this.maxFixedThreadPool + ) + ); + return new DelegatingSecurityContextExecutorService(executorService); + } } diff --git a/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/repository/ZoneRepository.java b/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/repository/ZoneRepository.java index 37c0ee241bb..b2570d8279a 100644 --- a/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/repository/ZoneRepository.java +++ b/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/repository/ZoneRepository.java @@ -25,6 +25,9 @@ public interface ZoneRepository extends BaseRepository findZonesByCodesAndLangCodeNonDeletedAndIsActive(List zonesCodes, String langCode); + @Query("FROM Zone zu WHERE zu.code in (:zoneId) AND (zu.isDeleted IS NULL OR zu.isDeleted = false) ") public List findListZonesFromZone(List zoneId); diff --git a/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/service/ZoneService.java b/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/service/ZoneService.java index 8188c076393..29535daef99 100644 --- a/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/service/ZoneService.java +++ b/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/service/ZoneService.java @@ -43,6 +43,8 @@ public interface ZoneService { public ZoneNameResponseDto getZone(String zoneCode, String langCode); + public List getZonesByCodesAndLanguage(List zonesCodes, String langCode); + public FilterResponseCodeDto zoneFilterValues(FilterValueDto filterValueDto); public List getZoneListBasedonZoneName(String zoneName); diff --git a/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/service/impl/UserDetailsServiceImpl.java b/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/service/impl/UserDetailsServiceImpl.java index f60ec0a499e..2442a5536f4 100644 --- a/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/service/impl/UserDetailsServiceImpl.java +++ b/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/service/impl/UserDetailsServiceImpl.java @@ -4,6 +4,9 @@ import java.time.LocalDateTime; import java.time.ZoneId; import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import io.mosip.kernel.masterdata.exception.RequestException; @@ -149,6 +152,14 @@ public class UserDetailsServiceImpl implements UserDetailsService { @Autowired ZoneUserRepository zoneUserRepository; + @Autowired + private ExecutorService enrichmentExecutor; + + @Value("${mosip.kernel.masterdata.enrichment.executor.timeout-seconds:30}") + private int enrichmentExecutorTimeoutSeconds; + + + @Override public UserDetailsGetExtnDto getUser(String id) { UserDetails ud = userDetailsRepository.findByIdAndIsDeletedFalseorIsDeletedIsNull(id); @@ -685,6 +696,37 @@ public PageResponseDto searchUserCenterMappingDetails( new OptionalFilter[] { zoneOptionalFilter }); if (page.getContent() != null && !page.getContent().isEmpty()) { zoneUserSearchDetails = MapperUtils.mapAll(page.getContent(), ZoneUserExtnDto.class); + // recover Users Ids for batch user's names search + final List usersIds = zoneUserSearchDetails.stream().map(ZoneUserExtnDto::getUserId).toList(); + // recover Users ZonesCodes for batch zones search + final List usersZonesCodes = zoneUserSearchDetails.stream().map( + ZoneUserExtnDto::getZoneCode + ).filter(org.springframework.util.StringUtils::hasText).toList(); + // Launch both calls of batch user's names search and zone's names search in parallel + CompletableFuture> usersNamesByIdsFuture = CompletableFuture.supplyAsync( + () -> this.getUsersNames(usersIds), + this.enrichmentExecutor + ); + CompletableFuture> recoveredZonesFuture = CompletableFuture.supplyAsync( + () -> this.zoneUtils.getZonesByCodesAndLanguage(usersZonesCodes, searchDto.getLanguageCode()), + this.enrichmentExecutor + ); + // Retrieve results (blocks here until available) + final Map usersNamesByIds = usersNamesByIdsFuture.orTimeout( + this.enrichmentExecutorTimeoutSeconds, + TimeUnit.SECONDS + ).exceptionally(ex -> { + logger.error("Failed to fetch user names in batch", ex); + return Collections.emptyMap(); + }).join(); + final List recoveredZones = recoveredZonesFuture.orTimeout( + this.enrichmentExecutorTimeoutSeconds, + TimeUnit.SECONDS + ).exceptionally(ex -> { + logger.error("Failed to fetch zones in batch", ex); + return Collections.emptyList(); + }).join(); + // Complete page result construction pageDto = PageUtils.pageResponse(page); zoneUserSearchDetails.forEach(z -> { ZoneUserSearchDto dto = new ZoneUserSearchDto(); @@ -698,12 +740,14 @@ public PageResponseDto searchUserCenterMappingDetails( dto.setUserId(z.getUserId()); dto.setUpdatedDateTime(z.getUpdatedDateTime()); dto.setUpdatedBy(z.getUpdatedBy()); - String username = getUserName(z.getUserId()); + String username = usersNamesByIds.getOrDefault(z.getUserId(), null); dto.setUserName(username == null || username.isBlank() ? z.getUserId() : String.format(USERNAME_FORMAT, z.getUserId(), username)); if (null != z.getZoneCode()) { - Zone zn = zoneUtils.getZoneBasedOnZoneCodeLanguage(z.getZoneCode(), searchDto.getLanguageCode()); + Zone zn = recoveredZones.stream().filter( + rzn -> z.getZoneCode().equals(rzn.getCode()) + ).findFirst().orElse(null); dto.setZoneName(null != zn ? zn.getName() : null); } else dto.setZoneName(null); @@ -748,6 +792,48 @@ private String getUserName(String userId) { return null; } + + private Map getUsersNames(List usersIds) { + if(usersIds == null || usersIds.isEmpty()) { + return Collections.emptyMap(); + } + final HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + final UriComponentsBuilder uribuilder = UriComponentsBuilder.fromUriString(userDetails + "/admin"); + final List userDetails = new ArrayList<>(usersIds); + final RequestWrapper>> requestWrapper = new RequestWrapper<>(); + final Map> requestEntry = new HashMap<>(); + requestEntry.put("userDetails", userDetails); + requestWrapper.setRequest(requestEntry); + final HttpEntity>>> httpEntity = new HttpEntity<>( + requestWrapper, + headers + ); + ResponseEntity response = restTemplate.exchange( + uribuilder.toUriString(), + HttpMethod.POST, + httpEntity, + String.class + ); + try { + final ObjectMapper mapper = new ObjectMapper(); + final Map responseBody = mapper.readValue(response.getBody(), Map.class); + List> usersListResponse = ( + (Map>>) responseBody.get("response") + ).get("mosipUserDtoList"); + if (usersListResponse.isEmpty()) { + return Collections.emptyMap(); + } else { + return usersListResponse.stream().collect(Collectors.toMap( + entry -> entry.get("userId"), + entry -> entry.get("name") + )); + } + } catch (Exception e) { + logger.error("failed to get users names from authmanager", e); + } + return Collections.emptyMap(); + } private List dtoMapper(List zoneUserSearchDtos, String languageCode) { List userCenterMappingExtnDtos=new ArrayList(); diff --git a/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/service/impl/ZoneServiceImpl.java b/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/service/impl/ZoneServiceImpl.java index b146e1b958b..1b436538d73 100644 --- a/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/service/impl/ZoneServiceImpl.java +++ b/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/service/impl/ZoneServiceImpl.java @@ -294,6 +294,30 @@ public ZoneNameResponseDto getZone(String zoneCode, String langCode) { return zoneNameResponseDto; } + @Override + public List getZonesByCodesAndLanguage(List zonesCodes, String langCode) { + final List zonesResponse = new ArrayList<>(); + List zones = null; + try { + final String effectiveLangCode = ((langCode == null) ? languageUtils.getDefaultLanguage() : langCode); + zones = this.zoneRepository.findZonesByCodesAndLangCodeNonDeletedAndIsActive(zonesCodes, effectiveLangCode); + } catch (DataAccessException | DataAccessLayerException exception) { + throw new MasterDataServiceException(ZoneErrorCode.INTERNAL_SERVER_ERROR.getErrorCode(), + ZoneErrorCode.INTERNAL_SERVER_ERROR.getErrorMessage()); + } + if (zones == null || zones.isEmpty()) { + throw new DataNotFoundException(ZoneErrorCode.ZONE_ENTITY_NOT_FOUND.getErrorCode(), + ZoneErrorCode.ZONE_ENTITY_NOT_FOUND.getErrorMessage()); + } + zones.forEach(zone -> { + ZoneNameResponseDto zoneNameResponseDto = new ZoneNameResponseDto(); + zoneNameResponseDto.setZoneCode(zone.getCode()); + zoneNameResponseDto.setZoneName(zone.getName()); + zonesResponse.add(zoneNameResponseDto); + }); + return zonesResponse; + } + @Override public FilterResponseCodeDto zoneFilterValues(FilterValueDto filterValueDto) { // TODO Auto-generated method stub diff --git a/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/service/impl/ZoneUserServiceImpl.java b/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/service/impl/ZoneUserServiceImpl.java index b91dde392fb..648ebb06a72 100644 --- a/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/service/impl/ZoneUserServiceImpl.java +++ b/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/service/impl/ZoneUserServiceImpl.java @@ -4,6 +4,9 @@ import java.time.ZoneId; import java.time.format.DateTimeParseException; import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import io.mosip.kernel.masterdata.dto.request.SearchFilter; @@ -22,6 +25,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; @@ -95,6 +99,13 @@ public class ZoneUserServiceImpl implements ZoneUserService { @Autowired private AuditUtil auditUtil; + @Autowired + private ExecutorService enrichmentExecutor; + + @Value("${mosip.kernel.masterdata.enrichment.executor.timeout-seconds:30}") + private int enrichmentExecutorTimeoutSeconds; + + private ObjectMapper mapper = new ObjectMapper(); @Override @@ -374,6 +385,37 @@ public PageResponseDto searchZoneUserMapping(SearchDtoWithout Page page = masterDataSearchHelper.searchMasterdataWithoutLangCode(ZoneUser.class, searchDto, new OptionalFilter[] { zoneOptionalFilter }); if (page.getContent() != null && !page.getContent().isEmpty()) { zoneUserSearchDetails = MapperUtils.mapAll(page.getContent(), ZoneUserExtnDto.class); + // recover Users Ids for batch user's names search + final List usersIds = zoneUserSearchDetails.stream().map(ZoneUserExtnDto::getUserId).toList(); + // recover Users ZonesCodes for batch zones search + final List usersZonesCodes = zoneUserSearchDetails.stream().map( + ZoneUserExtnDto::getZoneCode + ).filter(StringUtils::hasText).toList(); + // Launch both calls of batch user's names search and zone's names search in parallel + CompletableFuture> usersNamesByIdsFuture = CompletableFuture.supplyAsync( + () -> this.getUsersNames(usersIds), + this.enrichmentExecutor + ); + CompletableFuture> zoneNamesByCodesFuture = CompletableFuture.supplyAsync( + () -> this.zoneservice.getZonesByCodesAndLanguage(usersZonesCodes, searchDto.getLanguageCode()), + this.enrichmentExecutor + ); + // Retrieve results (blocks here until available) + final Map usersNamesByIds = usersNamesByIdsFuture.orTimeout( + this.enrichmentExecutorTimeoutSeconds, + TimeUnit.SECONDS + ).exceptionally(ex -> { + logger.error("Failed to fetch user names in batch", ex); + return Collections.emptyMap(); + }).join(); + final List zoneNamesByCodes = zoneNamesByCodesFuture.orTimeout( + this.enrichmentExecutorTimeoutSeconds, + TimeUnit.SECONDS + ).exceptionally(ex -> { + logger.error("Failed to fetch zone names in batch", ex); + return Collections.emptyList(); + }).join(); + // Complete page result construction pageDto = PageUtils.pageResponse(page); zoneUserSearchDetails.forEach(z -> { ZoneUserSearchDto dto = new ZoneUserSearchDto(); @@ -387,12 +429,13 @@ public PageResponseDto searchZoneUserMapping(SearchDtoWithout dto.setUserId(z.getUserId()); dto.setUpdatedDateTime(z.getUpdatedDateTime()); dto.setUpdatedBy(z.getUpdatedBy()); - String username = getUserName(z.getUserId()); + String username = usersNamesByIds.getOrDefault(z.getUserId(), null); dto.setUserName(username == null || username.isBlank() ? z.getUserId() : String.format(USERNAME_FORMAT, z.getUserId(), username)); - if (null != z.getZoneCode()) { - ZoneNameResponseDto zn = zoneservice.getZone(z.getZoneCode(),searchDto.getLanguageCode()); + ZoneNameResponseDto zn = zoneNamesByCodes.stream().filter( + znr -> znr.getZoneCode().equals(z.getZoneCode()) + ).findFirst().orElse(null); dto.setZoneName(null != zn ? zn.getZoneName() : null); } else dto.setZoneName(null); @@ -440,6 +483,47 @@ private String getUserName(String userId) { } + private Map getUsersNames(List usersIds) { + if (usersIds == null || usersIds.isEmpty()) { + return Collections.emptyMap(); + } + final HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + final UriComponentsBuilder uribuilder = UriComponentsBuilder.fromUriString(userDetails + "/admin"); + final List userDetails = new ArrayList<>(usersIds); + final RequestWrapper>> requestWrapper = new RequestWrapper<>(); + final Map> requestEntry = new HashMap<>(); + requestEntry.put("userDetails", userDetails); + requestWrapper.setRequest(requestEntry); + final HttpEntity>>> httpEntity = new HttpEntity<>( + requestWrapper, + headers + ); + ResponseEntity response = restTemplate.exchange( + uribuilder.toUriString(), + HttpMethod.POST, + httpEntity, + String.class + ); + try { + final Map responseBody = mapper.readValue(response.getBody(), Map.class); + List> usersListResponse = ( + (Map>>) responseBody.get("response") + ).get("mosipUserDtoList"); + if (usersListResponse.isEmpty()) { + return Collections.emptyMap(); + } else { + return usersListResponse.stream().collect(Collectors.toMap( + entry -> entry.get("userId"), + entry -> entry.get("name") + )); + } + } catch (Exception e) { + logger.error("failed to get users names from authmanager", e); + } + return Collections.emptyMap(); + } + private String getUserDetailsBasedonUserName(String userName) { HttpHeaders h = new HttpHeaders(); h.setContentType(MediaType.APPLICATION_JSON); diff --git a/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/utils/ZoneUtils.java b/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/utils/ZoneUtils.java index 74f1ef8fbe4..4cdbf2585ae 100644 --- a/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/utils/ZoneUtils.java +++ b/admin/kernel-masterdata-service/src/main/java/io/mosip/kernel/masterdata/utils/ZoneUtils.java @@ -387,6 +387,9 @@ public List getChildZoneList(List zoneIds, String zoneCode, String public Zone getZoneBasedOnZoneCodeLanguage(String zoneCode, String langCode) { return zoneRepository.findZoneByCodeAndLangCodeNonDeleted(zoneCode, langCode); } + public List getZonesByCodesAndLanguage(List zonesCodes, String langCode) { + return zoneRepository.findZonesByCodesAndLangCodeNonDeletedAndIsActive(zonesCodes, langCode); + } public List getZoneListBasedonZoneName(String zoneName) { return zoneRepository.findListZonesFromZoneName(zoneName.toLowerCase()); } diff --git a/admin/kernel-masterdata-service/src/main/resources/application-local1.properties b/admin/kernel-masterdata-service/src/main/resources/application-local1.properties index 492bcb453c9..1976b1a235c 100644 --- a/admin/kernel-masterdata-service/src/main/resources/application-local1.properties +++ b/admin/kernel-masterdata-service/src/main/resources/application-local1.properties @@ -403,3 +403,8 @@ mosip.kernel.masterdata.name.validate.regex=[^A-Za-z] #Spring AntPathMatcher for flexible path pattern support spring.mvc.pathmatch.matching-strategy=ANT_PATH_MATCHER + + +mosip.kernel.masterdata.enrichment.executor.timeout-seconds=30 +mosip.kernel.masterdata.enrichment.executor.min-fixed-thread-pool=2 +mosip.kernel.masterdata.enrichment.executor.max-fixed-thread-pool=8 \ No newline at end of file