From ed6c05157f60328b0215bde544f7a4e9894fd15f Mon Sep 17 00:00:00 2001 From: lukegleeson Date: Thu, 18 Aug 2022 17:47:24 +0100 Subject: Performance Improvement: Batch Update DataNodes Implemented methods to perform a batch operation on updating datanodes Refactored replace data node(s) tree methods to update data node(s) and descendants Issue-ID: CPS-1203 Signed-off-by: lukegleeson Change-Id: I365d657422b19c9ce384110c9a23d041eaed06f4 --- .../main/java/org/onap/cps/api/CpsDataService.java | 18 +++++++-- .../org/onap/cps/api/impl/CpsDataServiceImpl.java | 29 ++++++++++++-- .../onap/cps/spi/CpsDataPersistenceService.java | 13 ++++++- .../cps/api/impl/CpsDataServiceImplSpec.groovy | 45 +++++++++++++++++++--- 4 files changed, 91 insertions(+), 14 deletions(-) (limited to 'cps-service/src') diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java index cde25a9f9..decf67d24 100644 --- a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java +++ b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java @@ -24,6 +24,7 @@ package org.onap.cps.api; import java.time.OffsetDateTime; import java.util.Collection; +import java.util.Map; import org.onap.cps.spi.FetchDescendantsOption; import org.onap.cps.spi.model.DataNode; @@ -93,7 +94,7 @@ public interface CpsDataService { OffsetDateTime observedTimestamp); /** - * Replaces existing data node content including descendants. + * Replaces an existing data node's content including descendants. * * @param dataspaceName dataspace name * @param anchorName anchor name @@ -101,8 +102,19 @@ public interface CpsDataService { * @param jsonData json data * @param observedTimestamp observedTimestamp */ - void replaceNodeTree(String dataspaceName, String anchorName, String parentNodeXpath, String jsonData, - OffsetDateTime observedTimestamp); + void updateDataNodeAndDescendants(String dataspaceName, String anchorName, String parentNodeXpath, String jsonData, + OffsetDateTime observedTimestamp); + + /** + * Replaces multiple existing data nodes' content including descendants in a batch operation. + * + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param nodesJsonData map of xpath and node JSON data + * @param observedTimestamp observedTimestamp + */ + void updateDataNodesAndDescendants(String dataspaceName, String anchorName, Map nodesJsonData, + OffsetDateTime observedTimestamp); /** * Replaces list content by removing all existing elements and inserting the given new elements as json diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java index 7bdc2c166..092fd31fc 100755 --- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java @@ -28,6 +28,9 @@ import static org.onap.cps.notification.Operation.UPDATE; import java.time.OffsetDateTime; import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.onap.cps.api.CpsAdminService; @@ -142,14 +145,27 @@ public class CpsDataServiceImpl implements CpsDataService { } @Override - public void replaceNodeTree(final String dataspaceName, final String anchorName, final String parentNodeXpath, - final String jsonData, final OffsetDateTime observedTimestamp) { + public void updateDataNodeAndDescendants(final String dataspaceName, final String anchorName, + final String parentNodeXpath, final String jsonData, + final OffsetDateTime observedTimestamp) { CpsValidator.validateNameCharacters(dataspaceName, anchorName); final DataNode dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData); - cpsDataPersistenceService.replaceDataNodeTree(dataspaceName, anchorName, dataNode); + cpsDataPersistenceService.updateDataNodeAndDescendants(dataspaceName, anchorName, dataNode); processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, UPDATE, observedTimestamp); } + @Override + public void updateDataNodesAndDescendants(final String dataspaceName, final String anchorName, + final Map nodesJsonData, + final OffsetDateTime observedTimestamp) { + CpsValidator.validateNameCharacters(dataspaceName, anchorName); + final List dataNodes = buildDataNodes(dataspaceName, anchorName, nodesJsonData); + cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, dataNodes); + nodesJsonData.keySet().forEach(nodeXpath -> + processDataUpdatedEventAsync(dataspaceName, anchorName, nodeXpath, + UPDATE, observedTimestamp)); + } + @Override public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath, final String jsonData, final OffsetDateTime observedTimestamp) { @@ -209,6 +225,13 @@ public class CpsDataServiceImpl implements CpsDataService { .build(); } + private List buildDataNodes(final String dataspaceName, final String anchorName, + final Map nodesJsonData) { + return nodesJsonData.entrySet().stream().map(nodeJsonData -> + buildDataNode(dataspaceName, anchorName, nodeJsonData.getKey(), + nodeJsonData.getValue())).collect(Collectors.toList()); + } + private Collection buildDataNodes(final String dataspaceName, final String anchorName, final String parentNodeXpath, diff --git a/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java b/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java index b27a2976d..686f0f3fe 100644 --- a/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java +++ b/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java @@ -90,13 +90,22 @@ public interface CpsDataPersistenceService { void updateDataLeaves(String dataspaceName, String anchorName, String xpath, Map leaves); /** - * Replaces existing data node content including descendants. + * Replaces an existing data node's content including descendants. * * @param dataspaceName dataspace name * @param anchorName anchor name * @param dataNode data node */ - void replaceDataNodeTree(String dataspaceName, String anchorName, DataNode dataNode); + void updateDataNodeAndDescendants(String dataspaceName, String anchorName, DataNode dataNode); + + /** + * Replaces multiple existing data nodes' content including descendants in a batch operation. + * + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param dataNodes data nodes + */ + void updateDataNodesAndDescendants(String dataspaceName, String anchorName, final List dataNodes); /** * Replaces list content by removing all existing elements and inserting the given new elements diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy index 6c995fa85..cb352bcce 100644 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy @@ -262,13 +262,13 @@ class CpsDataServiceImplSpec extends Specification { } - def 'Replace data node: #scenario.'() { + def 'Replace data node using singular data node: #scenario.'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') when: 'replace data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath' - objectUnderTest.replaceNodeTree(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp) + objectUnderTest.updateDataNodeAndDescendants(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp) then: 'the persistence service method is invoked with correct parameters' - 1 * mockCpsDataPersistenceService.replaceDataNodeTree(dataspaceName, anchorName, + 1 * mockCpsDataPersistenceService.updateDataNodeAndDescendants(dataspaceName, anchorName, { dataNode -> dataNode.xpath == expectedNodeXpath }) and: 'data updated event is sent to notification service' 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, parentNodeXpath, Operation.UPDATE, observedTimestamp) @@ -278,13 +278,46 @@ class CpsDataServiceImplSpec extends Specification { 'level 2 node' | '/test-tree' | '{"branch": [{"name":"Name"}]}' || '/test-tree/branch[@name=\'Name\']' } - def 'Replace data node with invalid #scenario.'() { + def 'Replace data node using multiple data nodes: #scenario.'() { + given: 'schema set for given anchor and dataspace references test-tree model' + setupSchemaSetMocks('test-tree.yang') + when: 'replace data method is invoked with a map of xpaths and json data' + objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, nodesJsonData, observedTimestamp) + then: 'the persistence service method is invoked with correct parameters' + 1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, + { dataNode -> dataNode.xpath == expectedNodeXpath}) + and: 'data updated event is sent to notification service' + 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, nodesJsonData.keySet()[0], Operation.UPDATE, observedTimestamp) + 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, nodesJsonData.keySet()[1], Operation.UPDATE, observedTimestamp) + where: 'following parameters were used' + scenario | nodesJsonData || expectedNodeXpath + 'top level node' | ['/' : '{"test-tree": {"branch": []}}', '/test-tree' : '{"branch": [{"name":"Name"}]}'] || ["/test-tree", "/test-tree/branch[@name='Name']"] + 'level 2 node' | ['/test-tree' : '{"branch": [{"name":"Name"}]}', '/test-tree/branch[@name=\'Name\']':'{"nest":{"name":"nestName"}}'] || ["/test-tree/branch[@name='Name']", "/test-tree/branch[@name='Name']/nest"] + } + + def 'Replace data node using singular data node with invalid #scenario.'() { + when: 'replace data method is invoked with invalid #scenario' + objectUnderTest.updateDataNodeAndDescendants(dataspaceName, anchorName, '/', _ as String, observedTimestamp) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'the persistence service method is not invoked' + 0 * mockCpsDataPersistenceService.updateDataNodeAndDescendants(*_) + and: 'data updated event is not sent to notification service' + 0 * mockNotificationService.processDataUpdatedEvent(*_) + where: 'the following parameters are used' + scenario | dataspaceName | anchorName + 'dataspace name' | 'dataspace names with spaces' | 'anchorName' + 'anchor name' | 'dataspaceName' | 'anchor name with spaces' + 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces' + } + + def 'Replace data node using multiple data nodes with invalid #scenario.'() { when: 'replace data method is invoked with invalid #scenario' - objectUnderTest.replaceNodeTree(dataspaceName, anchorName, '/', _ as String, observedTimestamp) + objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, ['/': _ as String], observedTimestamp) then: 'a data validation exception is thrown' thrown(DataValidationException) and: 'the persistence service method is not invoked' - 0 * mockCpsDataPersistenceService.replaceDataNodeTree(*_) + 0 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(*_) and: 'data updated event is not sent to notification service' 0 * mockNotificationService.processDataUpdatedEvent(*_) where: 'the following parameters are used' -- cgit 1.2.3-korg