From ed6c05157f60328b0215bde544f7a4e9894fd15f Mon Sep 17 00:00:00 2001
From: lukegleeson <luke.gleeson@est.tech>
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 <luke.gleeson@est.tech>
Change-Id: I365d657422b19c9ce384110c9a23d041eaed06f4
---
 ...CpsDataPersistenceServiceIntegrationSpec.groovy | 42 +++++------
 .../spi/impl/CpsDataPersistenceServiceSpec.groovy  | 83 +++++++++++++++-------
 2 files changed, 79 insertions(+), 46 deletions(-)

(limited to 'cps-ri/src/test/groovy/org/onap')

diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy
index 6f780fc508..fee489d18b 100755
--- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy
+++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy
@@ -55,8 +55,8 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
     static final long ID_DATA_NODE_WITH_DESCENDANTS = 4001
     static final String XPATH_DATA_NODE_WITH_DESCENDANTS = '/parent-1'
     static final String XPATH_DATA_NODE_WITH_LEAVES = '/parent-100'
-    static final long UPDATE_DATA_NODE_FRAGMENT_ID = 4202L
-    static final long UPDATE_DATA_NODE_SUB_FRAGMENT_ID = 4203L
+    static final long DATA_NODE_202_FRAGMENT_ID = 4202L
+    static final long CHILD_OF_DATA_NODE_202_FRAGMENT_ID = 4203L
     static final long LIST_DATA_NODE_PARENT201_FRAGMENT_ID = 4206L
     static final long LIST_DATA_NODE_PARENT203_FRAGMENT_ID = 4214L
     static final long LIST_DATA_NODE_PARENT202_FRAGMENT_ID = 4211L
@@ -258,14 +258,14 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
             objectUnderTest.updateDataLeaves(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
                     "/parent-200/child-201", ['leaf-value': 'new'])
         then: 'leaves are updated for selected data node'
-            def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
+            def updatedFragment = fragmentRepository.getById(DATA_NODE_202_FRAGMENT_ID)
             def updatedLeaves = getLeavesMap(updatedFragment)
             assert updatedLeaves.size() == 1
             assert updatedLeaves.'leaf-value' == 'new'
         and: 'existing child entry remains as is'
             def childFragment = updatedFragment.childFragments.iterator().next()
             def childLeaves = getLeavesMap(childFragment)
-            assert childFragment.id == UPDATE_DATA_NODE_SUB_FRAGMENT_ID
+            assert childFragment.id == CHILD_OF_DATA_NODE_202_FRAGMENT_ID
             assert childLeaves.'leaf-value' == 'original'
     }
 
