a26ba11fab0127aeffbb72dd291cfdf1c843def1
[cps.git] /
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2022-2025 OpenInfra Foundation Europe. All rights reserved.
4  *  Modifications Copyright (C) 2022 Bell Canada
5  *  ================================================================================
6  *  Licensed under the Apache License, Version 2.0 (the "License");
7  *  you may not use this file except in compliance with the License.
8  *  You may obtain a copy of the License at
9  *
10  *        http://www.apache.org/licenses/LICENSE-2.0
11  *
12  *  Unless required by applicable law or agreed to in writing, software
13  *  distributed under the License is distributed on an "AS IS" BASIS,
14  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  *  See the License for the specific language governing permissions and
16  *  limitations under the License.
17  *
18  *  SPDX-License-Identifier: Apache-2.0
19  *  ============LICENSE_END=========================================================
20  */
21
22 package org.onap.cps.ncmp.impl.inventory.sync
23
24 import ch.qos.logback.classic.Level
25 import ch.qos.logback.classic.Logger
26 import ch.qos.logback.classic.spi.ILoggingEvent
27 import ch.qos.logback.core.read.ListAppender
28 import com.hazelcast.map.IMap
29 import org.onap.cps.init.actuator.ReadinessManager
30 import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
31 import org.slf4j.LoggerFactory
32 import spock.lang.Specification
33
34 import java.util.concurrent.ArrayBlockingQueue
35
36 class ModuleSyncWatchdogSpec extends Specification {
37
38     def mockModuleOperationsUtils = Mock(ModuleOperationsUtils)
39
40     def static testQueueCapacity = 50 + 2 * ModuleSyncWatchdog.MODULE_SYNC_BATCH_SIZE
41
42     def moduleSyncWorkQueue = new ArrayBlockingQueue(testQueueCapacity)
43
44     def mockModuleSyncStartedOnCmHandles = Mock(IMap<String, Object>)
45
46     def mockModuleSyncTasks = Mock(ModuleSyncTasks)
47
48     def mockCpsCommonLocks = Mock(IMap<String,String>)
49
50     def mockReadinessManager = Mock(ReadinessManager)
51
52     def objectUnderTest = new ModuleSyncWatchdog(mockModuleOperationsUtils, moduleSyncWorkQueue , mockModuleSyncStartedOnCmHandles, mockModuleSyncTasks, mockCpsCommonLocks, mockReadinessManager)
53
54     def logAppender = Spy(ListAppender<ILoggingEvent>)
55
56     void setup() {
57         def logger = LoggerFactory.getLogger(ModuleSyncWatchdog)
58         logger.setLevel(Level.INFO)
59         logger.addAppender(logAppender)
60         logAppender.start()
61     }
62
63     void cleanup() {
64         ((Logger) LoggerFactory.getLogger(ModuleSyncWatchdog.class)).detachAndStopAllAppenders()
65     }
66
67     def 'Module sync watchdog is triggered'(){
68         given: 'the system is not ready to accept traffic'
69             mockReadinessManager.isReady() >> false
70         when: 'module sync is started'
71             objectUnderTest.scheduledModuleSyncAdvisedCmHandles()
72         then: 'an event is logged with level INFO'
73             def loggingEvent = getLoggingEvent()
74             assert loggingEvent.level == Level.INFO
75         and: 'the log indicates that the system is not ready yet'
76             assert loggingEvent.formattedMessage == 'System is not ready yet'
77     }
78
79     def 'Module sync advised cm handles with #scenario.'() {
80         given: 'system is ready to accept traffic'
81             mockReadinessManager.isReady() >> true
82         and: 'module sync utilities returns #numberOfAdvisedCmHandles advised cm handles'
83             mockModuleOperationsUtils.getAdvisedCmHandleIds() >> createCmHandleIds(numberOfAdvisedCmHandles)
84         and: 'module sync utilities returns no failed (locked) cm handles'
85             mockModuleOperationsUtils.getCmHandlesThatFailedModelSyncOrUpgrade() >> []
86         and: 'the work queue can be locked'
87             mockCpsCommonLocks.tryLock('workQueueLock') >> true
88         when: ' module sync is started'
89             objectUnderTest.moduleSyncAdvisedCmHandles()
90         then: 'it performs #expectedNumberOfTaskExecutions tasks'
91             expectedNumberOfTaskExecutions * mockModuleSyncTasks.performModuleSync(*_)
92         and: 'the executing thread is unlocked'
93             1 * mockCpsCommonLocks.unlock('workQueueLock')
94         where: 'the following parameter are used'
95             scenario              | numberOfAdvisedCmHandles                                          || expectedNumberOfTaskExecutions
96             'none at all'         | 0                                                                 || 0
97             'less then 1 batch'   | 1                                                                 || 1
98             'exactly 1 batch'     | ModuleSyncWatchdog.MODULE_SYNC_BATCH_SIZE                         || 1
99             '2 batches'           | 2 * ModuleSyncWatchdog.MODULE_SYNC_BATCH_SIZE                     || 2
100             'queue capacity'      | testQueueCapacity                                                 || 3
101             'over queue capacity' | testQueueCapacity + 2 * ModuleSyncWatchdog.MODULE_SYNC_BATCH_SIZE || 3
102     }
103
104     def 'Module sync cm handles starts with no available threads.'() {
105         given: 'system is ready to accept traffic'
106             mockReadinessManager.isReady() >> true
107         and: 'module sync utilities returns a advise cm handles'
108             mockModuleOperationsUtils.getAdvisedCmHandleIds() >> createCmHandleIds(1)
109         and: 'the work queue can be locked'
110             mockCpsCommonLocks.tryLock('workQueueLock') >> true
111         when: ' module sync is started'
112             objectUnderTest.moduleSyncAdvisedCmHandles()
113         then: 'it performs one task'
114             1 * mockModuleSyncTasks.performModuleSync(*_)
115     }
116
117     def 'Module sync advised cm handle already handled by other thread.'() {
118         given: 'system is ready to accept traffic'
119             mockReadinessManager.isReady() >> true
120         and: 'module sync utilities returns an advised cm handle'
121             mockModuleOperationsUtils.getAdvisedCmHandleIds() >> createCmHandleIds(1)
122         and: 'the work queue can be locked'
123             mockCpsCommonLocks.tryLock('workQueueLock') >> true
124         and: 'the semaphore cache indicates the cm handle is already being processed'
125             mockModuleSyncStartedOnCmHandles.putIfAbsent(*_) >> 'Started'
126         when: 'module sync is started'
127             objectUnderTest.moduleSyncAdvisedCmHandles()
128         then: 'it does NOT execute a task to process the (empty) batch'
129             0 * mockModuleSyncTasks.performModuleSync(*_)
130     }
131
132     def 'Module sync with previous cm handle(s) left in work queue.'() {
133         given: 'system is ready to accept traffic'
134             mockReadinessManager.isReady() >> true
135         and: 'there is still a cm handle in the queue'
136             moduleSyncWorkQueue.offer('ch-1')
137         when: 'module sync is started'
138             objectUnderTest.moduleSyncAdvisedCmHandles()
139         then: 'it does executes only one task to process the remaining handle in the queue'
140             1 * mockModuleSyncTasks.performModuleSync(*_)
141     }
142
143     def 'Reset failed cm handles.'() {
144         given: 'system is ready to accept traffic'
145             mockReadinessManager.isReady() >> true
146         and: 'module sync utilities returns failed cm handles'
147             def failedCmHandles = [new YangModelCmHandle()]
148             mockModuleOperationsUtils.getCmHandlesThatFailedModelSyncOrUpgrade() >> failedCmHandles
149         when: 'reset failed cm handles is started'
150             objectUnderTest.setPreviouslyLockedCmHandlesToAdvised()
151         then: 'it is delegated to the module sync task (service)'
152             1 * mockModuleSyncTasks.setCmHandlesToAdvised(failedCmHandles)
153     }
154
155     def 'Module Sync Locking.'() {
156         given: 'system is ready to accept traffic'
157             mockReadinessManager.isReady() >> true
158         and: 'module sync utilities returns an advised cm handle'
159             mockModuleOperationsUtils.getAdvisedCmHandleIds() >> createCmHandleIds(1)
160         and: 'can be locked is : #canLock'
161             mockCpsCommonLocks.tryLock('workQueueLock') >> canLock
162         when: 'attempt to populate the work queue'
163             objectUnderTest.populateWorkQueueIfNeeded()
164         then: 'the queue remains empty is #expectQueueRemainsEmpty'
165             assert moduleSyncWorkQueue.isEmpty() == expectQueueRemainsEmpty
166         and: 'unlock is called only when thread is able to enter the critical section'
167             expectedInvocationToUnlock * mockCpsCommonLocks.unlock('workQueueLock')
168         where: 'the following lock states are applied'
169             canLock || expectQueueRemainsEmpty || expectedInvocationToUnlock
170             false   || true                    || 0
171             true    || false                   || 1
172     }
173
174     def createCmHandleIds(numberOfCmHandles) {
175         return (numberOfCmHandles > 0) ? (1..numberOfCmHandles).collect { 'ch-'+it } : []
176     }
177
178     def getLoggingEvent() {
179         return logAppender.list[0]
180     }
181 }