2 * ============LICENSE_START=======================================================
4 * ================================================================================
5 * Copyright (C) 2019-2021 AT&T Intellectual Property. All rights reserved.
6 * Modifications Copyright (C) 2023-2024 Nordix Foundation.
7 * ================================================================================
8 * Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 * ============LICENSE_END=========================================================
22 package org.onap.policy.drools.system.internal;
24 import static org.assertj.core.api.Assertions.assertThat;
25 import static org.assertj.core.api.Assertions.assertThatCode;
26 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
27 import static org.assertj.core.api.Assertions.assertThatThrownBy;
28 import static org.junit.jupiter.api.Assertions.assertEquals;
29 import static org.junit.jupiter.api.Assertions.assertFalse;
30 import static org.junit.jupiter.api.Assertions.assertNotNull;
31 import static org.junit.jupiter.api.Assertions.assertNull;
32 import static org.junit.jupiter.api.Assertions.assertSame;
33 import static org.junit.jupiter.api.Assertions.assertTrue;
34 import static org.mockito.ArgumentMatchers.any;
35 import static org.mockito.ArgumentMatchers.anyBoolean;
36 import static org.mockito.ArgumentMatchers.anyLong;
37 import static org.mockito.Mockito.mock;
38 import static org.mockito.Mockito.never;
39 import static org.mockito.Mockito.times;
40 import static org.mockito.Mockito.verify;
41 import static org.mockito.Mockito.when;
43 import java.io.ByteArrayInputStream;
44 import java.io.ByteArrayOutputStream;
45 import java.io.ObjectInputStream;
46 import java.io.ObjectOutputStream;
47 import java.io.Serial;
48 import java.util.ArrayList;
49 import java.util.List;
50 import java.util.Properties;
51 import java.util.concurrent.Executors;
52 import java.util.concurrent.ScheduledExecutorService;
53 import java.util.concurrent.ScheduledFuture;
54 import java.util.concurrent.Semaphore;
55 import java.util.concurrent.TimeUnit;
56 import java.util.concurrent.atomic.AtomicInteger;
57 import org.junit.jupiter.api.AfterAll;
58 import org.junit.jupiter.api.AfterEach;
59 import org.junit.jupiter.api.BeforeAll;
60 import org.junit.jupiter.api.BeforeEach;
61 import org.junit.jupiter.api.Test;
62 import org.junit.jupiter.api.extension.ExtendWith;
63 import org.kie.api.runtime.KieSession;
64 import org.mockito.ArgumentCaptor;
65 import org.mockito.Mock;
66 import org.mockito.MockitoAnnotations;
67 import org.mockito.junit.jupiter.MockitoExtension;
68 import org.onap.policy.common.utils.time.CurrentTime;
69 import org.onap.policy.common.utils.time.TestTime;
70 import org.onap.policy.drools.core.PolicySession;
71 import org.onap.policy.drools.core.lock.Lock;
72 import org.onap.policy.drools.core.lock.LockCallback;
73 import org.onap.policy.drools.core.lock.LockState;
74 import org.onap.policy.drools.system.PolicyEngineConstants;
75 import org.onap.policy.drools.system.internal.SimpleLockManager.SimpleLock;
76 import org.springframework.test.util.ReflectionTestUtils;
78 @ExtendWith(MockitoExtension.class)
79 class SimpleLockManagerTest {
80 private static final String POLICY_ENGINE_EXECUTOR_FIELD = "executorService";
81 private static final String TIME_FIELD = "currentTime";
82 private static final String OWNER_KEY = "my key";
83 private static final String RESOURCE = "my resource";
84 private static final String RESOURCE2 = "my resource #2";
85 private static final String RESOURCE3 = "my resource #3";
86 private static final int HOLD_SEC = 100;
87 private static final int HOLD_SEC2 = 120;
88 private static final int HOLD_MS = HOLD_SEC * 1000;
89 private static final int HOLD_MS2 = HOLD_SEC2 * 1000;
90 private static final int MAX_THREADS = 10;
91 private static final int MAX_LOOPS = 50;
93 private static CurrentTime saveTime;
94 private static ScheduledExecutorService saveExec;
95 private static ScheduledExecutorService realExec;
97 private TestTime testTime;
98 private AtomicInteger nactive;
99 private AtomicInteger nsuccesses;
100 private SimpleLockManager feature;
103 private KieSession kieSess;
106 private ScheduledExecutorService exsvc;
109 private ScheduledFuture<?> future;
112 private LockCallback callback;
114 AutoCloseable closeable;
117 * Saves static fields and configures the location of the property files.
120 static void setUpBeforeClass() {
121 saveTime = (CurrentTime) ReflectionTestUtils.getField(SimpleLockManager.class, TIME_FIELD);
122 saveExec = (ScheduledExecutorService) ReflectionTestUtils.getField(PolicyEngineConstants.getManager(),
123 POLICY_ENGINE_EXECUTOR_FIELD);
125 realExec = Executors.newScheduledThreadPool(3);
129 * Restores static fields.
132 static void tearDownAfterClass() {
133 ReflectionTestUtils.setField(SimpleLockManager.class, TIME_FIELD, saveTime);
134 ReflectionTestUtils.setField(PolicyEngineConstants.getManager(), POLICY_ENGINE_EXECUTOR_FIELD, saveExec);
140 * Initializes the mocks and creates a feature that uses {@link #exsvc} to execute
145 closeable = MockitoAnnotations.openMocks(this);
146 // grant() and deny() calls will come through here and be immediately executed
147 PolicySession session = new PolicySession(null, null, kieSess) {
149 public void insertDrools(Object object) {
150 ((Runnable) object).run();
154 session.setPolicySession();
156 testTime = new TestTime();
157 nactive = new AtomicInteger(0);
158 nsuccesses = new AtomicInteger(0);
160 ReflectionTestUtils.setField(SimpleLockManager.class, TIME_FIELD, testTime);
162 ReflectionTestUtils.setField(PolicyEngineConstants.getManager(), POLICY_ENGINE_EXECUTOR_FIELD, exsvc);
164 feature = new MyLockingFeature();
169 void closeMocks() throws Exception {
174 * Tests constructor() when properties are invalid.
177 void testSimpleLockManagerInvalidProperties() {
178 // use properties containing an invalid value
179 Properties props = new Properties();
180 props.setProperty(SimpleLockProperties.EXPIRE_CHECK_SEC, "abc");
182 assertThatThrownBy(() -> new MyLockingFeature(props)).isInstanceOf(SimpleLockManagerException.class);
187 assertTrue(feature.isAlive());
188 verify(exsvc).scheduleWithFixedDelay(any(), anyLong(), anyLong(), any());
190 assertFalse(feature.start());
193 assertTrue(feature.start());
198 assertTrue(feature.stop());
199 assertFalse(feature.isAlive());
200 verify(future).cancel(true);
202 assertFalse(feature.stop());
204 // no more invocations
205 verify(future).cancel(anyBoolean());
209 void testShutdown() {
212 verify(future).cancel(true);
216 void testCreateLock() {
217 // this lock should be granted immediately
218 SimpleLock lock = getLock(RESOURCE, HOLD_SEC, callback);
219 assertTrue(lock.isActive());
220 assertEquals(testTime.getMillis() + HOLD_MS, lock.getHoldUntilMs());
222 verify(callback).lockAvailable(lock);
223 verify(callback, never()).lockUnavailable(lock);
225 // this time it should be busy
226 Lock lock2 = feature.createLock(RESOURCE, OWNER_KEY, HOLD_SEC, callback, false);
227 assertFalse(lock2.isActive());
228 assertTrue(lock2.isUnavailable());
230 verify(callback, never()).lockAvailable(lock2);
231 verify(callback).lockUnavailable(lock2);
233 // should have been no change to the original lock
234 assertTrue(lock.isActive());
235 verify(callback).lockAvailable(lock);
236 verify(callback, never()).lockUnavailable(lock);
238 // should work with "true" value also
239 Lock lock3 = feature.createLock(RESOURCE2, OWNER_KEY, HOLD_SEC, callback, true);
240 assertTrue(lock3.isActive());
241 verify(callback).lockAvailable(lock3);
242 verify(callback, never()).lockUnavailable(lock3);
246 * Tests createLock() when the feature is not the latest instance.
249 void testCreateLockNotLatestInstance() {
250 SimpleLockManager.setLatestInstance(null);
252 Lock lock = feature.createLock(RESOURCE, OWNER_KEY, HOLD_SEC, callback, false);
253 assertTrue(lock.isUnavailable());
254 verify(callback, never()).lockAvailable(any());
255 verify(callback).lockUnavailable(lock);
259 void testCheckExpired() throws InterruptedException {
260 final SimpleLock lock = getLock(RESOURCE, HOLD_SEC, callback);
261 final SimpleLock lock2 = getLock(RESOURCE2, HOLD_SEC, callback);
262 final SimpleLock lock3 = getLock(RESOURCE3, HOLD_SEC2, callback);
264 ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
265 verify(exsvc).scheduleWithFixedDelay(captor.capture(), anyLong(), anyLong(), any());
267 Runnable checker = captor.getValue();
269 // time unchanged - checker should have no impact
271 assertTrue(lock.isActive());
272 assertTrue(lock2.isActive());
273 assertTrue(lock3.isActive());
275 // expire the first two locks
276 testTime.sleep(HOLD_MS);
278 assertFalse(lock.isActive());
279 assertFalse(lock2.isActive());
280 assertTrue(lock3.isActive());
282 verify(callback).lockUnavailable(lock);
283 verify(callback).lockUnavailable(lock2);
284 verify(callback, never()).lockUnavailable(lock3);
286 // should be able to get a lock on the first two resources
287 assertTrue(feature.createLock(RESOURCE, OWNER_KEY, HOLD_SEC + HOLD_SEC2, callback, false).isActive());
288 assertTrue(feature.createLock(RESOURCE2, OWNER_KEY, HOLD_SEC + HOLD_SEC2, callback, false).isActive());
290 // lock is still busy on the last resource
291 assertFalse(feature.createLock(RESOURCE3, OWNER_KEY, HOLD_SEC + HOLD_SEC2, callback, false).isActive());
293 // expire the last lock
294 testTime.sleep(HOLD_MS2);
296 assertFalse(lock3.isActive());
298 verify(callback).lockUnavailable(lock3);
302 * Tests checkExpired(), where the lock is removed from the map between invoking
303 * expired() and compute(). Should cause "null" to be returned by compute().
307 void testCheckExpiredLockDeleted() {
308 feature = new MyLockingFeature() {
310 protected SimpleLock makeLock(LockState waiting, String resourceId, String ownerKey, int holdSec,
311 LockCallback callback) {
312 return new SimpleLock(waiting, resourceId, ownerKey, holdSec, callback, feature) {
314 private static final long serialVersionUID = 1L;
317 public boolean expired(long currentMs) {
318 // remove the lock from the map
328 feature.createLock(RESOURCE, OWNER_KEY, HOLD_SEC, callback, false);
330 ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
331 verify(exsvc).scheduleWithFixedDelay(captor.capture(), anyLong(), anyLong(), any());
333 Runnable checker = captor.getValue();
337 // lock should now be gone and we should be able to get another
338 feature.createLock(RESOURCE, OWNER_KEY, HOLD_SEC, callback, false);
340 // should have succeeded twice
341 verify(callback, times(2)).lockAvailable(any());
343 // lock should not be available now
344 feature.createLock(RESOURCE, OWNER_KEY, HOLD_SEC, callback, false);
345 verify(callback).lockUnavailable(any());
349 * Tests checkExpired(), where the lock is removed from the map and replaced with a
350 * new lock, between invoking expired() and compute(). Should cause the new lock to be
353 * @throws InterruptedException if the test is interrupted
356 void testCheckExpiredLockReplaced() throws InterruptedException {
357 feature = new MyLockingFeature() {
358 private boolean madeLock = false;
361 protected SimpleLock makeLock(LockState waiting, String resourceId, String ownerKey, int holdSec,
362 LockCallback callback) {
364 return new SimpleLock(waiting, resourceId, ownerKey, holdSec, callback, feature);
369 return new SimpleLock(waiting, resourceId, ownerKey, holdSec, callback, feature) {
371 private static final long serialVersionUID = 1L;
374 public boolean expired(long currentMs) {
375 // remove the lock from the map and add a new lock
377 feature.createLock(RESOURCE, OWNER_KEY, HOLD_SEC, callback, false);
387 feature.createLock(RESOURCE, OWNER_KEY, HOLD_SEC, callback, false);
389 ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
390 verify(exsvc).scheduleWithFixedDelay(captor.capture(), anyLong(), anyLong(), any());
392 Runnable checker = captor.getValue();
396 // lock should not be available now
397 feature.createLock(RESOURCE, OWNER_KEY, HOLD_SEC, callback, false);
398 verify(callback).lockUnavailable(any());
402 void testGetThreadPool() {
403 // use a real feature
404 feature = new SimpleLockManager(null, new Properties());
409 // should create thread pool
410 feature.createLock(RESOURCE, OWNER_KEY, HOLD_SEC, callback, false);
412 // should shut down thread pool
413 assertThatCode(() -> feature.stop()).doesNotThrowAnyException();
417 void testSimpleLockNoArgs() {
418 SimpleLock lock = new SimpleLock();
419 assertNull(lock.getResourceId());
420 assertNull(lock.getOwnerKey());
421 assertNull(lock.getCallback());
422 assertEquals(0, lock.getHoldSec());
424 assertEquals(0, lock.getHoldUntilMs());
428 void testSimpleLockSimpleLock() {
429 SimpleLock lock = getLock(RESOURCE, HOLD_SEC, callback);
430 assertEquals(RESOURCE, lock.getResourceId());
431 assertEquals(OWNER_KEY, lock.getOwnerKey());
432 assertSame(callback, lock.getCallback());
433 assertEquals(HOLD_SEC, lock.getHoldSec());
435 assertThatIllegalArgumentException()
436 .isThrownBy(() -> feature.createLock(RESOURCE, OWNER_KEY, -1, callback, false))
437 .withMessageContaining("holdSec");
441 void testSimpleLockSerializable() throws Exception {
442 SimpleLock lock = getLock(RESOURCE, HOLD_SEC, callback);
443 lock = roundTrip(lock);
445 assertTrue(lock.isActive());
447 assertEquals(RESOURCE, lock.getResourceId());
448 assertEquals(OWNER_KEY, lock.getOwnerKey());
449 assertNull(lock.getCallback());
450 assertEquals(HOLD_SEC, lock.getHoldSec());
454 void testSimpleLockExpired() {
455 SimpleLock lock = getLock(RESOURCE, HOLD_SEC, callback);
458 assertFalse(lock.expired(testTime.getMillis()));
459 assertFalse(lock.expired(testTime.getMillis() + HOLD_MS - 1));
460 assertTrue(lock.expired(testTime.getMillis() + HOLD_MS));
464 void testSimpleLockFree() {
465 final SimpleLock lock = getLock(RESOURCE, HOLD_SEC, callback);
467 // lock2 should be denied
468 SimpleLock lock2 = getLock(RESOURCE, HOLD_SEC, callback);
469 verify(callback, never()).lockAvailable(lock2);
470 verify(callback).lockUnavailable(lock2);
472 // lock2 was denied, so nothing new should happen when freed
473 assertFalse(lock2.free());
475 // force lock2 to be active - still nothing should happen
476 ReflectionTestUtils.setField(lock2, "state", LockState.ACTIVE);
477 assertFalse(lock2.free());
479 // now free the first lock
480 assertTrue(lock.free());
481 assertEquals(LockState.UNAVAILABLE, lock.getState());
483 // should be able to get the lock now
484 SimpleLock lock3 = getLock(RESOURCE, HOLD_SEC, callback);
485 assertTrue(lock3.isActive());
487 verify(callback).lockAvailable(lock3);
488 verify(callback, never()).lockUnavailable(lock3);
492 * Tests that free() works on a serialized lock with a new feature.
494 * @throws Exception if an error occurs
497 void testSimpleLockFreeSerialized() throws Exception {
498 SimpleLock lock = getLock(RESOURCE, HOLD_SEC, callback);
500 feature = new MyLockingFeature();
503 lock = roundTrip(lock);
504 assertTrue(lock.free());
505 assertTrue(lock.isUnavailable());
509 void testSimpleLockExtend() {
510 final SimpleLock lock = getLock(RESOURCE, HOLD_SEC, callback);
512 // lock2 should be denied
513 SimpleLock lock2 = getLock(RESOURCE, HOLD_SEC, callback);
514 verify(callback, never()).lockAvailable(lock2);
515 verify(callback).lockUnavailable(lock2);
517 // lock2 will still be denied
518 lock2.extend(HOLD_SEC, callback);
519 verify(callback, times(2)).lockUnavailable(lock2);
521 // force lock2 to be active - should still be denied
522 ReflectionTestUtils.setField(lock2, "state", LockState.ACTIVE);
523 lock2.extend(HOLD_SEC, callback);
524 verify(callback, times(3)).lockUnavailable(lock2);
526 assertThatIllegalArgumentException().isThrownBy(() -> lock.extend(-1, callback))
527 .withMessageContaining("holdSec");
529 // now extend the first lock
530 lock.extend(HOLD_SEC2, callback);
531 assertEquals(HOLD_SEC2, lock.getHoldSec());
532 assertEquals(testTime.getMillis() + HOLD_MS2, lock.getHoldUntilMs());
533 verify(callback, times(2)).lockAvailable(lock);
534 verify(callback, never()).lockUnavailable(lock);
538 * Tests that extend() works on a serialized lock with a new feature.
540 * @throws Exception if an error occurs
543 void testSimpleLockExtendSerialized() throws Exception {
544 SimpleLock lock = getLock(RESOURCE, HOLD_SEC, callback);
546 feature = new MyLockingFeature();
549 lock = roundTrip(lock);
550 LockCallback scallback = mock(LockCallback.class);
552 lock.extend(HOLD_SEC, scallback);
553 assertTrue(lock.isActive());
555 verify(scallback).lockAvailable(lock);
556 verify(scallback, never()).lockUnavailable(lock);
560 * Tests that extend() fails when there is no feature.
562 * @throws Exception if an error occurs
565 void testSimpleLockExtendNoFeature() throws Exception {
566 SimpleLock lock = getLock(RESOURCE, HOLD_SEC, callback);
568 SimpleLockManager.setLatestInstance(null);
570 lock = roundTrip(lock);
571 LockCallback scallback = mock(LockCallback.class);
573 lock.extend(HOLD_SEC, scallback);
574 assertTrue(lock.isUnavailable());
576 verify(scallback, never()).lockAvailable(lock);
577 verify(scallback).lockUnavailable(lock);
581 void testSimpleLockToString() {
582 String text = feature.createLock(RESOURCE, OWNER_KEY, HOLD_SEC, callback, false).toString();
584 assertThat(text).contains("holdUntil").doesNotContain("ownerInfo").doesNotContain("callback");
588 * Performs a multi-threaded test of the locking facility.
590 * @throws InterruptedException if the current thread is interrupted while waiting for
591 * the background threads to complete
594 void testMultiThreaded() throws InterruptedException {
595 ReflectionTestUtils.setField(SimpleLockManager.class, TIME_FIELD, testTime);
596 ReflectionTestUtils.setField(PolicyEngineConstants.getManager(), POLICY_ENGINE_EXECUTOR_FIELD, realExec);
597 feature = new SimpleLockManager(null, new Properties());
600 List<MyThread> threads = new ArrayList<>(MAX_THREADS);
601 for (int x = 0; x < MAX_THREADS; ++x) {
602 threads.add(new MyThread());
605 threads.forEach(Thread::start);
607 for (MyThread thread : threads) {
609 assertFalse(thread.isAlive());
612 for (MyThread thread : threads) {
613 if (thread.err != null) {
618 assertTrue(nsuccesses.get() > 0);
621 private SimpleLock getLock(String resource, int holdSec, LockCallback callback) {
622 return (SimpleLock) feature.createLock(resource, SimpleLockManagerTest.OWNER_KEY, holdSec, callback, false);
625 private SimpleLock roundTrip(SimpleLock lock) throws Exception {
626 ByteArrayOutputStream baos = new ByteArrayOutputStream();
627 try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
628 oos.writeObject(lock);
631 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
632 try (ObjectInputStream ois = new ObjectInputStream(bais)) {
633 return (SimpleLock) ois.readObject();
638 * Feature that uses <i>exsvc</i> to execute requests.
640 private class MyLockingFeature extends SimpleLockManager {
642 public MyLockingFeature() {
643 this(new Properties());
646 public MyLockingFeature(Properties props) {
649 exsvc = mock(ScheduledExecutorService.class);
650 ReflectionTestUtils.setField(PolicyEngineConstants.getManager(), POLICY_ENGINE_EXECUTOR_FIELD, exsvc);
652 when(exsvc.scheduleWithFixedDelay(any(), anyLong(), anyLong(), any())).thenAnswer(answer -> {
659 * Thread used with the multithreaded test. It repeatedly attempts to get a lock,
660 * extend it, and then unlock it.
662 private class MyThread extends Thread {
663 AssertionError err = null;
672 for (int x = 0; x < MAX_LOOPS; ++x) {
676 } catch (AssertionError e) {
681 private void makeAttempt() {
683 Semaphore sem = new Semaphore(0);
685 LockCallback cb = new LockCallback() {
687 public void lockAvailable(Lock lock) {
692 public void lockUnavailable(Lock lock) {
697 Lock lock = feature.createLock(RESOURCE, getName(), HOLD_SEC, cb, false);
699 // wait for callback, whether available or unavailable
700 assertTrue(sem.tryAcquire(5, TimeUnit.SECONDS));
701 if (!lock.isActive()) {
705 nsuccesses.incrementAndGet();
707 assertEquals(1, nactive.incrementAndGet());
709 lock.extend(HOLD_SEC2, cb);
710 assertTrue(sem.tryAcquire(5, TimeUnit.SECONDS));
711 assertTrue(lock.isActive());
713 // decrement BEFORE free()
714 nactive.decrementAndGet();
716 assertTrue(lock.free());
717 assertTrue(lock.isUnavailable());
719 } catch (InterruptedException e) {
720 Thread.currentThread().interrupt();
721 throw new AssertionError("interrupted", e);