@@ -283,32 +283,32 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
-    def 'Replace data node tree with descendants removal.'() {
+    def 'Update data node and descendants by removing descendants.'() {
         given: 'data node object with leaves updated, no children'
             def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [])
-        when: 'replace data node tree is performed'
-            objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
+        when: 'update data nodes and descendants is performed'
+            objectUnderTest.updateDataNodeAndDescendants(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
         then: 'leaves have been updated for selected data node'
-            def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
+            def updatedFragment = fragmentRepository.getById(DATA_NODE_202_FRAGMENT_ID)
             def updatedLeaves = getLeavesMap(updatedFragment)
             assert updatedLeaves.size() == 1
             assert updatedLeaves.'leaf-value' == 'new'
         and: 'updated entry has no children'
             updatedFragment.childFragments.isEmpty()
         and: 'previously attached child entry is removed from database'
-            fragmentRepository.findById(UPDATE_DATA_NODE_SUB_FRAGMENT_ID).isEmpty()
+            fragmentRepository.findById(CHILD_OF_DATA_NODE_202_FRAGMENT_ID).isEmpty()
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
-    def 'Replace data node tree with descendants.'() {
+    def 'Update data node and descendants with new descendants'() {
         given: 'data node object with leaves updated, having child with old content'
             def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [
                   buildDataNode("/parent-200/child-201/grand-child", ['leaf-value': 'original'], [])
             ])
         when: 'update is performed including descendants'
-            objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
+            objectUnderTest.updateDataNodeAndDescendants(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
         then: 'leaves have been updated for selected data node'
-            def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
+            def updatedFragment = fragmentRepository.getById(DATA_NODE_202_FRAGMENT_ID)
             def updatedLeaves = getLeavesMap(updatedFragment)
             assert updatedLeaves.size() == 1
             assert updatedLeaves.'leaf-value' == 'new'
@@ -320,15 +320,15 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
-    def 'Replace data node tree with same descendants but changed leaf value.'() {
+    def 'Update data node and descendants with same descendants but changed leaf value.'() {
         given: 'data node object with leaves updated, having child with old content'
             def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [
                     buildDataNode("/parent-200/child-201/grand-child", ['leaf-value': 'new'], [])
             ])
         when: 'update is performed including descendants'
-            objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
+            objectUnderTest.updateDataNodeAndDescendants(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
         then: 'leaves have been updated for selected data node'
-            def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
+            def updatedFragment = fragmentRepository.getById(DATA_NODE_202_FRAGMENT_ID)
             def updatedLeaves = getLeavesMap(updatedFragment)
             assert updatedLeaves.size() == 1
             assert updatedLeaves.'leaf-value' == 'new'
@@ -340,20 +340,20 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
-    def 'Replace data node tree with different descendants xpath'() {
+    def 'Update data node and descendants with different descendants xpath'() {
         given: 'data node object with leaves updated, having child with old content'
             def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [
                     buildDataNode("/parent-200/child-201/grand-child-new", ['leaf-value': 'new'], [])
             ])
         when: 'update is performed including descendants'
-            objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
+            objectUnderTest.updateDataNodeAndDescendants(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
         then: 'leaves have been updated for selected data node'
-            def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
+            def updatedFragment = fragmentRepository.getById(DATA_NODE_202_FRAGMENT_ID)
             def updatedLeaves = getLeavesMap(updatedFragment)
             assert updatedLeaves.size() == 1
             assert updatedLeaves.'leaf-value' == 'new'
         and: 'previously attached child entry is removed from database'
-            fragmentRepository.findById(UPDATE_DATA_NODE_SUB_FRAGMENT_ID).isEmpty()
+            fragmentRepository.findById(CHILD_OF_DATA_NODE_202_FRAGMENT_ID).isEmpty()
         and: 'new child entry is persisted'
             def childFragment = updatedFragment.childFragments.iterator().next()
             childFragment.xpath == '/parent-200/child-201/grand-child-new'
@@ -362,11 +362,11 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
-    def 'Replace data node tree error scenario: #scenario.'() {
+    def 'Update data node and descendants error scenario: #scenario.'() {
         given: 'data node object'
             def submittedDataNode = buildDataNode(xpath, ['leaf-name': 'leaf-value'], [])
         when: 'attempt to update data node for #scenario'
-            objectUnderTest.replaceDataNodeTree(dataspaceName, anchorName, submittedDataNode)
+            objectUnderTest.updateDataNodeAndDescendants(dataspaceName, anchorName, submittedDataNode)
         then: 'a #expectedException is thrown'
             thrown(expectedException)
         where: 'the following data is used'
diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy
index bde2f3de9f..1bbf358e54 100644
--- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy
+++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy
@@ -28,6 +28,7 @@ import org.onap.cps.spi.entities.SchemaSetEntity
 import org.onap.cps.spi.entities.YangResourceEntity
 import org.onap.cps.spi.exceptions.ConcurrencyException
 import org.onap.cps.spi.exceptions.DataValidationException
+import org.onap.cps.spi.model.DataNode
 import org.onap.cps.spi.model.DataNodeBuilder
 import org.onap.cps.spi.repository.AnchorRepository
 import org.onap.cps.spi.repository.DataspaceRepository
@@ -67,40 +68,40 @@ class CpsDataPersistenceServiceSpec extends Specification {
     )] as Set
 
 
-    def 'Handling of StaleStateException (caused by concurrent updates) during data node tree update.'() {
-
-        def parentXpath = '/parent-01'
-        def myDataspaceName = 'my-dataspace'
-        def myAnchorName = 'my-anchor'
-
-        given: 'data node object'
-            def submittedDataNode = new DataNodeBuilder()
-                    .withXpath(parentXpath)
-                    .withLeaves(['leaf-name': 'leaf-value'])
-                    .build()
-        and: 'fragment to be updated'
-            mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> {
+    def 'Handling of StaleStateException (caused by concurrent updates) during update data node and descendants.'() {
+        given: 'the fragment repository returns a fragment entity'
+            mockFragmentRepository.getByDataspaceAndAnchorAndXpath(*_) >> {
                 def fragmentEntity = new FragmentEntity()
-                fragmentEntity.setXpath(parentXpath)
-                fragmentEntity.setChildFragments(Collections.emptySet())
+                fragmentEntity.setChildFragments([new FragmentEntity()] as Set<FragmentEntity>)
                 return fragmentEntity
             }
-        and: 'data node is concurrently updated by another transaction'
+        and: 'a data node is concurrently updated by another transaction'
             mockFragmentRepository.save(_) >> { throw new StaleStateException("concurrent updates") }
+        when: 'attempt to update data node with submitted data nodes'
+            objectUnderTest.updateDataNodeAndDescendants('some-dataspace', 'some-anchor', new DataNodeBuilder().withXpath('/some/xpath').build())
+        then: 'concurrency exception is thrown'
+            def concurrencyException = thrown(ConcurrencyException)
+            assert concurrencyException.getDetails().contains('some-dataspace')
+            assert concurrencyException.getDetails().contains('some-anchor')
+            assert concurrencyException.getDetails().contains('/some/xpath')
+    }
 
-        when: 'attempt to update data node'
-            objectUnderTest.replaceDataNodeTree(myDataspaceName, myAnchorName, submittedDataNode)
-
+    def 'Handling of StaleStateException (caused by concurrent updates) during  update data nodes and descendants.'() {
+        given: 'the fragment repository returns a list of fragment entities'
+            mockFragmentRepository.getByDataspaceAndAnchorAndXpath(*_) >> new FragmentEntity()
+        and: 'a data node is concurrently updated by another transaction'
+            mockFragmentRepository.saveAll(*_) >> { throw new StaleStateException("concurrent updates") }
+        when: 'attempt to update data node with submitted data nodes'
+            objectUnderTest.updateDataNodesAndDescendants('some-dataspace', 'some-anchor', [])
         then: 'concurrency exception is thrown'
             def concurrencyException = thrown(ConcurrencyException)
-            assert concurrencyException.getDetails().contains(myDataspaceName)
-            assert concurrencyException.getDetails().contains(myAnchorName)
-            assert concurrencyException.getDetails().contains(parentXpath)
+            assert concurrencyException.getDetails().contains('some-dataspace')
+            assert concurrencyException.getDetails().contains('some-anchor')
     }
 
     def 'Retrieving a data node with a property JSON value of #scenario'() {
         given: 'a fragment with a property JSON value of #scenario'
-        mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> {
+        mockFragmentRepository.getByDataspaceAndAnchorAndXpath(*_) >> {
             new FragmentEntity(childFragments: Collections.emptySet(),
                     attributes: "{\"some attribute\": ${dataString}}",
                     anchor: new AnchorEntity(schemaSet: new SchemaSetEntity(yangResources: yangResourceSet )))
@@ -128,11 +129,11 @@ class CpsDataPersistenceServiceSpec extends Specification {
 
     def 'Retrieving a data node with invalid JSON'() {
         given: 'a fragment with invalid JSON'
-            mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> {
+            mockFragmentRepository.getByDataspaceAndAnchorAndXpath(*_) >> {
                 new FragmentEntity(childFragments: Collections.emptySet(), attributes: '{invalid json')
             }
         when: 'getting the data node represented by this fragment'
-            def dataNode = objectUnderTest.getDataNode('my-dataspace', 'my-anchor',
+            objectUnderTest.getDataNode('my-dataspace', 'my-anchor',
                     '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
         then: 'a data validation exception is thrown'
             thrown(DataValidationException)
@@ -160,4 +161,36 @@ class CpsDataPersistenceServiceSpec extends Specification {
         then: 'the session manager method to lock anchor is invoked with same parameters'
             1 * mockSessionManager.lockAnchor('mySessionId', 'myDataspaceName', 'myAnchorName', 123L)
     }
+
+    def 'update data node and descendants: #scenario'(){
+        given: 'mocked responses'
+            mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, '/test/xpath') >> new FragmentEntity(xpath: '/test/xpath', childFragments: [])
+        when: 'replace data node tree'
+            objectUnderTest.updateDataNodesAndDescendants('dataspaceName', 'anchorName', dataNodes)
+        then: 'call fragment repository save all method'
+            1 * mockFragmentRepository.saveAll({fragmentEntities -> assert fragmentEntities as List == expectedFragmentEntities})
+        where: 'the following Data Type is passed'
+            scenario                         | dataNodes                                                                          || expectedFragmentEntities
+            'empty data node list'           | []                                                                                 || []
+            'one data node in list'          | [new DataNode(xpath: '/test/xpath', leaves: ['id': 'testId'], childDataNodes: [])] || [new FragmentEntity(xpath: '/test/xpath', attributes: '{"id":"testId"}', childFragments: [])]
+    }
+
+    def 'update data nodes and descendants'() {
+        given: 'the fragment repository returns a fragment entity related to the xpath input'
+            mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, '/test/xpath1') >> new FragmentEntity(xpath: '/test/xpath1', childFragments: [])
+            mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, '/test/xpath2') >> new FragmentEntity(xpath: '/test/xpath2', childFragments: [])
+        and: 'some data nodes with descendants'
+            def dataNode1 = new DataNode(xpath: '/test/xpath1', leaves: ['id': 'testId1'], childDataNodes: [new DataNode(xpath: '/test/xpath1/child', leaves: ['id': 'childTestId1'])])
+            def dataNode2 = new DataNode(xpath: '/test/xpath2', leaves: ['id': 'testId2'], childDataNodes: [new DataNode(xpath: '/test/xpath2/child', leaves: ['id': 'childTestId2'])])
+        when: 'the fragment entities are update by the data nodes'
+            objectUnderTest.updateDataNodesAndDescendants('dataspaceName', 'anchorName', [dataNode1, dataNode2])
+        then: 'call fragment repository save all method is called with the updated fragments'
+            1 * mockFragmentRepository.saveAll({fragmentEntities -> {
+                fragmentEntities.containsAll([
+                    new FragmentEntity(xpath: '/test/xpath1', attributes: '{"id":"testId1"}', childFragments: [new FragmentEntity(xpath: '/test/xpath1/child', attributes: '{"id":"childTestId1"}', childFragments: [])]),
+                    new FragmentEntity(xpath: '/test/xpath2', attributes: '{"id":"testId2"}', childFragments: [new FragmentEntity(xpath: '/test/xpath2/child', attributes: '{"id":"childTestId2"}', childFragments: [])])
+                ])
+                assert fragmentEntities.size() == 2
+            }})
+    }
 }
\ No newline at end of file
-- 
cgit