DCM Write optimization using batching 85/140885/4
authorToineSiebelink <toine.siebelink@est.tech>
Thu, 8 May 2025 16:24:39 +0000 (17:24 +0100)
committerToineSiebelink <toine.siebelink@est.tech>
Thu, 15 May 2025 10:02:34 +0000 (11:02 +0100)
- Add method to AlternateIdMatcher to match a batch of paths and return the relevant yang model cm handles
- Extended inventory service to load cm handles without properties (i.e. descendants)
- Switch from NcmpServiceCmHandles to YangModel Cm Handles
- Preload required cm handles (without properties) using batching
- Removed (now) unused methods
- Improved test coverage on some legacy but related functionality

Issue-ID: CPS-2743
Change-Id: Ie80fdd4c12b72fc72ab1a87aa463ec0e6b664e3a
Signed-off-by: ToineSiebelink <toine.siebelink@est.tech>
15 files changed:
cps-ncmp-service/pom.xml
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/datajobs/WriteRequestExaminer.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/CmHandleRegistrationService.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/InventoryPersistence.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/InventoryPersistenceImpl.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/ParameterizedCmHandleQueryService.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/ParameterizedCmHandleQueryServiceImpl.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/utils/AlternateIdMatcher.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/datajobs/WriteRequestExaminerSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/CmHandleRegistrationServiceSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/ParameterizedCmHandleQueryServiceSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/utils/AlternateIdMatcherSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy
integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/AlternateIdPerfTest.groovy
integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/WriteDataJobPerfTest.groovy

index fbc30eb..78ef226 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
   ============LICENSE_START=======================================================
-  Copyright (C) 2021-2024 Nordix Foundation
+  Copyright (C) 2021-2025 OpenInfra Foundation Europe. All rights reserved.
   Modifications Copyright (C) 2021 Pantheon.tech
   Modifications Copyright (C) 2022 Bell Canada
   ================================================================================
index 7d08d91..9cb3517 100644 (file)
 package org.onap.cps.ncmp.impl.datajobs;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.ncmp.api.datajobs.models.DataJobWriteRequest;
 import org.onap.cps.ncmp.api.datajobs.models.DmiWriteOperation;
 import org.onap.cps.ncmp.api.datajobs.models.ProducerKey;
 import org.onap.cps.ncmp.api.datajobs.models.WriteOperation;
-import org.onap.cps.ncmp.api.inventory.models.NcmpServiceCmHandle;
 import org.onap.cps.ncmp.impl.dmi.DmiServiceNameResolver;
 import org.onap.cps.ncmp.impl.inventory.InventoryPersistence;
+import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle;
 import org.onap.cps.ncmp.impl.models.RequiredDmiService;
 import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher;
