- 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>
<?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
================================================================================
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;
*/
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);
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());
}
+
}
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();
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.
*
@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
.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));
+ }
+
}
*/
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();
}
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;
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;
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 =
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,
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;
@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) {
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.
*
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
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.'() {
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'
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'
+ }
+
}
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
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 {
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.'() {
'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.' () {
}
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
+}
/*
* ============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');
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}" })
}
/*
* ============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.
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"
}
}
*/
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) {
)
}
- 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()
println "*** ${jobId} Execution time: ${result.executionTime} seconds | Memory usage: ${result.memoryUsage} MB"
}
- def deregisterTestCmHandles(numberOfCmHandles) {
- deregisterSequenceOfCmHandles(DMI1_URL, numberOfCmHandles, 1)
- }
}