-import org.onap.cps.ncmp.impl.utils.YangDataConverter;
 import org.springframework.stereotype.Service;
 
 @Slf4j
 @Service
 @RequiredArgsConstructor
 public class WriteRequestExaminer {
+
     private static final String PATH_SEPARATOR = "/";
 
     private final AlternateIdMatcher alternateIdMatcher;
@@ -56,24 +58,28 @@ public class WriteRequestExaminer {
      */
     public Map<ProducerKey, List<DmiWriteOperation>> splitDmiWriteOperationsFromRequest(
             final String dataJobId, final DataJobWriteRequest dataJobWriteRequest) {
+        final Map<String, YangModelCmHandle> cmHandlePerAlternateId = preloadCmHandles(dataJobWriteRequest);
         final Map<ProducerKey, List<DmiWriteOperation>> dmiWriteOperationsPerProducerKey = new HashMap<>();
         for (final WriteOperation writeOperation : dataJobWriteRequest.data()) {
-            examineWriteOperation(dataJobId, dmiWriteOperationsPerProducerKey, writeOperation);
+            examineWriteOperation(dataJobId,
+                                  dmiWriteOperationsPerProducerKey,
+                                  cmHandlePerAlternateId,
+                                  writeOperation);
         }
         return dmiWriteOperationsPerProducerKey;
     }
 
     private void examineWriteOperation(final String dataJobId,
                                        final Map<ProducerKey, List<DmiWriteOperation>> dmiWriteOperationsPerProducerKey,
+                                       final Map<String, YangModelCmHandle> cmHandlePerAlternateId,
                                        final WriteOperation writeOperation) {
         log.debug("data job id for write operation is: {}", dataJobId);
-        final String cmHandleId = alternateIdMatcher
-                .getCmHandleIdByLongestMatchingAlternateId(writeOperation.path(), PATH_SEPARATOR);
-        final NcmpServiceCmHandle ncmpServiceCmHandle = YangDataConverter.toNcmpServiceCmHandle(
-                inventoryPersistence.getYangModelCmHandle(cmHandleId));
-        final DmiWriteOperation dmiWriteOperation = createDmiWriteOperation(writeOperation, ncmpServiceCmHandle);
+        final YangModelCmHandle yangModelCmHandle = alternateIdMatcher
+                .getCmHandleByLongestMatchingAlternateId(writeOperation.path(), PATH_SEPARATOR, cmHandlePerAlternateId);
+
+        final DmiWriteOperation dmiWriteOperation = createDmiWriteOperation(writeOperation, yangModelCmHandle);
 
-        final ProducerKey producerKey = createProducerKey(ncmpServiceCmHandle);
+        final ProducerKey producerKey = createProducerKey(yangModelCmHandle);
         final List<DmiWriteOperation> dmiWriteOperations;
         if (dmiWriteOperationsPerProducerKey.containsKey(producerKey)) {
             dmiWriteOperations = dmiWriteOperationsPerProducerKey.get(producerKey);
@@ -84,19 +90,31 @@ public class WriteRequestExaminer {
         dmiWriteOperations.add(dmiWriteOperation);
     }
 
-    private ProducerKey createProducerKey(final NcmpServiceCmHandle ncmpServiceCmHandle) {
+    private Map<String, YangModelCmHandle> preloadCmHandles(final DataJobWriteRequest dataJobWriteRequest) {
+        final Collection<String> uniquePaths
+            = dataJobWriteRequest.data().stream().map(operation -> operation.path()).collect(Collectors.toSet());
+        final Collection<String> cmHandleIds
+            = alternateIdMatcher.getCmHandleIdsByLongestMatchingAlternateIds(uniquePaths, PATH_SEPARATOR);
+        final Collection<YangModelCmHandle> yangModelCmHandles
+            = inventoryPersistence.getYangModelCmHandlesWithoutProperties(cmHandleIds);
+        return yangModelCmHandles.stream()
+            .collect(Collectors.toMap(YangModelCmHandle::getAlternateId, yangModelCmHandle -> yangModelCmHandle));
+    }
+
+    private ProducerKey createProducerKey(final YangModelCmHandle yangModelCmHandle) {
         final String dmiDataServiceName =
-                DmiServiceNameResolver.resolveDmiServiceName(RequiredDmiService.DATA, ncmpServiceCmHandle);
-        return new ProducerKey(dmiDataServiceName, ncmpServiceCmHandle.getDataProducerIdentifier());
+                DmiServiceNameResolver.resolveDmiServiceName(RequiredDmiService.DATA, yangModelCmHandle);
+        return new ProducerKey(dmiDataServiceName, yangModelCmHandle.getDataProducerIdentifier());
     }
 
     private DmiWriteOperation createDmiWriteOperation(final WriteOperation writeOperation,
-                                                      final NcmpServiceCmHandle ncmpServiceCmHandle) {
+                                                      final YangModelCmHandle yangModelCmHandle) {
         return new DmiWriteOperation(
                 writeOperation.path(),
                 writeOperation.op(),
-                ncmpServiceCmHandle.getModuleSetTag(),
+                yangModelCmHandle.getModuleSetTag(),
                 writeOperation.value(),
                 writeOperation.operationId());
     }
+
 }
index 6153fd6..d380668 100644 (file)
@@ -411,7 +411,7 @@ public class CmHandleRegistrationService {
             ncmpServiceCmHandle.getDataProducerIdentifier());
     }
 
-    private void removeAlternateIdsFromCache(final Collection<YangModelCmHandle> yangModelCmHandles) {
+    void removeAlternateIdsFromCache(final Collection<YangModelCmHandle> yangModelCmHandles) {
         for (final YangModelCmHandle yangModelCmHandle: yangModelCmHandles) {
             final String cmHandleId = yangModelCmHandle.getId();
             final String alternateId = yangModelCmHandle.getAlternateId();
index 29eaf2e..ecf27a3 100644 (file)
@@ -65,13 +65,22 @@ public interface InventoryPersistence extends NcmpPersistence {
     YangModelCmHandle getYangModelCmHandle(String cmHandleId);
 
     /**
-     * This method retrieves DMI service name, DMI properties and the state for a given cm handle.
+     * This method retrieves YangModelCmHandles for a given collection of cm handle ids.
      *
      * @param cmHandleIds a list of the ids of the cm handles
      * @return collection of yang model cm handles
      */
     Collection<YangModelCmHandle> getYangModelCmHandles(Collection<String> cmHandleIds);
 
+    /**
+     * This method retrieves YangModelCmHandles for a given collection of cm handle ids.
+     * This variant does not include the state, additional and private properties.
+     *
+     * @param cmHandleIds a list of the ids of the cm handles
+     * @return collection of yang model cm handles
+     */
+    Collection<YangModelCmHandle> getYangModelCmHandlesWithoutProperties(Collection<String> cmHandleIds);
+
     /**
      * Method to return module definitions by cmHandleId.
      *
index 9bbc8b8..e06a46e 100644 (file)
@@ -118,17 +118,12 @@ public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements Inv
 
     @Override
     public Collection<YangModelCmHandle> getYangModelCmHandles(final Collection<String> cmHandleIds) {
-        final Collection<String> validCmHandleIds = new ArrayList<>(cmHandleIds.size());
-        cmHandleIds.forEach(cmHandleId -> {
-            try {
-                cpsValidator.validateNameCharacters(cmHandleId);
-                validCmHandleIds.add(cmHandleId);
-            } catch (final DataValidationException dataValidationException) {
-                log.error("DataValidationException in CmHandleId {} to be ignored",
-                        dataValidationException.getMessage());
-            }
-        });
-        return YangDataConverter.toYangModelCmHandles(getCmHandleDataNodes(validCmHandleIds, INCLUDE_ALL_DESCENDANTS));
+        return getYangModelCmHandlesWithDescendantsOption(cmHandleIds, INCLUDE_ALL_DESCENDANTS);
+    }
+
+    @Override
+    public Collection<YangModelCmHandle> getYangModelCmHandlesWithoutProperties(final Collection<String> cmHandleIds) {
+        return getYangModelCmHandlesWithDescendantsOption(cmHandleIds, OMIT_DESCENDANTS);
     }
 
     @Override
@@ -220,4 +215,22 @@ public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements Inv
                 .filter(StringUtils::isNotBlank)
                 .collect(Collectors.toSet());
     }
+
+    private Collection<YangModelCmHandle> getYangModelCmHandlesWithDescendantsOption(final Collection<String>
+                                                                                         cmHandleIds,
+                                                                                     final FetchDescendantsOption
+                                                                                         fetchDescendantsOption) {
+        final Collection<String> validCmHandleIds = new ArrayList<>(cmHandleIds.size());
+        cmHandleIds.forEach(cmHandleId -> {
+            try {
+                cpsValidator.validateNameCharacters(cmHandleId);
+                validCmHandleIds.add(cmHandleId);
+            } catch (final DataValidationException dataValidationException) {
+                log.error("DataValidationException in CmHandleId {} to be ignored",
+                    dataValidationException.getMessage());
+            }
+        });
+        return YangDataConverter.toYangModelCmHandles(getCmHandleDataNodes(validCmHandleIds, fetchDescendantsOption));
+    }
+
 }
index 5a105b3..64d4689 100644 (file)
@@ -71,13 +71,4 @@ public interface ParameterizedCmHandleQueryService {
      */
     Flux<NcmpServiceCmHandle> queryCmHandles(CmHandleQueryServiceParameters cmHandleQueryServiceParameters);
 
-    /**
-     * Retrieves all {@code NcmpServiceCmHandle} instances without their associated properties.
-     * This method fetches the relevant data nodes from the inventory persistence layer and
-     * converts them into {@code NcmpServiceCmHandle} objects. Only the handles are returned,
-     * without any additional properties.
-     *
-     * @return a collection of {@code NcmpServiceCmHandle} instances without properties.
-     */
-    Collection<NcmpServiceCmHandle> getAllCmHandlesWithoutProperties();
 }
index bafb065..19b6199 100644 (file)
 
 package org.onap.cps.ncmp.impl.inventory;
 
-import static org.onap.cps.api.parameters.FetchDescendantsOption.DIRECT_CHILDREN_ONLY;
 import static org.onap.cps.ncmp.impl.inventory.CmHandleQueryParametersValidator.validateCpsPathConditionProperties;
 import static org.onap.cps.ncmp.impl.inventory.CmHandleQueryParametersValidator.validateModuleNameConditionProperties;
-import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NCMP_DMI_REGISTRY_PARENT;
 import static org.onap.cps.ncmp.impl.inventory.models.CmHandleQueryConditions.HAS_ALL_MODULES;
 import static org.onap.cps.ncmp.impl.inventory.models.CmHandleQueryConditions.HAS_ALL_PROPERTIES;
 import static org.onap.cps.ncmp.impl.inventory.models.CmHandleQueryConditions.WITH_CPS_PATH;
 import static org.onap.cps.ncmp.impl.inventory.models.CmHandleQueryConditions.WITH_TRUST_LEVEL;
-import static org.onap.cps.ncmp.impl.utils.YangDataConverter.toNcmpServiceCmHandle;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -37,11 +34,9 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.function.BiFunction;
-import java.util.stream.Collectors;
 import lombok.RequiredArgsConstructor;
 import org.onap.cps.api.exceptions.DataValidationException;
 import org.onap.cps.api.model.ConditionProperties;
-import org.onap.cps.api.model.DataNode;
 import org.onap.cps.cpspath.parser.PathParsingException;
 import org.onap.cps.ncmp.api.inventory.models.CmHandleQueryServiceParameters;
 import org.onap.cps.ncmp.api.inventory.models.NcmpServiceCmHandle;
@@ -91,16 +86,6 @@ public class ParameterizedCmHandleQueryServiceImpl implements ParameterizedCmHan
         return getNcmpServiceCmHandles(cmHandleIds);
     }
 
-    @Override
-    public Collection<NcmpServiceCmHandle> getAllCmHandlesWithoutProperties() {
-        return toNcmpServiceCmHandles(inventoryPersistence.getDataNode(NCMP_DMI_REGISTRY_PARENT, DIRECT_CHILDREN_ONLY));
-    }
-
-    private Collection<NcmpServiceCmHandle> toNcmpServiceCmHandles(final Collection<DataNode> dataNodes) {
-        final DataNode dataNode = dataNodes.iterator().next();
-        return dataNode.getChildDataNodes().stream().map(this::createNcmpServiceCmHandle).collect(Collectors.toSet());
-    }
-
     private Collection<String> queryCmHandlesByDmiPlugin(
             final CmHandleQueryServiceParameters cmHandleQueryServiceParameters, final boolean outputAlternateId) {
         final Map<String, String> dmiPropertyQueryPairs =
@@ -246,10 +231,6 @@ public class ParameterizedCmHandleQueryServiceImpl implements ParameterizedCmHan
         return ncmpServiceCmHandles;
     }
 
-    private NcmpServiceCmHandle createNcmpServiceCmHandle(final DataNode dataNode) {
-        return toNcmpServiceCmHandle(YangDataConverter.toYangModelCmHandle(dataNode));
-    }
-
     private Collection<String> executeQueries(final CmHandleQueryServiceParameters cmHandleQueryServiceParameters,
                                               final boolean outputAlternateId,
                                               final BiFunction<CmHandleQueryServiceParameters, Boolean,
index bd5210f..e2b39e5 100644 (file)
 package org.onap.cps.ncmp.impl.utils;
 
 import com.hazelcast.map.IMap;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.onap.cps.ncmp.api.exceptions.CmHandleNotFoundException;
 import org.onap.cps.ncmp.exceptions.NoAlternateIdMatchFoundException;
+import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Service;
 
@@ -37,19 +44,20 @@ public class AlternateIdMatcher {
     @Qualifier("cmHandleIdPerAlternateId")
     private final IMap<String, String> cmHandleIdPerAlternateId;
 
+    private static final String URI_PATH_COMPONENT_SEPARATOR = "#";
+
     /**
      * Get cm handle that matches longest alternate id by removing elements
      * (as defined by the separator string) from right to left.
      * If alternate id contains a hash then all elements after that hash are ignored.
      *
      * @param alternateId            alternate ID
-     * @param separator              a string that separates each element from the next.
+     * @param separator              a string that separates each path element from the next.
      * @return ncmp service cm handle
      */
-    public String getCmHandleIdByLongestMatchingAlternateId(
-            final String alternateId, final String separator) {
-        final String[] splitPath = alternateId.split("#", 2);
-        String bestMatch = splitPath[0];
+    public String getCmHandleIdByLongestMatchingAlternateId(final String alternateId, final String separator) {
+        final String[] uriPathComponents = alternateId.split(URI_PATH_COMPONENT_SEPARATOR, 2);
+        String bestMatch = uriPathComponents[0];
         while (StringUtils.isNotEmpty(bestMatch)) {
             final String cmHandleId = cmHandleIdPerAlternateId.get(bestMatch);
             if (cmHandleId != null) {
@@ -60,6 +68,54 @@ public class AlternateIdMatcher {
         throw new NoAlternateIdMatchFoundException(alternateId);
     }
 
+    /**
+     * Get cm handle that matches longest alternate id in the given map.
+     *
+     * @param alternateId            the target alternate id
+     * @param separator              a string that separates each path element from the next.
+     * @param cmHandlePerAlternateId a map of cm handles with the alternate id as key
+     * @return cm handle as a YangModelCmHandle
+     */
+    public YangModelCmHandle getCmHandleByLongestMatchingAlternateId(
+                                                    final String alternateId,
+                                                    final String separator,
+                                                    final Map<String, YangModelCmHandle> cmHandlePerAlternateId) {
+        final String[] splitPathOnHashExtension = alternateId.split("#", 2);
+        String bestMatch = splitPathOnHashExtension[0];
+        while (StringUtils.isNotEmpty(bestMatch)) {
+            final YangModelCmHandle yangModelCmHandle = cmHandlePerAlternateId.get(bestMatch);
+            if (yangModelCmHandle != null) {
+                return yangModelCmHandle;
+            }
+            bestMatch = getParentPath(bestMatch, separator);
+        }
+        throw new NoAlternateIdMatchFoundException(alternateId);
+    }
+
+    /**
+     * Get collection of cm handle ids whose alternate id best (longest) match the given paths.
+     * If alternate id contains a hash then all elements after that hash are ignored.
+     *
+     * @param paths            collection of paths
+     * @param separator        a string that separates each path element from the next.
+     * @return collection of cm handle ids
+     */
+    public Collection<String> getCmHandleIdsByLongestMatchingAlternateIds(final Collection<String> paths,
+                                                                          final String separator) {
+        final Collection<String> cmHandleIds = new ArrayList<>();
+        Set<String> unresolvedPaths = new HashSet<>(paths);
+        while (!unresolvedPaths.isEmpty()) {
+            final Map<String, String> resolvedCmHandleIdPerAlternateId
+                = cmHandleIdPerAlternateId.getAll(unresolvedPaths);
+            cmHandleIds.addAll(resolvedCmHandleIdPerAlternateId.values());
+            unresolvedPaths.removeAll(resolvedCmHandleIdPerAlternateId.keySet());
+            unresolvedPaths = unresolvedPaths.stream().map(p -> getParentPath(p, separator))
+                .filter(StringUtils::isNotEmpty)
+                .collect(Collectors.toSet());
+        }
+        return cmHandleIds;
+    }
+
     /**
      * Get cm handle Id from given cmHandleReference.
      *
index 8f37f6e..75cad77 100644 (file)
@@ -22,7 +22,6 @@ package org.onap.cps.ncmp.impl.datajobs
 
 import org.onap.cps.ncmp.api.datajobs.models.DataJobWriteRequest
 import org.onap.cps.ncmp.api.datajobs.models.WriteOperation
-import org.onap.cps.ncmp.api.inventory.models.NcmpServiceCmHandle
 import org.onap.cps.ncmp.impl.inventory.InventoryPersistence
 import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
 import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher
@@ -36,21 +35,22 @@ class WriteRequestExaminerSpec extends Specification {
 
     def setup() {
         def yangModelCmHandle1 = new YangModelCmHandle(id: 'ch1', dmiServiceName: 'dmiA', dmiProperties: [],
-            publicProperties: [], compositeState: null, moduleSetTag: '', alternateId: '', dataProducerIdentifier: 'p1')
+            publicProperties: [], compositeState: null, moduleSetTag: '', alternateId: 'fdn1', dataProducerIdentifier: 'p1')
         def yangModelCmHandle2 = new YangModelCmHandle(id: 'ch2', dmiServiceName: 'dmiA', dmiProperties: [],
-            publicProperties: [], compositeState: null, moduleSetTag: '', alternateId: '', dataProducerIdentifier: 'p1')
+            publicProperties: [], compositeState: null, moduleSetTag: '', alternateId: 'fdn2', dataProducerIdentifier: 'p1')
         def yangModelCmHandle3 = new YangModelCmHandle(id: 'ch3', dmiServiceName: 'dmiA', dmiProperties: [],
-            publicProperties: [], compositeState: null, moduleSetTag: '', alternateId: '', dataProducerIdentifier: 'p2')
+            publicProperties: [], compositeState: null, moduleSetTag: '', alternateId: 'fdn3', dataProducerIdentifier: 'p2')
         def yangModelCmHandle4 = new YangModelCmHandle(id: 'ch4', dmiServiceName: 'dmiB', dmiProperties: [],
-            publicProperties: [], compositeState: null, moduleSetTag: '', alternateId: '', dataProducerIdentifier: 'p1')
-        mockInventoryPersistence.getYangModelCmHandle('ch1') >> yangModelCmHandle1
-        mockInventoryPersistence.getYangModelCmHandle('ch2') >> yangModelCmHandle2
-        mockInventoryPersistence.getYangModelCmHandle('ch3') >> yangModelCmHandle3
-        mockInventoryPersistence.getYangModelCmHandle('ch4') >> yangModelCmHandle4
-        mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('fdn1', '/') >> 'ch1'
-        mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('fdn2', '/') >> 'ch2'
-        mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('fdn3', '/') >> 'ch3'
-        mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('fdn4', '/') >> 'ch4'
+            publicProperties: [], compositeState: null, moduleSetTag: '', alternateId: 'fdn4', dataProducerIdentifier: 'p1')
+        mockAlternateIdMatcher.getCmHandleIdsByLongestMatchingAlternateIds(_, '/') > ['ch1','ch2','ch3','ch4']
+
+        mockAlternateIdMatcher.getCmHandleByLongestMatchingAlternateId('fdn1', '/', _) >> yangModelCmHandle1
+        mockAlternateIdMatcher.getCmHandleByLongestMatchingAlternateId('fdn2', '/', _) >> yangModelCmHandle2
+        mockAlternateIdMatcher.getCmHandleByLongestMatchingAlternateId('fdn3', '/', _) >> yangModelCmHandle3
+        mockAlternateIdMatcher.getCmHandleByLongestMatchingAlternateId('fdn4', '/', _) >> yangModelCmHandle4
+
+        mockInventoryPersistence.getYangModelCmHandlesWithoutProperties(_) >>
+            [ yangModelCmHandle1, yangModelCmHandle2, yangModelCmHandle3, yangModelCmHandle4 ]
     }
 
     def 'Create a map of dmi write requests per producer key with #scenario.'() {
@@ -93,9 +93,9 @@ class WriteRequestExaminerSpec extends Specification {
 
     def 'Validate the creation of a ProducerKey with correct dmiservicename.'() {
         given: 'yangModelCmHandles with service name: "#dmiServiceName" and data service name: "#dataServiceName"'
-            def ncmpServiceCmHandle = new NcmpServiceCmHandle(dmiServiceName: dmiServiceName, dmiDataServiceName: dataServiceName, dataProducerIdentifier: 'dpi1')
+            def yangModelCmHandle = new YangModelCmHandle(dmiServiceName: dmiServiceName, dmiDataServiceName: dataServiceName, dataProducerIdentifier: 'dpi1')
         when: 'the ProducerKey is created'
-            def result = objectUnderTest.createProducerKey(ncmpServiceCmHandle).toString()
+            def result = objectUnderTest.createProducerKey(yangModelCmHandle).toString()
         then: 'we get the ProducerKey with the correct service name'
             assert result == expectedProducerKey
         where: 'the following services are registered'
index f99fe2d..5e4e738 100644 (file)
@@ -433,4 +433,22 @@ class CmHandleRegistrationServiceSpec extends Specification {
             0 * mockInventoryPersistence.saveCmHandleState(_, _)
     }
 
+    def 'Adding and removing id - alternate id mappings with #scenario.'() {
+        given: 'a yang model cm handle with #scenario'
+            def yangModelCmHandle = new YangModelCmHandle(id:'ch-1', alternateId: alternateId)
+        when: 'it is added to the cache'
+            objectUnderTest.addAlternateIdsToCache([yangModelCmHandle])
+        then: 'it is added to the cache with the expected key'
+            1 * mockCmHandleIdPerAlternateId.putAll([(expectedKey):'ch-1'])
+        when: 'it is removed from the cache'
+            objectUnderTest.removeAlternateIdsFromCache([yangModelCmHandle])
+        then: 'the correct key is deleted from the cache'
+            1 * mockCmHandleIdPerAlternateId.delete(expectedKey)
+        where: 'the following alternate ids are used'
+            scenario             | alternateId || expectedKey
+            'with alternate id'  | 'alt-1'     || 'alt-1'
+            'blank alternate id' | ''          || 'ch-1'
+            'no alternate id'    | null        || 'ch-1'
+    }
+
 }
index 594d7fb..5d9a05c 100644 (file)
 
 package org.onap.cps.ncmp.impl.inventory
 
-import org.onap.cps.cpspath.parser.PathParsingException
-import org.onap.cps.ncmp.api.inventory.models.CmHandleQueryServiceParameters
-import org.onap.cps.ncmp.api.inventory.models.NcmpServiceCmHandle
-import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
-import org.onap.cps.api.parameters.FetchDescendantsOption
 import org.onap.cps.api.exceptions.DataInUseException
 import org.onap.cps.api.exceptions.DataValidationException
 import org.onap.cps.api.model.ConditionProperties
 import org.onap.cps.api.model.DataNode
+import org.onap.cps.cpspath.parser.PathParsingException
+import org.onap.cps.ncmp.api.inventory.models.CmHandleQueryServiceParameters
+import org.onap.cps.ncmp.api.inventory.models.NcmpServiceCmHandle
+import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
 import org.onap.cps.ncmp.impl.inventory.trustlevel.TrustLevelManager
 import spock.lang.Specification
 
index 84a0507..74d047e 100644 (file)
@@ -23,6 +23,7 @@ package org.onap.cps.ncmp.impl.utils
 import com.hazelcast.map.IMap
 import org.onap.cps.ncmp.api.exceptions.CmHandleNotFoundException
 import org.onap.cps.ncmp.exceptions.NoAlternateIdMatchFoundException
+import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
 import spock.lang.Specification
 
 class AlternateIdMatcherSpec extends Specification {
@@ -31,21 +32,34 @@ class AlternateIdMatcherSpec extends Specification {
 
     def objectUnderTest = new AlternateIdMatcher(mockCmHandleIdPerAlternateId)
 
-    def 'Finding longest alternate id matches.'() {
-        given:
+    def testYangModelCmHandle = new YangModelCmHandle(id:1)
+
+    def 'Finding longest alternate id matches, scenario: #scenario.'() {
+        given: ' a match for alternate id "/a/b"'
             mockCmHandleIdPerAlternateId.get('/a/b') >> 'ch1'
-        expect: 'querying for alternate id a matching result found'
+        expect: 'a match has been found'
             assert objectUnderTest.getCmHandleIdByLongestMatchingAlternateId(targetAlternateId, '/') != null
-        where: 'the following parameters are used'
-            scenario                             | targetAlternateId
-            'exact match'                        | '/a/b'
-            'parent match'                       | '/a/b/c'
-            'grand parent match'                 | '/a/b/c/d'
-            'trailing separator match'           | '/a/b/'
-            'trailing hash'                      | '/a/b#q'
-            'trailing hash parent match'         | '/a/b/c#q'
-            'trailing hash grand parent match'   | '/a/b/c/d#q'
-            'trailing separator then hash match' | '/a/b/#q'
+        where: 'the following alternate ids are used'
+            scenario                                                   | targetAlternateId
+            'exact match'                                              | '/a/b'
+            'parent match'                                             | '/a/b/c'
+            'grand parent match'                                       | '/a/b/c/d'
+            'trailing separator match'                                 | '/a/b/'
+            'with attribute path component and exact match'            | '/a/b#q'
+            'with attribute path component and parent match'           | '/a/b/c#q'
+            'with attribute path component and grand parent match'     | '/a/b/c/d#q'
+            'with attribute path component and additional slash match' | '/a/b/#q'
+    }
+
+    def 'Finding longest alternate id matches for a batch.'() {
+        given: 'a batch of alternate ids'
+            def aBatchOfAlternateIds = ['content does','not matter']
+        and: 'the cached map returns a map of some matches'
+            mockCmHandleIdPerAlternateId.getAll(_) >> [fdn1:'ch1', fdn2:'ch2']
+        when: 'getting the matches alternate ids for the batch'
+            def result = objectUnderTest.getCmHandleIdsByLongestMatchingAlternateIds(aBatchOfAlternateIds, '/')
+        then: 'the result are the ids (values) from the cached map'
+            assert result == ['ch1', 'ch2']
     }
 
     def 'Attempt to find longest alternate id match without any matches.'() {
@@ -61,6 +75,43 @@ class AlternateIdMatcherSpec extends Specification {
             'no match for parent only' | '/a'
             'no match for other child' | '/a/c'
             'no match at all'          | '/x/y'
+            'no root'                  | 'c'
+    }
+
+    def 'Find cm handle with longest match using pre-loaded map, scenario: #scenario.'() {
+        given: 'preloaded map with one yang model cm handle and its alternate id'
+            def cmHandlePerAlternateId = ['/a/b': testYangModelCmHandle]
+        when: 'getting the best matching yang model cm handle'
+            def result = objectUnderTest.getCmHandleByLongestMatchingAlternateId(targetAlternateId, '/', cmHandlePerAlternateId)
+        then: 'the correct yang model cm handle is found'
+            assert result == testYangModelCmHandle
+        where: 'the following alternate ids are used'
+            scenario                                                   | targetAlternateId
+            'exact match'                                              | '/a/b'
+            'parent match'                                             | '/a/b/c'
+            'grand parent match'                                       | '/a/b/c/d'
+            'trailing separator match'                                 | '/a/b/'
+            'with attribute path component and exact match'            | '/a/b#q'
+            'with attribute path component and parent match'           | '/a/b/c#q'
+            'with attribute path component and grand parent match'     | '/a/b/c/d#q'
+            'with attribute path component and additional slash match' | '/a/b/#q'
+    }
+
+    def 'Attempt to find cm handle with longest match using pre-loaded map without any matches.'() {
+        given: 'preloaded map with one yang model cm handle and its alternate id'
+            def cmHandlePerAlternateId = ['/a/b': testYangModelCmHandle]
+        when: 'attempt to find yang model cm handle'
+            objectUnderTest.getCmHandleByLongestMatchingAlternateId(targetAlternateId, '/', cmHandlePerAlternateId)
+        then: 'no alternate id match found exception thrown'
+            def thrown = thrown(NoAlternateIdMatchFoundException)
+        and: 'the exception has the relevant details from the error response'
+            assert thrown.message == 'No matching cm handle found using alternate ids'
+            assert thrown.details == 'cannot find a datanode with alternate id ' + targetAlternateId
+        where: 'the following parameters are used'
+            scenario                   | targetAlternateId
+            'no match for parent only' | '/a'
+            'no match for other child' | '/a/c'
+            'no match at all'          | '/x/y'
     }
 
     def 'Get cm handle id from a cm handle reference that is a #scenario id.' () {
@@ -78,12 +129,10 @@ class AlternateIdMatcherSpec extends Specification {
     }
 
     def 'Get cm handle id when given reference DOES NOT exist in cache.'() {
-        given: 'cmHandleIdPerAlternateId cache returns null'
-            mockCmHandleIdPerAlternateId.get('nonExistingId') >> null
         when: 'getting a cm handle id from the reference'
             objectUnderTest.getCmHandleId('nonExistingId')
         then: 'an exception is thrown'
             def thrownException = thrown(CmHandleNotFoundException)
             assert thrownException.getMessage().contains('Cm handle not found')
     }
-}
\ No newline at end of file
+}
index b432823..589443f 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2023-2025 Nordix Foundation
+ *  Copyright (C) 2023-2025 OpenInfra Foundation Europe. All rights reserved.
  *  Modifications Copyright (C) 2024-2025 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the 'License');
@@ -279,6 +279,11 @@ abstract class CpsIntegrationSpecBase extends Specification {
         registerSequenceOfCmHandles(dmiPlugin, moduleSetTag, numberOfCmHandles, offset, ModuleNameStrategy.UNIQUE, { id -> "alt=${id}" })
     }
 
+    def registerSequenceOfCmHandlesWithManyModuleReferencesButDoNotWaitForReady(dmiPlugin, moduleSetTag, numberOfCmHandles, offset, altIdPrefix) {
+        registerSequenceOfCmHandles(dmiPlugin, moduleSetTag, numberOfCmHandles, offset, ModuleNameStrategy.UNIQUE, { id -> "${altIdPrefix}alt=${id}" })
+    }
+
+
     def registerSequenceOfCmHandlesWithManyModuleReferencesButDoNotWaitForReady(dmiPlugin, moduleSetTag, numberOfCmHandles, offset, ModuleNameStrategy moduleNameStrategy) {
         registerSequenceOfCmHandles(dmiPlugin, moduleSetTag, numberOfCmHandles, offset, moduleNameStrategy, { id -> "alt=${id}" })
     }
index b9d57cf..a733d42 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2025 Nordix Foundation
+ *  Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the 'License');
  *  you may not use this file except in compliance with the License.
@@ -30,18 +30,39 @@ class AlternateIdPerfTest extends CpsIntegrationSpecBase {
 
     def resourceMeter = new ResourceMeter()
 
+    def NETWORK_SIZE = 10_000
+    def altIdPrefix = '/a=1/b=2/c=3/'
+
+    def setup() {
+        registerSequenceOfCmHandlesWithManyModuleReferencesButDoNotWaitForReady(DMI1_URL, 'tagA', NETWORK_SIZE, 1, altIdPrefix)
+    }
+
+    def cleanup() {
+        deregisterSequenceOfCmHandles(DMI1_URL, NETWORK_SIZE, 1)
+    }
+
     def 'Alternate Id Lookup Performance.'() {
-        given: 'register 1,000 cm handles (with alternative ids)'
-            registerSequenceOfCmHandlesWithManyModuleReferencesButDoNotWaitForReady(DMI1_URL, 'tagA', 1000, 1)
         when: 'perform a 1,000 lookups by alternate id'
             resourceMeter.start()
             (1..1000).each {
-                networkCmProxyInventoryFacade.getNcmpServiceCmHandle("alt=${it}")
+                networkCmProxyInventoryFacade.getNcmpServiceCmHandle("${altIdPrefix}alt=${it}")
             }
             resourceMeter.stop()
         then: 'record the result. Not asserted, just recorded in See https://lf-onap.atlassian.net/browse/CPS-2605'
             println "*** CPS-2605 Execution time: ${resourceMeter.totalTimeInSeconds} ms"
-        cleanup: 'deregister test cm handles'
-            deregisterSequenceOfCmHandles(DMI1_URL, 1000, 1)
+    }
+
+    def 'Alternate Id Longest Match Performance.'() {
+        given: 'an offset at 90% of the network size, so matches are not at the start...'
+            def offset = (int) (0.9 * NETWORK_SIZE)
+        when: 'perform a 100 longest matches'
+            resourceMeter.start()
+            (1..100).each {
+                def target = "${altIdPrefix}alt=${it + offset}/d=4/e=5/f=6/g=7"
+                alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId(target, "/")
+            }
+            resourceMeter.stop()
+        then: 'record the result. Not asserted, just recorded in See https://lf-onap.atlassian.net/browse/CPS-2743?focusedCommentId=83220'
+            println "*** CPS-2743 Execution time: ${resourceMeter.totalTimeInSeconds} ms"
     }
 }
index de7ffab..23d0524 100644 (file)
@@ -36,45 +36,56 @@ import java.util.concurrent.Executors
  */
 class WriteDataJobPerfTest extends CpsIntegrationSpecBase {
 
+    def NETWORK_SIZE = 1_000  // Increase to 40_000 for more realistic tests!
+
     @Autowired
     DataJobService dataJobService
 
-    def populateDataJobWriteRequests(int numberOfWriteOperations) {
-        def writeOperations = []
-        for (int i = 1; i <= numberOfWriteOperations; i++) {
-            def basePath = "/SubNetwork=Europe/SubNetwork=Ireland/MeContext=MyRadioNode${i}/ManagedElement=MyManagedElement${i}"
-            writeOperations.add(new WriteOperation("${basePath}/SomeChild=child-1", 'operation1', '1', null))
-            writeOperations.add(new WriteOperation("${basePath}/SomeChild=child-2", 'operation2', '2', null))
-            writeOperations.add(new WriteOperation(basePath, 'operation3', '3', null))
-        }
-        return new DataJobWriteRequest(writeOperations)
+    def setup() {
+        registerTestCmHandles(NETWORK_SIZE)
+    }
+
+    def cleanup() {
+        deregisterSequenceOfCmHandles(DMI1_URL, NETWORK_SIZE, 1)
+    }
+
+    @Ignore  // CPS-2691 / CPS-2692
+    def 'Performance test Large cm write data job.'() {
+        given: '3 large cm write data jobs'
+            def dataJobWriteRequest1 = populateDataJobWriteRequests(NETWORK_SIZE, 0)
+            def dataJobWriteRequest2 = populateDataJobWriteRequests(NETWORK_SIZE, 0)
+            def dataJobWriteRequest3 = populateDataJobWriteRequests(NETWORK_SIZE, 0)
+        when: 'sending a write jobs to NCMP with dynamically generated write operations'
+            def executionResult1 = executeWriteJob('d1', dataJobWriteRequest1)
+            def executionResult2 = executeWriteJob('d1', dataJobWriteRequest2)
+            def executionResult3 = executeWriteJob('d1', dataJobWriteRequest3)
+        then: 'record the results (about 3-4 seconds). Not asserted, just recorded in See https://lf-onap.atlassian.net/browse/CPS-2691'
+            println "*** CPS-2691 (L) Execution time run 1: ${executionResult1.executionTime} seconds | Memory usage: ${executionResult1.memoryUsage} MB"
+            println "*** CPS-2691 (L) Execution time run 2: ${executionResult2.executionTime} seconds | Memory usage: ${executionResult2.memoryUsage} MB"
+            println "*** CPS-2691 (L) Execution time run 3: ${executionResult3.executionTime} seconds | Memory usage: ${executionResult3.memoryUsage} MB"
     }
 
-    @Ignore  // CPS-2691
-    def 'Performance test for writeDataJob method'() {
-        given: 'register 10_000 cm handles (with alternate ids)'
-            registerTestCmHandles(10_000)
-            def dataJobWriteRequest = populateDataJobWriteRequests(10_000)
+    def 'Performance test Small cm write data job.'() {
+        given: 'a small'
+            def dataJobWriteRequest = populateDataJobWriteRequests(100, 0)
         when: 'sending a write job to NCMP with dynamically generated write operations'
             def executionResult = executeWriteJob('d1', dataJobWriteRequest)
-        then: 'record the result. Not asserted, just recorded in See https://lf-onap.atlassian.net/browse/CPS-2691'
-            println "*** CPS-2691 Execution time: ${executionResult.executionTime} seconds | Memory usage: ${executionResult.memoryUsage} MB"
-        cleanup: 'deregister test cm handles'
-            deregisterTestCmHandles(10_000)
+        then: 'record the result (about 2-3 second). Not asserted, TO BE be recorded in https://lf-onap.atlassian.net/browse/CPS-2743'
+            println "*** CPS-2691 (S) Execution time: ${executionResult.executionTime} seconds | Memory usage: ${executionResult.memoryUsage} MB"
     }
 
     @Ignore  // CPS-2692
-    def 'Performance test for writeDataJob method with 10 parallel requests'() {
-        given: 'register 10_000 cm handles (with alternate ids)'
-            registerTestCmHandles(1_000)
-        when: 'sending 10 parallel write jobs to NCMP'
-            def executionResults = executeParallelWriteJobs(10, 1_000)
-        then: 'record execution times'
-            executionResults.eachWithIndex { result, index ->
-                logExecutionResults("CPS-2692 Job-${index + 1}", result)
-            }
-        cleanup: 'deregister test cm handles'
-            deregisterSequenceOfCmHandles(DMI1_URL, 1_000, 1)
+    def 'Performance test parallel small cm write data jobs.'() {
+        when: 'sending 10 parallel write jobs to NCMP, execute test 3 times with some delay and different offsets'
+            def executionResults1 = executeParallelWriteJobs(10, 100, 0)
+            Thread.sleep(500)
+            def executionResults2 = executeParallelWriteJobs(10, 100, 200)
+            Thread.sleep(500)
+            def executionResults3 = executeParallelWriteJobs(10, 100, 300)
+        then: 'record execution times, note how 3rd run will be fastest!'
+            executionResults1.eachWithIndex { result1, index1 -> logExecutionResults("CPS-2692 run 1 Job-${index1 + 1}", result1) }
+            executionResults2.eachWithIndex { result2, index2 -> logExecutionResults("CPS-2692 run 2 Job-${index2 + 1}", result2) }
+            executionResults3.eachWithIndex { result3, index3 -> logExecutionResults("CPS-2692 run 3 Job-${index3 + 1}", result3) }
     }
 
     def registerTestCmHandles(numberOfCmHandles) {
@@ -84,16 +95,28 @@ class WriteDataJobPerfTest extends CpsIntegrationSpecBase {
         )
     }
 
-    def executeParallelWriteJobs(numberOfJobs, numberOfWriteOperations) {
+    def executeParallelWriteJobs(numberOfJobs, numberOfWriteOperations, offset) {
         def executorService = Executors.newFixedThreadPool(numberOfJobs)
         def futures = (0..<numberOfJobs).collect { jobId ->
-            CompletableFuture.supplyAsync({ -> executeWriteJob(jobId, populateDataJobWriteRequests(numberOfWriteOperations)) }, executorService)
+            CompletableFuture.supplyAsync({ -> executeWriteJob(jobId, populateDataJobWriteRequests(numberOfWriteOperations, offset)) }, executorService)
         }
         def executionResults = futures.collect { it.join() }
         executorService.shutdown()
         return executionResults
     }
 
+    def populateDataJobWriteRequests(numberOfWriteOperations, offset) {
+        def writeOperations = []
+        for (int i = 1; i <= numberOfWriteOperations; i++) {
+            def basePath = "/SubNetwork=Europe/SubNetwork=Ireland/MeContext=MyRadioNode${offset + i}/ManagedElement=MyManagedElement${offset + i}"
+            writeOperations.add(new WriteOperation("${basePath}/SomeChild=child-1", 'operation1', '1', null))
+            writeOperations.add(new WriteOperation("${basePath}/SomeChild=child-2", 'operation2', '2', null))
+            writeOperations.add(new WriteOperation(basePath, 'operation3', '3', null))
+        }
+        return new DataJobWriteRequest(writeOperations)
+    }
+
+
     def executeWriteJob(jobId, dataJobWriteRequest) {
         def localMeter = new ResourceMeter()
         localMeter.start()
@@ -106,7 +129,4 @@ class WriteDataJobPerfTest extends CpsIntegrationSpecBase {
         println "*** ${jobId} Execution time: ${result.executionTime} seconds | Memory usage: ${result.memoryUsage} MB"
     }
 
-    def deregisterTestCmHandles(numberOfCmHandles) {
-        deregisterSequenceOfCmHandles(DMI1_URL, numberOfCmHandles, 1)
-    }
 }