From 3a80de9806cbdd0461716e814f5e674e259d42b3 Mon Sep 17 00:00:00 2001 From: Jim Hahn Date: Thu, 29 Mar 2018 15:07:36 -0400 Subject: Add api-resource-locks feature Added ResourceLockFeatureAPI, to provide an interface for PolicyGuard to use to access other locking mechanisms. Move lock API to policy-core. Changed the interface and added Callback support. Added a class to invoke the API implementers. Changed to set a future instead of expecting the callback to implement the future interface. Also refactored, creating "lock" sub-package. Add junit tests. Increase junit coverage. IsLocked & isLockedBy should check all arguments. Make ImplFuture protected instead of public. Simplify locking - move callback support to an optional feature. Move Reference class to policy-utils. Return plain Future instead of LockRequestFuture. Change-Id: I65154d33833914b4332e02d1ef2512f4c5f09b16 Issue-ID: POLICY-577 Signed-off-by: Jim Hahn --- policy-core/pom.xml | 6 + .../org/onap/policy/drools/core/lock/Lock.java | 162 +++++++ .../policy/drools/core/lock/LockRequestFuture.java | 261 +++++++++++ .../core/lock/PolicyResourceLockFeatureAPI.java | 190 ++++++++ .../core/lock/PolicyResourceLockManager.java | 208 +++++++++ .../policy/drools/core/lock/SimpleLockManager.java | 168 +++++++ .../drools/core/lock/LockRequestFutureTest.java | 431 +++++++++++++++++ .../org/onap/policy/drools/core/lock/LockTest.java | 131 ++++++ .../lock/PolicyResourceLockFeatureAPITest.java | 76 +++ .../core/lock/PolicyResourceLockManagerTest.java | 509 +++++++++++++++++++++ .../drools/core/lock/SimpleLockManagerTest.java | 280 ++++++++++++ .../onap/policy/drools/core/lock/TestUtils.java | 59 +++ .../org/onap/policy/drools/utils/Reference.java | 72 +++ .../onap/policy/drools/utils/ReferenceTest.java | 84 ++++ 14 files changed, 2637 insertions(+) create mode 100644 policy-core/src/main/java/org/onap/policy/drools/core/lock/Lock.java create mode 100644 policy-core/src/main/java/org/onap/policy/drools/core/lock/LockRequestFuture.java create mode 100644 policy-core/src/main/java/org/onap/policy/drools/core/lock/PolicyResourceLockFeatureAPI.java create mode 100644 policy-core/src/main/java/org/onap/policy/drools/core/lock/PolicyResourceLockManager.java create mode 100644 policy-core/src/main/java/org/onap/policy/drools/core/lock/SimpleLockManager.java create mode 100644 policy-core/src/test/java/org/onap/policy/drools/core/lock/LockRequestFutureTest.java create mode 100644 policy-core/src/test/java/org/onap/policy/drools/core/lock/LockTest.java create mode 100644 policy-core/src/test/java/org/onap/policy/drools/core/lock/PolicyResourceLockFeatureAPITest.java create mode 100644 policy-core/src/test/java/org/onap/policy/drools/core/lock/PolicyResourceLockManagerTest.java create mode 100644 policy-core/src/test/java/org/onap/policy/drools/core/lock/SimpleLockManagerTest.java create mode 100644 policy-core/src/test/java/org/onap/policy/drools/core/lock/TestUtils.java create mode 100644 policy-utils/src/main/java/org/onap/policy/drools/utils/Reference.java create mode 100644 policy-utils/src/test/java/org/onap/policy/drools/utils/ReferenceTest.java diff --git a/policy-core/pom.xml b/policy-core/pom.xml index 8cecd362..ecd5b19e 100644 --- a/policy-core/pom.xml +++ b/policy-core/pom.xml @@ -94,5 +94,11 @@ junit test + + org.mockito + mockito-core + 2.13.0 + test + diff --git a/policy-core/src/main/java/org/onap/policy/drools/core/lock/Lock.java b/policy-core/src/main/java/org/onap/policy/drools/core/lock/Lock.java new file mode 100644 index 00000000..ea5e2521 --- /dev/null +++ b/policy-core/src/main/java/org/onap/policy/drools/core/lock/Lock.java @@ -0,0 +1,162 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.core.lock; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map.Entry; +import org.onap.policy.drools.utils.Pair; + +/** + * Lock that is held for a resource. This not only identifies the current owner of the + * lock, but it also includes a queue of requesters. An item is associated with each + * requester that is waiting in the queue. Note: this class is not thread-safe. + * + * @param type of item to be associated with a request + */ +public class Lock { + + /** + * Result returned by removeRequester(). + */ + public enum RemoveResult { + /** + * The requester was the owner of the lock, and the lock is no longer needed, + * because there were no other requesters waiting to get the lock. + */ + UNLOCKED, + + /** + * The requester was the owner of the lock, and has been replaced with the next + * requester waiting in the queue. + */ + RELOCKED, + + /** + * The requester had been waiting in the queue, and has now been removed. + */ + REMOVED, + + /** + * The requester was not the owner, nor was it waiting in the queue. + */ + NOT_FOUND + }; + + /** + * The last owner to grab the lock, never {@code null}. + */ + private String owner; + + /** + * Requesters waiting to get the lock. Maps the requester (i.e., owner for which the + * request is being made) to its associated item. Uses a Linked map so that the order + * of the requesters is maintained. We don't expect many requesters for any given + * lock, thus we'll start with a small hash size. + */ + private LinkedHashMap requester2item = new LinkedHashMap<>(5); + + /** + * + * @param owner the current owner of this lock + */ + public Lock(String owner) { + this.owner = owner; + } + + /** + * + * @return the current owner of the lock, or the last owner of the lock, if the lock + * is not currently owned. (This will never be {@code null}.) + */ + public String getOwner() { + return owner; + } + + /** + * Adds a new requester to the queue of requesters. + * + * @param requester + * @param item to be associated with the requester, must not be {@code null} + * @return {@code true} if the requester was added, {@code false} if it already owns + * the lock or is already in the queue + * @throws IllegalArgumentException if the item is null + */ + public boolean add(String requester, T item) { + if (item == null) { + throw LockRequestFuture.makeNullArgException("lock requester item is null"); + } + + if (requester.equals(owner)) { + // requester already owns the lock + return false; + } + + T prev = requester2item.putIfAbsent(requester, item); + + // if there's a previous value, then that means this requester is already + // waiting for a lock on this resource. In that case, we return false + return (prev == null); + } + + /** + * Removes a requester from the lock. The requester may currently own the lock, or it + * may be in the queue waiting for the lock. Note: as this is agnostic to the type of + * item associated with the requester, it is unable to notify the new owner that it's + * the new owner; that is left up to the code that invokes this method. + * + * @param requester + * @param newOwner the new owner info is placed here, if the result is RELOCKED + * @return the result + */ + public RemoveResult removeRequester(String requester, Pair newOwner) { + + if (!requester.equals(owner)) { + // requester does not currently own the lock - remove it from the + // queue + T ent = requester2item.remove(requester); + + // if there was an entry in the queue, then return true to indicate + // that it was removed. Otherwise, return false + return (ent != null ? RemoveResult.REMOVED : RemoveResult.NOT_FOUND); + } + + /* + * requester was the owner - find something to take over + */ + Iterator> it = requester2item.entrySet().iterator(); + if (!it.hasNext()) { + // no one to take over the lock - it's now unlocked + return RemoveResult.UNLOCKED; + } + + // there's another requester to take over + Entry ent = it.next(); + it.remove(); + + owner = ent.getKey(); + + newOwner.first(owner); + newOwner.second(ent.getValue()); + + return RemoveResult.RELOCKED; + } +} diff --git a/policy-core/src/main/java/org/onap/policy/drools/core/lock/LockRequestFuture.java b/policy-core/src/main/java/org/onap/policy/drools/core/lock/LockRequestFuture.java new file mode 100644 index 00000000..46d1ff2d --- /dev/null +++ b/policy-core/src/main/java/org/onap/policy/drools/core/lock/LockRequestFuture.java @@ -0,0 +1,261 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.core.lock; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; +import org.onap.policy.drools.core.lock.PolicyResourceLockFeatureAPI.Callback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Future associated with a lock request. + */ +public class LockRequestFuture implements Future { + + // messages used in exceptions + public static final String MSG_NULL_RESOURCE_ID = "null resourceId"; + public static final String MSG_NULL_OWNER = "null owner"; + + private static Logger logger = LoggerFactory.getLogger(LockRequestFuture.class); + + /** + * The resource on which the lock was requested. + */ + private final String resourceId; + + /** + * The owner for which the lock was requested. + */ + private final String owner; + + /** + * Possible states for this future. + */ + private enum State { + WAITING, CANCELLED, ACQUIRED, DENIED + }; + + private AtomicReference state; + + /** + * Used to wait for the lock request to complete. + */ + private CountDownLatch waiter = new CountDownLatch(1); + + /** + * Callback to invoke once the lock is acquired (or denied). This is set to + * {@code null} once the callback has been invoked. + */ + private final AtomicReference callback; + + /** + * Constructs a future that has already been completed. + * + * @param resourceId + * @param owner owner for which the lock was requested + * @param locked {@code true} if the lock has been acquired, {@code false} if the lock + * request has been denied + * @throws IllegalArgumentException if any of the arguments are {@code null} + */ + public LockRequestFuture(String resourceId, String owner, boolean locked) { + if (resourceId == null) { + throw makeNullArgException(MSG_NULL_RESOURCE_ID); + } + + if (owner == null) { + throw makeNullArgException(MSG_NULL_OWNER); + } + + this.resourceId = resourceId; + this.owner = owner; + this.callback = new AtomicReference<>(null); + this.state = new AtomicReference<>(locked ? State.ACQUIRED : State.DENIED); + + // indicate that it's already done + this.waiter.countDown(); + } + + /** + * Constructs a future that has not yet been completed. + * + * @param resourceId + * @param owner owner for which the lock was requested + * @param callback item to be wrapped + * @throws IllegalArgumentException if the resourceId or owner is {@code null} + */ + public LockRequestFuture(String resourceId, String owner, Callback callback) { + if (resourceId == null) { + throw makeNullArgException(MSG_NULL_RESOURCE_ID); + } + + if (owner == null) { + throw makeNullArgException(MSG_NULL_OWNER); + } + + this.resourceId = resourceId; + this.owner = owner; + this.callback = new AtomicReference<>(callback); + this.state = new AtomicReference<>(State.WAITING); + } + + public String getResourceId() { + return resourceId; + } + + public String getOwner() { + return owner; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + boolean cancelled = state.compareAndSet(State.WAITING, State.CANCELLED); + + if (cancelled) { + logger.info("resource {} owner {} cancelled lock request", resourceId, owner); + waiter.countDown(); + } + + return cancelled; + } + + /** + * Indicates that the lock has been acquired or denied. + * + * @param locked {@code true} if the lock has been acquired, {@code false} if the lock + * request has been denied + * + * @return {@code true} if it was not already completed, {@code false} otherwise + */ + protected boolean setLocked(boolean locked) { + State newState = (locked ? State.ACQUIRED : State.DENIED); + if (state.compareAndSet(State.WAITING, newState)) { + waiter.countDown(); + return true; + + } else { + return false; + } + } + + @Override + public boolean isCancelled() { + return (state.get() == State.CANCELLED); + } + + @Override + public boolean isDone() { + return (state.get() != State.WAITING); + } + + /** + * Gets the current status of the lock. + * + * @return {@code true} if the lock has been acquired, {@code false} otherwise + */ + public boolean isLocked() { + return (state.get() == State.ACQUIRED); + } + + /** + * @return {@code true} if the lock was acquired, {@code false} if it was denied + */ + @Override + public Boolean get() throws CancellationException, InterruptedException { + waiter.await(); + + switch (state.get()) { + case CANCELLED: + throw new CancellationException("lock request was cancelled"); + case ACQUIRED: + return true; + default: + // should only be DENIED at this point + return false; + } + } + + /** + * @return {@code true} if the lock was acquired, {@code false} if it was denied + */ + @Override + public Boolean get(long timeout, TimeUnit unit) + throws CancellationException, InterruptedException, TimeoutException { + + if (!waiter.await(timeout, unit)) { + throw new TimeoutException("lock request did not complete in time"); + } + + return get(); + } + + /** + * Invokes the callback, indicating whether or not the lock was acquired. + * + * @throws IllegalStateException if the request was previously cancelled, has not yet + * completed, or if the callback has already been invoked + */ + protected void invokeCallback() { + boolean locked; + + switch (state.get()) { + case ACQUIRED: + locked = true; + break; + case DENIED: + locked = false; + break; + case CANCELLED: + throw new IllegalStateException("cancelled lock request callback"); + default: + // only other choice is WAITING + throw new IllegalStateException("incomplete lock request callback"); + } + + Callback cb = callback.get(); + if (cb == null || !callback.compareAndSet(cb, null)) { + throw new IllegalStateException("already invoked lock request callback"); + } + + + // notify the callback + try { + cb.set(locked); + + } catch (RuntimeException e) { + logger.info("lock request callback for resource {} owner {} threw an exception", resourceId, owner, e); + } + } + + /** + * Makes an exception for when an argument is {@code null}. + * + * @param msg exception message + * @return a new Exception + */ + public static IllegalArgumentException makeNullArgException(String msg) { + return new IllegalArgumentException(msg); + } +} diff --git a/policy-core/src/main/java/org/onap/policy/drools/core/lock/PolicyResourceLockFeatureAPI.java b/policy-core/src/main/java/org/onap/policy/drools/core/lock/PolicyResourceLockFeatureAPI.java new file mode 100644 index 00000000..718ed5e9 --- /dev/null +++ b/policy-core/src/main/java/org/onap/policy/drools/core/lock/PolicyResourceLockFeatureAPI.java @@ -0,0 +1,190 @@ +/* + * ============LICENSE_START======================================================= + * api-resource-locks + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.core.lock; + +import java.util.concurrent.Future; +import org.onap.policy.drools.utils.OrderedService; +import org.onap.policy.drools.utils.OrderedServiceImpl; + +/** + * Resource locks. Each lock has an "owner", which is intended to be unique across a + * single instance of a running PolicyEngine. + *

+ * This interface provides a way to invoke optional features at various points in the + * code. At appropriate points in the application, the code iterates through this list, + * invoking these optional methods. + *

+ * Implementers may choose to implement a level of locking appropriate to the application. + * For instance, they may choose to implement an engine-wide locking scheme, or they may + * choose to implement a global locking scheme (e.g., through a shared DB). + */ +public interface PolicyResourceLockFeatureAPI extends OrderedService { + + /** + * 'FeatureAPI.impl.getList()' returns an ordered list of objects implementing the + * 'FeatureAPI' interface. + */ + public static OrderedServiceImpl impl = + new OrderedServiceImpl<>(PolicyResourceLockFeatureAPI.class); + + /** + * Callback that an implementer invokes when a lock is acquired (or denied), + * asynchronously. The implementer invokes the method to indicate that the lock was + * acquired (or denied). + */ + @FunctionalInterface + public static interface Callback { + + /** + * + * @param locked {@code true} if the lock was acquired, {@code false} if the lock + * was denied + */ + public void set(boolean locked); + } + + /** + * This method is called before a lock is acquired on a resource. If a callback is + * provided, and the implementer is unable to acquire the lock immediately, then the + * implementer will invoke the callback once the lock is acquired. If the implementer + * handled the request, then it will return a future, which may be in one of three + * states: + *

+ *
isDone()=true and get()=true
+ *
the lock has been acquired; the callback may or may not have been invoked
+ *
isDone()=true and get()=false
+ *
the lock request has been denied; the callback may or may not have been + * invoked
+ *
isDone()=false
+ *
the lock was not immediately available and a callback was provided. The + * callback will be invoked once the lock is acquired (or denied). In this case, the + * future may be used to cancel the request
+ *
+ * + * @param resourceId + * @param owner + * @param callback function to invoke, if the requester wishes to wait for the lock to + * come available, {@code null} to provide immediate replies + * @return a future for the lock, if the implementer handled the request, {@code null} + * if additional locking logic should be performed + * @throws IllegalStateException if the owner already holds the lock or is already in + * the queue to get the lock + */ + public default Future beforeLock(String resourceId, String owner, Callback callback) { + return null; + } + + /** + * This method is called after a lock for a resource has been acquired or denied. This + * may be invoked immediately, if the status can be determined immediately, or it may + * be invoked asynchronously, once the status has been determined. + * + * @param resourceId + * @param owner + * @param locked {@code true} if the lock was acquired, {@code false} if it was denied + * @return {@code true} if the implementer handled the request, {@code false} + * otherwise + */ + public default boolean afterLock(String resourceId, String owner, boolean locked) { + return false; + } + + /** + * This method is called before a lock on a resource is released. + * + * @param resourceId + * @param owner + *
true
+ *
the implementer handled the request and found the resource to be locked + * by the given owner; the resource was unlocked and no additional locking + * logic should be performed
+ *
false
+ *
the implementer handled the request and found the resource was not + * locked by given the owner; no additional locking logic should be + * performed
+ *
null
+ *
the implementer did not handle the request; additional locking logic + * should be performed + * + */ + public default Boolean beforeUnlock(String resourceId, String owner) { + return null; + } + + /** + * This method is called after a lock on a resource is released. + * + * @param resourceId + * @param owner + * @param unlocked {@code true} if the lock was released, {@code false} if the owner + * did not have a lock on the resource + * @return {@code true} if the implementer handled the request, {@code false} + * otherwise + */ + public default boolean afterUnlock(String resourceId, String owner, boolean unlocked) { + return false; + } + + /** + * This method is called before a check is made to determine if a resource is locked. + * + * @param resourceId + * @return + *
+ *
true
+ *
the implementer handled the request and found the resource to be + * locked; no additional locking logic should be performed
+ *
false
+ *
the implementer handled the request and found the resource was not + * locked; no additional locking logic should be performed
+ *
null
+ *
the implementer did not handle the request; additional locking logic + * should be performed + *
+ */ + public default Boolean beforeIsLocked(String resourceId) { + return null; + } + + /** + * This method is called before a check is made to determine if a particular owner + * holds the lock on a resource. + * + * @param resourceId + * @param owner + * @return + *
+ *
true
+ *
the implementer handled the request and found the resource to be locked + * by the given owner; no additional locking logic should be performed
+ *
false
+ *
the implementer handled the request and found the resource was not + * locked by given the owner; no additional locking logic should be + * performed
+ *
null
+ *
the implementer did not handle the request; additional locking logic + * should be performed + *
+ */ + public default Boolean beforeIsLockedBy(String resourceId, String owner) { + return null; + } +} diff --git a/policy-core/src/main/java/org/onap/policy/drools/core/lock/PolicyResourceLockManager.java b/policy-core/src/main/java/org/onap/policy/drools/core/lock/PolicyResourceLockManager.java new file mode 100644 index 00000000..d51f2b91 --- /dev/null +++ b/policy-core/src/main/java/org/onap/policy/drools/core/lock/PolicyResourceLockManager.java @@ -0,0 +1,208 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.core.lock; + +import static org.onap.policy.drools.core.lock.LockRequestFuture.MSG_NULL_OWNER; +import static org.onap.policy.drools.core.lock.LockRequestFuture.MSG_NULL_RESOURCE_ID; +import static org.onap.policy.drools.core.lock.LockRequestFuture.makeNullArgException; +import java.util.List; +import java.util.concurrent.Future; +import java.util.function.Function; +import org.onap.policy.drools.core.lock.PolicyResourceLockFeatureAPI.Callback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manager of resource locks. Checks for API implementers. + */ +public class PolicyResourceLockManager extends SimpleLockManager { + + private static Logger logger = LoggerFactory.getLogger(PolicyResourceLockManager.class); + + /** + * Used to access various objects. + */ + public static Factory factory = new Factory(); + + /** + * Used by junit tests. + */ + protected PolicyResourceLockManager() { + super(); + } + + /** + * + * @return the manager singleton + */ + public static PolicyResourceLockManager getInstance() { + return Singleton.instance; + } + + protected static Factory getFactory() { + return factory; + } + + /** + * Sets the factory to be used by junit tests. + * + * @param factory + */ + protected static void setFactory(Factory factory) { + PolicyResourceLockManager.factory = factory; + } + + @Override + public Future lock(String resourceId, String owner, Callback callback) { + if (resourceId == null) { + throw makeNullArgException(MSG_NULL_RESOURCE_ID); + } + + if (owner == null) { + throw makeNullArgException(MSG_NULL_OWNER); + } + + Future result = doIntercept(null, impl -> impl.beforeLock(resourceId, owner, callback)); + if (result != null) { + return result; + } + + // implementer didn't do the work - use superclass + result = super.lock(resourceId, owner, callback); + + boolean locked = ((LockRequestFuture) result).isLocked(); + + doIntercept(false, impl -> impl.afterLock(resourceId, owner, locked)); + + return result; + } + + @Override + public boolean unlock(String resourceId, String owner) { + if (resourceId == null) { + throw makeNullArgException(MSG_NULL_RESOURCE_ID); + } + + if (owner == null) { + throw makeNullArgException(MSG_NULL_OWNER); + } + + Boolean result = doIntercept(null, impl -> impl.beforeUnlock(resourceId, owner)); + if (result != null) { + return result; + } + + // implementer didn't do the work - use superclass + boolean unlocked = super.unlock(resourceId, owner); + + doIntercept(false, impl -> impl.afterUnlock(resourceId, owner, unlocked)); + + return unlocked; + } + + /** + * + * @throws IllegalArgumentException if the resourceId is {@code null} + */ + @Override + public boolean isLocked(String resourceId) { + if (resourceId == null) { + throw makeNullArgException(MSG_NULL_RESOURCE_ID); + } + + Boolean result = doIntercept(null, impl -> impl.beforeIsLocked(resourceId)); + if (result != null) { + return result; + } + + return super.isLocked(resourceId); + } + + /** + * + * @throws IllegalArgumentException if the resourceId or owner is {@code null} + */ + @Override + public boolean isLockedBy(String resourceId, String owner) { + if (resourceId == null) { + throw makeNullArgException(MSG_NULL_RESOURCE_ID); + } + + if (owner == null) { + throw makeNullArgException(MSG_NULL_OWNER); + } + + Boolean result = doIntercept(null, impl -> impl.beforeIsLockedBy(resourceId, owner)); + if (result != null) { + return result; + } + + return super.isLockedBy(resourceId, owner); + } + + /** + * Applies a function to each implementer of the lock feature. Returns as soon as one + * of them returns a non-null value. + * + * @param continueValue if the implementer returns this value, then it continues to + * check addition implementers + * @param func function to be applied to the implementers + * @return first non-null value returned by an implementer, continueValue if + * they all returned continueValue + */ + public static T doIntercept(T continueValue, Function func) { + + for (PolicyResourceLockFeatureAPI impl : factory.getImplementers()) { + try { + T result = func.apply(impl); + if (result != continueValue) { + return result; + } + + } catch (RuntimeException e) { + logger.warn("lock feature {} threw an exception", impl, e); + } + } + + return continueValue; + } + + /** + * Initialization-on-demand holder idiom. + */ + private static class Singleton { + private static final PolicyResourceLockManager instance = new PolicyResourceLockManager(); + } + + /** + * Used to access various objects. + */ + public static class Factory { + + /** + * + * @return the list of feature implementers + */ + public List getImplementers() { + return PolicyResourceLockFeatureAPI.impl.getList(); + } + } +} diff --git a/policy-core/src/main/java/org/onap/policy/drools/core/lock/SimpleLockManager.java b/policy-core/src/main/java/org/onap/policy/drools/core/lock/SimpleLockManager.java new file mode 100644 index 00000000..14cffaab --- /dev/null +++ b/policy-core/src/main/java/org/onap/policy/drools/core/lock/SimpleLockManager.java @@ -0,0 +1,168 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.core.lock; + +import static org.onap.policy.drools.core.lock.LockRequestFuture.MSG_NULL_OWNER; +import static org.onap.policy.drools.core.lock.LockRequestFuture.MSG_NULL_RESOURCE_ID; +import static org.onap.policy.drools.core.lock.LockRequestFuture.makeNullArgException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Future; +import org.onap.policy.drools.core.lock.PolicyResourceLockFeatureAPI.Callback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Simple lock manager. Callbacks are ignored. Does not redirect to lock feature + * implementers. + */ +public class SimpleLockManager { + + protected static Logger logger = LoggerFactory.getLogger(SimpleLockManager.class); + + /** + * Maps a resource to the owner that holds the lock on it. + */ + private ConcurrentHashMap resource2owner = new ConcurrentHashMap<>(); + + /** + * + */ + public SimpleLockManager() { + super(); + } + + // TODO: for ease of use by clients, should we always invoke the callback, even though + // this is synchronous? + + /** + * Attempts to lock a resource. This method ignores the callback and always returns a + * {@link CompletedLockRequest}. + * + * @param resourceId + * @param owner + * @param callback function to invoke, if the requester wishes to wait for the lock to + * be acquired, {@code null} to provide immediate replies + * @return a future for the lock request. The future will be in one of three states: + *
+ *
isDone()=true and get()=true
+ *
the lock has been acquired; the callback may or may not have been + * invoked
+ *
isDone()=true and get()=false
+ *
the lock request has been denied; the callback may or may not have been + * invoked
+ *
isDone()=false
+ *
the lock was not immediately available and a callback was provided. The + * callback will be invoked once the lock is acquired (or denied). In this + * case, the future may be used to cancel the request
+ *
+ * @throws IllegalArgumentException if the resourceId or owner is {@code null} + * @throws IllegalStateException if the owner already holds the lock or is already in + * the queue to get the lock + */ + public Future lock(String resourceId, String owner, Callback callback) { + + if (resourceId == null) { + throw makeNullArgException(MSG_NULL_RESOURCE_ID); + } + + if (owner == null) { + throw makeNullArgException(MSG_NULL_OWNER); + } + + boolean locked = (resource2owner.putIfAbsent(resourceId, owner) == null); + + if (!locked && owner.equals(resource2owner.get(resourceId))) { + throw new IllegalStateException("lock for resource " + resourceId + " already owned by " + owner); + } + + logger.info("lock {} for resource {} owner {}", locked, resourceId, owner); + + return new LockRequestFuture(resourceId, owner, locked); + } + + /** + * Unlocks a resource. + * + * @param resourceId + * @param owner + * @return {@code true} if unlocked, {@code false} if the given owner does not + * currently hold a lock on the resource + * @throws IllegalArgumentException if the resourceId or owner is {@code null} + */ + public boolean unlock(String resourceId, String owner) { + if (resourceId == null) { + throw makeNullArgException(MSG_NULL_RESOURCE_ID); + } + + if (owner == null) { + throw makeNullArgException(MSG_NULL_OWNER); + } + + boolean unlocked = resource2owner.remove(resourceId, owner); + logger.info("unlock resource {} owner {} = {}", resourceId, owner, unlocked); + + return unlocked; + } + + /** + * Determines if a resource is locked by anyone. + * + * @param resourceId + * @return {@code true} if the resource is locked, {@code false} otherwise + * @throws IllegalArgumentException if the resourceId is {@code null} + */ + public boolean isLocked(String resourceId) { + + if (resourceId == null) { + throw makeNullArgException(MSG_NULL_RESOURCE_ID); + } + + boolean locked = resource2owner.containsKey(resourceId); + + logger.debug("resource {} isLocked = {}", resourceId, locked); + + return locked; + } + + /** + * Determines if a resource is locked by a particular owner. + * + * @param resourceId + * @param owner + * @return {@code true} if the resource is locked, {@code false} otherwise + * @throws IllegalArgumentException if the resourceId or owner is {@code null} + */ + public boolean isLockedBy(String resourceId, String owner) { + + if (resourceId == null) { + throw makeNullArgException(MSG_NULL_RESOURCE_ID); + } + + if (owner == null) { + throw makeNullArgException(MSG_NULL_OWNER); + } + + boolean locked = owner.equals(resource2owner.get(resourceId)); + logger.debug("resource {} isLockedBy {} = {}", resourceId, owner, locked); + + return locked; + } +} diff --git a/policy-core/src/test/java/org/onap/policy/drools/core/lock/LockRequestFutureTest.java b/policy-core/src/test/java/org/onap/policy/drools/core/lock/LockRequestFutureTest.java new file mode 100644 index 00000000..883778eb --- /dev/null +++ b/policy-core/src/test/java/org/onap/policy/drools/core/lock/LockRequestFutureTest.java @@ -0,0 +1,431 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.core.lock; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.onap.policy.drools.core.lock.TestUtils.expectException; +import java.util.concurrent.CancellationException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.Before; +import org.junit.Test; +import org.onap.policy.drools.core.lock.PolicyResourceLockFeatureAPI.Callback; + +public class LockRequestFutureTest { + + private static final int WAIT_SEC = 1; + private static final String RESOURCE = "my.resource"; + private static final String OWNER = "my.owner"; + private static final String EXPECTED_EXCEPTION = "expected exception"; + + private Callback callback; + private LockRequestFuture fut; + + @Before + public void setUp() { + callback = mock(Callback.class); + fut = new LockRequestFuture(RESOURCE, OWNER, callback); + } + + @Test + public void testLockRequestFutureStringStringBoolean_False() throws Exception { + fut = new LockRequestFuture(RESOURCE, OWNER, false); + + assertTrue(fut.isDone()); + assertEquals(RESOURCE, fut.getResourceId()); + assertEquals(OWNER, fut.getOwner()); + + assertFalse(fut.isLocked()); + assertFalse(fut.get(0, TimeUnit.SECONDS)); + } + + @Test + public void testLockRequestFutureStringStringBoolean_True() throws Exception { + fut = new LockRequestFuture(RESOURCE, OWNER, true); + + assertTrue(fut.isDone()); + assertEquals(RESOURCE, fut.getResourceId()); + assertEquals(OWNER, fut.getOwner()); + + assertTrue(fut.isLocked()); + assertTrue(fut.get(0, TimeUnit.SECONDS)); + } + + @Test + public void testLockRequestFutureStringStringBoolean_ArgEx() throws Exception { + + // null resource id + IllegalArgumentException ex = expectException(IllegalArgumentException.class, + xxx -> new LockRequestFuture(null, OWNER, true)); + assertEquals("null resourceId", ex.getMessage()); + + + // null owner + ex = expectException(IllegalArgumentException.class, xxx -> new LockRequestFuture(RESOURCE, null, true)); + assertEquals("null owner", ex.getMessage()); + } + + @Test + public void testLockRequestFutureStringStringCallback() throws Exception { + assertFalse(fut.isDone()); + assertEquals(RESOURCE, fut.getResourceId()); + assertEquals(OWNER, fut.getOwner()); + + fut.setLocked(true); + fut.invokeCallback(); + + // ensure it invoked the callback + verify(callback).set(true); + } + + @Test + public void testLockRequestFutureStringStringCallback_ArgEx() throws Exception { + + // null resource id + IllegalArgumentException ex = expectException(IllegalArgumentException.class, + xxx -> new LockRequestFuture(null, OWNER, callback)); + assertEquals("null resourceId", ex.getMessage()); + + + // null owner + ex = expectException(IllegalArgumentException.class, xxx -> new LockRequestFuture(RESOURCE, null, callback)); + assertEquals("null owner", ex.getMessage()); + + + // null callback is OK + new LockRequestFuture(RESOURCE, OWNER, null); + } + + @Test + public void testGetResourceId() { + assertEquals(RESOURCE, fut.getResourceId()); + } + + @Test + public void testGetOwner() { + assertEquals(OWNER, fut.getOwner()); + } + + @Test + public void testCancel() throws Exception { + // not cancelled yet + assertFalse(fut.isDone()); + + // cancel it + assertTrue(fut.cancel(false)); + assertTrue(fut.isDone()); + + // should not block now + expectException(CancellationException.class, xxx -> fut.get(0, TimeUnit.SECONDS)); + + } + + @Test + public void testCancel_AlreadyCancelled() throws Exception { + + fut.cancel(true); + + assertFalse(fut.cancel(true)); + assertTrue(fut.isDone()); + + expectException(CancellationException.class, xxx -> fut.get(0, TimeUnit.SECONDS)); + } + + @Test + public void testCancel_AlreadyAcquired() throws Exception { + + fut.setLocked(true); + + assertFalse(fut.cancel(true)); + assertTrue(fut.isDone()); + assertTrue(fut.get(0, TimeUnit.SECONDS)); + } + + @Test + public void testCancel_AlreadyDenied() throws Exception { + + fut.setLocked(false); + + assertFalse(fut.cancel(true)); + assertTrue(fut.isDone()); + assertFalse(fut.get(0, TimeUnit.SECONDS)); + } + + @Test + public void testSetLocked_True() throws Exception { + assertTrue(fut.setLocked(true)); + + assertTrue(fut.isDone()); + assertTrue(fut.get(0, TimeUnit.SECONDS)); + } + + @Test + public void testSetLocked_False() throws Exception { + assertTrue(fut.setLocked(false)); + + assertTrue(fut.isDone()); + assertFalse(fut.get(0, TimeUnit.SECONDS)); + } + + @Test + public void testSetLocked_AlreadyCancelled() { + + fut.cancel(true); + + assertFalse(fut.setLocked(true)); + assertFalse(fut.setLocked(false)); + + assertTrue(fut.isDone()); + expectException(CancellationException.class, xxx -> fut.get(0, TimeUnit.SECONDS)); + } + + @Test + public void testSetLocked_AlreadyAcquired() throws Exception { + fut.setLocked(true); + + assertTrue(fut.isDone()); + assertTrue(fut.get(0, TimeUnit.SECONDS)); + + assertFalse(fut.cancel(true)); + assertFalse(fut.setLocked(true)); + assertFalse(fut.setLocked(false)); + } + + @Test + public void testSetLocked_AlreadyDenied() throws Exception { + fut.setLocked(false); + + assertTrue(fut.isDone()); + assertFalse(fut.get(0, TimeUnit.SECONDS)); + + assertFalse(fut.cancel(true)); + assertFalse(fut.setLocked(true)); + assertFalse(fut.setLocked(false)); + } + + @Test + public void testIsCancelled() { + assertFalse(fut.isCancelled()); + + fut.cancel(false); + assertTrue(fut.isCancelled()); + } + + @Test + public void testIsCancelled_Acquired() { + fut.setLocked(true); + assertFalse(fut.isCancelled()); + } + + @Test + public void testIsCancelled_Denied() { + fut.setLocked(false); + assertFalse(fut.isCancelled()); + } + + @Test + public void testIsDone_Cancelled() { + fut.cancel(false); + assertTrue(fut.isDone()); + } + + @Test + public void testIsDone_Acquired() { + fut.setLocked(true); + assertTrue(fut.isDone()); + } + + @Test + public void testIsDone_Denied() { + fut.setLocked(false); + assertTrue(fut.isDone()); + } + + @Test + public void testIsDone_Waiting() { + assertFalse(fut.isDone()); + } + + @Test + public void testIsLocked_Cancelled() { + fut.cancel(false); + assertFalse(fut.isLocked()); + } + + @Test + public void testIsLocked_Acquired() { + fut.setLocked(true); + assertTrue(fut.isLocked()); + } + + @Test + public void testIsLocked_Denied() { + fut.setLocked(false); + assertFalse(fut.isLocked()); + } + + @Test + public void testIsLocked_Waiting() { + assertFalse(fut.isLocked()); + } + + @Test + public void testGet_Cancelled() throws Exception { + new Thread() { + @Override + public void run() { + fut.cancel(false); + } + }.start(); + + expectException(CancellationException.class, xxx -> fut.get()); + } + + @Test + public void testGet_Acquired() throws Exception { + new Thread() { + @Override + public void run() { + fut.setLocked(true); + } + }.start(); + + assertTrue(fut.get()); + } + + @Test + public void testGet_Denied() throws Exception { + new Thread() { + @Override + public void run() { + fut.setLocked(false); + } + }.start(); + + assertFalse(fut.get()); + } + + @Test + public void testGetLongTimeUnit() throws Exception { + expectException(TimeoutException.class, xxx -> fut.get(0, TimeUnit.SECONDS)); + + fut.setLocked(true); + assertTrue(fut.get(0, TimeUnit.SECONDS)); + } + + @Test + public void testGetLongTimeUnit_Timeout() throws Exception { + expectException(TimeoutException.class, xxx -> fut.get(0, TimeUnit.SECONDS)); + expectException(TimeoutException.class, xxx -> fut.get(2, TimeUnit.MILLISECONDS)); + } + + @Test + public void testGetLongTimeUnit_Cancelled() throws Exception { + new Thread() { + @Override + public void run() { + fut.cancel(false); + } + }.start(); + + expectException(CancellationException.class, xxx -> fut.get(WAIT_SEC, TimeUnit.SECONDS)); + } + + @Test + public void testGetLongTimeUnit_Acquired() throws Exception { + new Thread() { + @Override + public void run() { + fut.setLocked(true); + } + }.start(); + + assertTrue(fut.get(WAIT_SEC, TimeUnit.SECONDS)); + } + + @Test + public void testGetLongTimeUnit_Denied() throws Exception { + new Thread() { + @Override + public void run() { + fut.setLocked(false); + } + }.start(); + + assertFalse(fut.get(WAIT_SEC, TimeUnit.SECONDS)); + } + + @Test(expected = IllegalStateException.class) + public void testInvokeCallback() { + fut.setLocked(true); + fut.invokeCallback(); + + // re-invoke - should throw an exception + fut.invokeCallback(); + } + + @Test(expected = IllegalStateException.class) + public void testInvokeCallback_Cancelled() { + fut.cancel(false); + + // invoke after cancel - should throw an exception + fut.invokeCallback(); + } + + @Test + public void testInvokeCallback_Acquired() { + fut.setLocked(true); + fut.invokeCallback(); + + verify(callback).set(true); + verify(callback, never()).set(false); + } + + @Test + public void testInvokeCallback_Denied() { + fut.setLocked(false); + fut.invokeCallback(); + + verify(callback).set(false); + verify(callback, never()).set(true); + } + + @Test + public void testInvokeCallback_Ex() { + doThrow(new RuntimeException(EXPECTED_EXCEPTION)).when(callback).set(anyBoolean()); + + fut.setLocked(false); + fut.invokeCallback(); + } + + @Test + public void testMakeNullArgException() { + IllegalArgumentException ex = LockRequestFuture.makeNullArgException(EXPECTED_EXCEPTION); + assertEquals(EXPECTED_EXCEPTION, ex.getMessage()); + } +} diff --git a/policy-core/src/test/java/org/onap/policy/drools/core/lock/LockTest.java b/policy-core/src/test/java/org/onap/policy/drools/core/lock/LockTest.java new file mode 100644 index 00000000..bd11dcf9 --- /dev/null +++ b/policy-core/src/test/java/org/onap/policy/drools/core/lock/LockTest.java @@ -0,0 +1,131 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.core.lock; + +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; +import org.onap.policy.drools.core.lock.Lock.RemoveResult; +import org.onap.policy.drools.utils.Pair; + +public class LockTest { + + private static final String OWNER = "my.owner"; + private static final String OWNER2 = "another.owner"; + private static final String OWNER3 = "third.owner"; + + private static final Integer ITEM2 = 10; + private static final Integer ITEM3 = 20; + + private Lock lock; + private Pair newOwner; + + @Before + public void setUp() { + lock = new Lock<>(OWNER); + newOwner = new Pair<>(null, null); + } + + + @Test + public void testLock() { + assertEquals(OWNER, lock.getOwner()); + } + + @Test + public void testGetOwner() { + assertEquals(OWNER, lock.getOwner()); + } + + @Test + public void testAdd() { + assertTrue(lock.add(OWNER2, ITEM2)); + assertTrue(lock.add(OWNER3, ITEM3)); + + // attempt to re-add owner2 with the same item - should fail + assertFalse(lock.add(OWNER2, ITEM2)); + + // attempt to re-add owner2 with a different item - should fail + assertFalse(lock.add(OWNER2, ITEM3)); + } + + @Test(expected = IllegalArgumentException.class) + public void testAdd_ArgEx() { + lock.add(OWNER2, null); + } + + @Test + public void testAdd_AlreadyOwner() { + assertFalse(lock.add(OWNER, ITEM2)); + } + + @Test + public void testAdd_AlreadyInQueue() { + lock.add(OWNER2, ITEM2); + + assertFalse(lock.add(OWNER2, ITEM2)); + } + + @Test + public void testRemoveRequester_Owner_QueueEmpty() { + assertEquals(RemoveResult.UNLOCKED, lock.removeRequester(OWNER, newOwner)); + } + + @Test + public void testRemoveRequester_Owner_QueueHasOneItem() { + lock.add(OWNER2, ITEM2); + + assertEquals(RemoveResult.RELOCKED, lock.removeRequester(OWNER, newOwner)); + assertEquals(OWNER2, newOwner.first()); + assertEquals(ITEM2, newOwner.second()); + + assertEquals(RemoveResult.UNLOCKED, lock.removeRequester(OWNER2, newOwner)); + } + + @Test + public void testRemoveRequester_Owner_QueueHasMultipleItems() { + lock.add(OWNER2, ITEM2); + lock.add(OWNER3, ITEM3); + + assertEquals(RemoveResult.RELOCKED, lock.removeRequester(OWNER, newOwner)); + assertEquals(OWNER2, newOwner.first()); + assertEquals(ITEM2, newOwner.second()); + + assertEquals(RemoveResult.RELOCKED, lock.removeRequester(OWNER2, newOwner)); + assertEquals(OWNER3, newOwner.first()); + assertEquals(ITEM3, newOwner.second()); + + assertEquals(RemoveResult.UNLOCKED, lock.removeRequester(OWNER3, newOwner)); + } + + @Test + public void testRemoveRequester_InQueue() { + lock.add(OWNER2, ITEM2); + + assertEquals(RemoveResult.REMOVED, lock.removeRequester(OWNER2, newOwner)); + } + + @Test + public void testRemoveRequester_NeitherOwnerNorInQueue() { + assertEquals(RemoveResult.NOT_FOUND, lock.removeRequester(OWNER2, newOwner)); + } + +} diff --git a/policy-core/src/test/java/org/onap/policy/drools/core/lock/PolicyResourceLockFeatureAPITest.java b/policy-core/src/test/java/org/onap/policy/drools/core/lock/PolicyResourceLockFeatureAPITest.java new file mode 100644 index 00000000..5a826bb7 --- /dev/null +++ b/policy-core/src/test/java/org/onap/policy/drools/core/lock/PolicyResourceLockFeatureAPITest.java @@ -0,0 +1,76 @@ +/* + * ============LICENSE_START======================================================= + * api-resource-locks + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.core.lock; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import org.junit.Before; +import org.junit.Test; + +public class PolicyResourceLockFeatureAPITest { + + private static final String RESOURCE_ID = "the resource"; + private static final String OWNER = "the owner"; + + private PolicyResourceLockFeatureAPI api; + + @Before + public void setUp() { + api = new PolicyResourceLockFeatureAPI() { + @Override + public int getSequenceNumber() { + return 0; + } + }; + } + + @Test + public void testBeforeLock() { + assertNull(api.beforeLock(RESOURCE_ID, OWNER, null)); + } + + @Test + public void testAfterLock() { + assertFalse(api.afterLock(RESOURCE_ID, OWNER, true)); + assertFalse(api.afterLock(RESOURCE_ID, OWNER, false)); + } + + @Test + public void testBeforeUnlock() { + assertNull(api.beforeUnlock(RESOURCE_ID, OWNER)); + } + + @Test + public void testAfterUnlock() { + assertFalse(api.afterUnlock(RESOURCE_ID, OWNER, true)); + assertFalse(api.afterUnlock(RESOURCE_ID, OWNER, false)); + } + + @Test + public void testBeforeIsLocked() { + assertNull(api.beforeIsLocked(RESOURCE_ID)); + } + + @Test + public void testBeforeIsLockedBy() { + assertNull(api.beforeIsLockedBy(RESOURCE_ID, OWNER)); + } +} diff --git a/policy-core/src/test/java/org/onap/policy/drools/core/lock/PolicyResourceLockManagerTest.java b/policy-core/src/test/java/org/onap/policy/drools/core/lock/PolicyResourceLockManagerTest.java new file mode 100644 index 00000000..2f8b5f83 --- /dev/null +++ b/policy-core/src/test/java/org/onap/policy/drools/core/lock/PolicyResourceLockManagerTest.java @@ -0,0 +1,509 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.core.lock; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.onap.policy.drools.core.lock.TestUtils.expectException; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Future; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.onap.policy.drools.core.lock.PolicyResourceLockFeatureAPI.Callback; +import org.onap.policy.drools.core.lock.PolicyResourceLockManager.Factory; + +public class PolicyResourceLockManagerTest { + + private static final String NULL_RESOURCE_ID = "null resourceId"; + private static final String NULL_OWNER = "null owner"; + + private static final String RESOURCE_A = "resource.a"; + private static final String RESOURCE_B = "resource.b"; + private static final String RESOURCE_C = "resource.c"; + + private static final String OWNER1 = "owner.one"; + private static final String OWNER2 = "owner.two"; + private static final String OWNER3 = "owner.three"; + + /** + * Saved at the start of the tests and restored once all tests complete. + */ + private static Factory saveFactory; + + private Callback callback1; + private PolicyResourceLockFeatureAPI impl1; + private PolicyResourceLockFeatureAPI impl2; + private List implList; + + private Future fut; + + private PolicyResourceLockManager mgr; + + @BeforeClass + public static void setUpBeforeClass() { + saveFactory = PolicyResourceLockManager.getFactory(); + } + + @AfterClass + public static void tearDownAfterClass() { + PolicyResourceLockManager.setFactory(saveFactory); + } + + @Before + public void setUp() { + callback1 = mock(Callback.class); + impl1 = mock(PolicyResourceLockFeatureAPI.class); + impl2 = mock(PolicyResourceLockFeatureAPI.class); + + initImplementer(impl1); + initImplementer(impl2); + + // list of feature API implementers + implList = new LinkedList<>(Arrays.asList(impl1, impl2)); + + PolicyResourceLockManager.setFactory(new Factory() { + + @Override + public List getImplementers() { + return implList; + } + }); + + mgr = new PolicyResourceLockManager(); + } + + /** + * Initializes an implementer so it always returns {@code null}. + * + * @param impl + */ + private void initImplementer(PolicyResourceLockFeatureAPI impl) { + when(impl.beforeLock(anyString(), anyString(), any(Callback.class))).thenReturn(null); + when(impl.beforeUnlock(anyString(), anyString())).thenReturn(null); + when(impl.beforeIsLocked(anyString())).thenReturn(null); + when(impl.beforeIsLockedBy(anyString(), anyString())).thenReturn(null); + } + + @Test + public void testLock() throws Exception { + fut = mgr.lock(RESOURCE_A, OWNER1, callback1); + + assertTrue(fut.isDone()); + assertTrue(fut.get()); + + verify(impl1).beforeLock(RESOURCE_A, OWNER1, callback1); + verify(impl2).beforeLock(RESOURCE_A, OWNER1, callback1); + verify(impl1).afterLock(RESOURCE_A, OWNER1, true); + verify(impl2).afterLock(RESOURCE_A, OWNER1, true); + + assertTrue(mgr.isLocked(RESOURCE_A)); + assertTrue(mgr.isLockedBy(RESOURCE_A, OWNER1)); + assertFalse(mgr.isLocked(RESOURCE_B)); + assertFalse(mgr.isLockedBy(RESOURCE_A, OWNER2)); + + // null callback - not locked yet + fut = mgr.lock(RESOURCE_C, OWNER3, null); + assertTrue(fut.isDone()); + assertTrue(fut.get()); + + // null callback - already locked + fut = mgr.lock(RESOURCE_A, OWNER3, null); + assertTrue(fut.isDone()); + assertFalse(fut.get()); + } + + @Test + public void testLock_ArgEx() { + IllegalArgumentException ex = + expectException(IllegalArgumentException.class, xxx -> mgr.lock(null, OWNER1, callback1)); + assertEquals(NULL_RESOURCE_ID, ex.getMessage()); + + ex = expectException(IllegalArgumentException.class, xxx -> mgr.lock(RESOURCE_A, null, callback1)); + assertEquals(NULL_OWNER, ex.getMessage()); + + // this should not throw an exception + mgr.lock(RESOURCE_A, OWNER1, null); + } + + @Test + public void testLock_BeforeIntercepted() { + fut = mock(LockRequestFuture.class); + + // NOT async + when(fut.isDone()).thenReturn(true); + + // have impl1 intercept + when(impl1.beforeLock(RESOURCE_A, OWNER1, callback1)).thenReturn(fut); + + assertEquals(fut, mgr.lock(RESOURCE_A, OWNER1, callback1)); + + verify(impl1).beforeLock(RESOURCE_A, OWNER1, callback1); + verify(impl2, never()).beforeLock(anyString(), anyString(), any(Callback.class)); + + verify(impl1, never()).afterLock(anyString(), anyString(), anyBoolean()); + verify(impl2, never()).afterLock(anyString(), anyString(), anyBoolean()); + } + + @Test + public void testLock_Acquired_AfterIntercepted() throws Exception { + + // impl1 intercepts during afterLock() + when(impl1.afterLock(RESOURCE_A, OWNER1, true)).thenReturn(true); + + fut = mgr.lock(RESOURCE_A, OWNER1, callback1); + + assertTrue(fut.isDone()); + assertTrue(fut.get()); + + // impl1 sees it, but impl2 does not + verify(impl1).afterLock(RESOURCE_A, OWNER1, true); + verify(impl2, never()).afterLock(anyString(), anyString(), anyBoolean()); + } + + @Test + public void testLock_Acquired() throws Exception { + fut = mgr.lock(RESOURCE_A, OWNER1, callback1); + + assertTrue(fut.isDone()); + assertTrue(fut.get()); + + verify(impl1).afterLock(RESOURCE_A, OWNER1, true); + verify(impl2).afterLock(RESOURCE_A, OWNER1, true); + } + + @Test + public void testLock_Denied_AfterIntercepted() throws Exception { + + mgr.lock(RESOURCE_A, OWNER1, callback1); + + // impl1 intercepts during afterLock() + when(impl1.afterLock(RESOURCE_A, OWNER2, false)).thenReturn(true); + + // owner2 tries to lock + fut = mgr.lock(RESOURCE_A, OWNER2, null); + + assertTrue(fut.isDone()); + assertFalse(fut.get()); + + // impl1 sees it, but impl2 does not + verify(impl1).afterLock(RESOURCE_A, OWNER2, false); + verify(impl2, never()).afterLock(RESOURCE_A, OWNER2, false); + } + + @Test + public void testLock_Denied() { + + mgr.lock(RESOURCE_A, OWNER1, callback1); + + // owner2 tries to lock + fut = mgr.lock(RESOURCE_A, OWNER2, null); + + verify(impl1).afterLock(RESOURCE_A, OWNER2, false); + verify(impl2).afterLock(RESOURCE_A, OWNER2, false); + } + + @Test + public void testUnlock() throws Exception { + mgr.lock(RESOURCE_A, OWNER1, null); + mgr.lock(RESOURCE_B, OWNER1, null); + + assertTrue(mgr.unlock(RESOURCE_A, OWNER1)); + + verify(impl1).beforeUnlock(RESOURCE_A, OWNER1); + verify(impl2).beforeUnlock(RESOURCE_A, OWNER1); + + verify(impl1).afterUnlock(RESOURCE_A, OWNER1, true); + verify(impl2).afterUnlock(RESOURCE_A, OWNER1, true); + } + + @Test + public void testUnlock_ArgEx() { + IllegalArgumentException ex = expectException(IllegalArgumentException.class, xxx -> mgr.unlock(null, OWNER1)); + assertEquals(NULL_RESOURCE_ID, ex.getMessage()); + + ex = expectException(IllegalArgumentException.class, xxx -> mgr.unlock(RESOURCE_A, null)); + assertEquals(NULL_OWNER, ex.getMessage()); + } + + @Test + public void testUnlock_BeforeInterceptedTrue() { + + mgr.lock(RESOURCE_A, OWNER1, null); + + // have impl1 intercept + when(impl1.beforeUnlock(RESOURCE_A, OWNER1)).thenReturn(true); + + assertTrue(mgr.unlock(RESOURCE_A, OWNER1)); + + verify(impl1).beforeUnlock(RESOURCE_A, OWNER1); + verify(impl2, never()).beforeUnlock(anyString(), anyString()); + + verify(impl1, never()).afterUnlock(anyString(), anyString(), anyBoolean()); + verify(impl2, never()).afterUnlock(anyString(), anyString(), anyBoolean()); + } + + @Test + public void testUnlock_BeforeInterceptedFalse() { + + mgr.lock(RESOURCE_A, OWNER1, null); + + // have impl1 intercept + when(impl1.beforeUnlock(RESOURCE_A, OWNER1)).thenReturn(false); + + assertFalse(mgr.unlock(RESOURCE_A, OWNER1)); + + verify(impl1).beforeUnlock(RESOURCE_A, OWNER1); + verify(impl2, never()).beforeUnlock(anyString(), anyString()); + + verify(impl1, never()).afterUnlock(anyString(), anyString(), anyBoolean()); + verify(impl2, never()).afterUnlock(anyString(), anyString(), anyBoolean()); + } + + @Test + public void testUnlock_Unlocked() { + mgr.lock(RESOURCE_A, OWNER1, null); + + assertTrue(mgr.unlock(RESOURCE_A, OWNER1)); + + verify(impl1).beforeUnlock(RESOURCE_A, OWNER1); + verify(impl2).beforeUnlock(RESOURCE_A, OWNER1); + + verify(impl1).afterUnlock(RESOURCE_A, OWNER1, true); + verify(impl2).afterUnlock(RESOURCE_A, OWNER1, true); + } + + @Test + public void testUnlock_Unlocked_AfterIntercepted() { + // have impl1 intercept + when(impl1.afterUnlock(RESOURCE_A, OWNER1, true)).thenReturn(true); + + mgr.lock(RESOURCE_A, OWNER1, null); + + assertTrue(mgr.unlock(RESOURCE_A, OWNER1)); + + verify(impl1).beforeUnlock(RESOURCE_A, OWNER1); + verify(impl2).beforeUnlock(RESOURCE_A, OWNER1); + + verify(impl1).afterUnlock(RESOURCE_A, OWNER1, true); + verify(impl2, never()).afterUnlock(RESOURCE_A, OWNER1, true); + } + + @Test + public void testUnlock_NotUnlocked() { + assertFalse(mgr.unlock(RESOURCE_A, OWNER1)); + + verify(impl1).beforeUnlock(RESOURCE_A, OWNER1); + verify(impl2).beforeUnlock(RESOURCE_A, OWNER1); + + verify(impl1).afterUnlock(RESOURCE_A, OWNER1, false); + verify(impl2).afterUnlock(RESOURCE_A, OWNER1, false); + } + + @Test + public void testUnlock_NotUnlocked_AfterIntercepted() { + // have impl1 intercept + when(impl1.afterUnlock(RESOURCE_A, OWNER1, false)).thenReturn(true); + + assertFalse(mgr.unlock(RESOURCE_A, OWNER1)); + + verify(impl1).beforeUnlock(RESOURCE_A, OWNER1); + verify(impl2).beforeUnlock(RESOURCE_A, OWNER1); + + verify(impl1).afterUnlock(RESOURCE_A, OWNER1, false); + verify(impl2, never()).afterUnlock(RESOURCE_A, OWNER1, false); + } + + @Test + public void testIsLocked_True() { + mgr.lock(RESOURCE_A, OWNER1, null); + + assertTrue(mgr.isLocked(RESOURCE_A)); + + verify(impl1).beforeIsLocked(RESOURCE_A); + verify(impl2).beforeIsLocked(RESOURCE_A); + } + + @Test + public void testIsLocked_False() { + assertFalse(mgr.isLocked(RESOURCE_A)); + + verify(impl1).beforeIsLocked(RESOURCE_A); + verify(impl2).beforeIsLocked(RESOURCE_A); + } + + @Test + public void testIsLocked_ArgEx() { + IllegalArgumentException ex = expectException(IllegalArgumentException.class, xxx -> mgr.isLocked(null)); + assertEquals(NULL_RESOURCE_ID, ex.getMessage()); + } + + @Test + public void testIsLocked_BeforeIntercepted_True() { + + // have impl1 intercept + when(impl1.beforeIsLocked(RESOURCE_A)).thenReturn(true); + + assertTrue(mgr.isLocked(RESOURCE_A)); + + verify(impl1).beforeIsLocked(RESOURCE_A); + verify(impl2, never()).beforeIsLocked(RESOURCE_A); + } + + @Test + public void testIsLocked_BeforeIntercepted_False() { + + // lock it so we can verify that impl1 overrides the superclass isLocker() + mgr.lock(RESOURCE_A, OWNER1, null); + + // have impl1 intercept + when(impl1.beforeIsLocked(RESOURCE_A)).thenReturn(false); + + assertFalse(mgr.isLocked(RESOURCE_A)); + + verify(impl1).beforeIsLocked(RESOURCE_A); + verify(impl2, never()).beforeIsLocked(RESOURCE_A); + } + + @Test + public void testIsLockedBy_True() { + mgr.lock(RESOURCE_A, OWNER1, null); + + assertTrue(mgr.isLockedBy(RESOURCE_A, OWNER1)); + + verify(impl1).beforeIsLockedBy(RESOURCE_A, OWNER1); + verify(impl2).beforeIsLockedBy(RESOURCE_A, OWNER1); + } + + @Test + public void testIsLockedBy_False() { + // different owner + mgr.lock(RESOURCE_A, OWNER2, null); + + assertFalse(mgr.isLockedBy(RESOURCE_A, OWNER1)); + + verify(impl1).beforeIsLockedBy(RESOURCE_A, OWNER1); + verify(impl2).beforeIsLockedBy(RESOURCE_A, OWNER1); + } + + @Test + public void testIsLockedBy_ArgEx() { + IllegalArgumentException ex = + expectException(IllegalArgumentException.class, xxx -> mgr.isLockedBy(null, OWNER1)); + assertEquals(NULL_RESOURCE_ID, ex.getMessage()); + + ex = expectException(IllegalArgumentException.class, xxx -> mgr.isLockedBy(RESOURCE_A, null)); + assertEquals(NULL_OWNER, ex.getMessage()); + } + + @Test + public void testIsLockedBy_BeforeIntercepted_True() { + + // have impl1 intercept + when(impl1.beforeIsLockedBy(RESOURCE_A, OWNER1)).thenReturn(true); + + assertTrue(mgr.isLockedBy(RESOURCE_A, OWNER1)); + + verify(impl1).beforeIsLockedBy(RESOURCE_A, OWNER1); + verify(impl2, never()).beforeIsLockedBy(RESOURCE_A, OWNER1); + } + + @Test + public void testIsLockedBy_BeforeIntercepted_False() { + + // lock it so we can verify that impl1 overrides the superclass isLocker() + mgr.lock(RESOURCE_A, OWNER1, null); + + // have impl1 intercept + when(impl1.beforeIsLockedBy(RESOURCE_A, OWNER1)).thenReturn(false); + + assertFalse(mgr.isLockedBy(RESOURCE_A, OWNER1)); + + verify(impl1).beforeIsLockedBy(RESOURCE_A, OWNER1); + verify(impl2, never()).beforeIsLockedBy(RESOURCE_A, OWNER1); + } + + @Test + public void testGetInstance() { + PolicyResourceLockManager inst = PolicyResourceLockManager.getInstance(); + assertNotNull(inst); + + // should return the same instance each time + assertEquals(inst, PolicyResourceLockManager.getInstance()); + assertEquals(inst, PolicyResourceLockManager.getInstance()); + } + + @Test + public void testDoIntercept_Empty() { + // clear the implementer list + implList.clear(); + + mgr.lock(RESOURCE_A, OWNER1, null); + + assertTrue(mgr.isLocked(RESOURCE_A)); + assertFalse(mgr.isLocked(RESOURCE_B)); + + verify(impl1, never()).beforeIsLocked(anyString()); + } + + @Test + public void testDoIntercept_Impl1() { + when(impl1.beforeIsLocked(RESOURCE_A)).thenReturn(true); + + assertTrue(mgr.isLocked(RESOURCE_A)); + + verify(impl1).beforeIsLocked(RESOURCE_A); + verify(impl2, never()).beforeIsLocked(anyString()); + } + + @Test + public void testDoIntercept_Impl2() { + when(impl2.beforeIsLocked(RESOURCE_A)).thenReturn(true); + + assertTrue(mgr.isLocked(RESOURCE_A)); + + verify(impl1).beforeIsLocked(RESOURCE_A); + verify(impl2).beforeIsLocked(RESOURCE_A); + } + + @Test + public void testDoIntercept_Ex() { + doThrow(new RuntimeException("expected exception")).when(impl1).beforeIsLocked(RESOURCE_A); + + assertFalse(mgr.isLocked(RESOURCE_A)); + + verify(impl1).beforeIsLocked(RESOURCE_A); + verify(impl2).beforeIsLocked(RESOURCE_A); + } +} diff --git a/policy-core/src/test/java/org/onap/policy/drools/core/lock/SimpleLockManagerTest.java b/policy-core/src/test/java/org/onap/policy/drools/core/lock/SimpleLockManagerTest.java new file mode 100644 index 00000000..6abc5bf9 --- /dev/null +++ b/policy-core/src/test/java/org/onap/policy/drools/core/lock/SimpleLockManagerTest.java @@ -0,0 +1,280 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.core.lock; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.onap.policy.drools.core.lock.TestUtils.expectException; +import java.util.LinkedList; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Before; +import org.junit.Test; + +public class SimpleLockManagerTest { + + private static final String NULL_RESOURCE_ID = "null resourceId"; + private static final String NULL_OWNER = "null owner"; + + private static final String RESOURCE_A = "resource.a"; + private static final String RESOURCE_B = "resource.b"; + private static final String RESOURCE_C = "resource.c"; + + private static final String OWNER1 = "owner.one"; + private static final String OWNER2 = "owner.two"; + private static final String OWNER3 = "owner.three"; + + private Future fut; + private SimpleLockManager mgr; + + @Before + public void setUp() { + mgr = new SimpleLockManager(); + } + + @Test + public void testLock() throws Exception { + fut = mgr.lock(RESOURCE_A, OWNER1, null); + + assertTrue(fut.isDone()); + assertTrue(fut.get()); + + assertTrue(mgr.isLocked(RESOURCE_A)); + assertTrue(mgr.isLockedBy(RESOURCE_A, OWNER1)); + assertFalse(mgr.isLocked(RESOURCE_B)); + assertFalse(mgr.isLockedBy(RESOURCE_A, OWNER2)); + + // null callback - not locked yet + fut = mgr.lock(RESOURCE_C, OWNER3, null); + assertTrue(fut.isDone()); + assertTrue(fut.get()); + + // null callback - already locked + fut = mgr.lock(RESOURCE_A, OWNER3, null); + assertTrue(fut.isDone()); + assertFalse(fut.get()); + } + + @Test + public void testLock_AlreadyLocked() throws Exception { + mgr.lock(RESOURCE_A, OWNER1, null); + + fut = mgr.lock(RESOURCE_A, OWNER2, null); + assertTrue(fut.isDone()); + assertFalse(fut.get()); + } + + @Test(expected = IllegalStateException.class) + public void testLock_SameOwner() throws Exception { + mgr.lock(RESOURCE_A, OWNER1, null); + + // should throw an exception + mgr.lock(RESOURCE_A, OWNER1, null); + } + + @Test + public void testLock_ArgEx() { + IllegalArgumentException ex = + expectException(IllegalArgumentException.class, xxx -> mgr.lock(null, OWNER1, null)); + assertEquals(NULL_RESOURCE_ID, ex.getMessage()); + + ex = expectException(IllegalArgumentException.class, xxx -> mgr.lock(RESOURCE_A, null, null)); + assertEquals(NULL_OWNER, ex.getMessage()); + + // this should not throw an exception + mgr.lock(RESOURCE_A, OWNER1, null); + } + + @Test + public void testUnlock() throws Exception { + mgr.lock(RESOURCE_A, OWNER1, null); + + // unlock it + assertTrue(mgr.unlock(RESOURCE_A, OWNER1)); + } + + @Test + public void testUnlock_ArgEx() { + IllegalArgumentException ex = expectException(IllegalArgumentException.class, xxx -> mgr.unlock(null, OWNER1)); + assertEquals(NULL_RESOURCE_ID, ex.getMessage()); + + ex = expectException(IllegalArgumentException.class, xxx -> mgr.unlock(RESOURCE_A, null)); + assertEquals(NULL_OWNER, ex.getMessage()); + } + + @Test + public void testUnlock_NotLocked() { + assertFalse(mgr.unlock(RESOURCE_A, OWNER1)); + } + + @Test + public void testIsLocked() { + assertFalse(mgr.isLocked(RESOURCE_A)); + + mgr.lock(RESOURCE_A, OWNER1, null); + mgr.lock(RESOURCE_B, OWNER1, null); + + assertTrue(mgr.isLocked(RESOURCE_A)); + assertTrue(mgr.isLocked(RESOURCE_B)); + assertFalse(mgr.isLocked(RESOURCE_C)); + + // unlock from first resource + mgr.unlock(RESOURCE_A, OWNER1); + assertFalse(mgr.isLocked(RESOURCE_A)); + assertTrue(mgr.isLocked(RESOURCE_B)); + assertFalse(mgr.isLocked(RESOURCE_C)); + + // unlock from second resource + mgr.unlock(RESOURCE_B, OWNER1); + assertFalse(mgr.isLocked(RESOURCE_A)); + assertFalse(mgr.isLocked(RESOURCE_B)); + assertFalse(mgr.isLocked(RESOURCE_C)); + } + + @Test + public void testIsLocked_ArgEx() { + IllegalArgumentException ex = expectException(IllegalArgumentException.class, xxx -> mgr.isLocked(null)); + assertEquals(NULL_RESOURCE_ID, ex.getMessage()); + } + + @Test + public void testIsLockedBy() { + assertFalse(mgr.isLockedBy(RESOURCE_A, OWNER1)); + + mgr.lock(RESOURCE_A, OWNER1, null); + + assertFalse(mgr.isLockedBy(RESOURCE_B, OWNER1)); + + assertTrue(mgr.isLockedBy(RESOURCE_A, OWNER1)); + assertFalse(mgr.isLockedBy(RESOURCE_A, OWNER2)); + + // unlock from the resource + mgr.unlock(RESOURCE_A, OWNER1); + assertFalse(mgr.isLockedBy(RESOURCE_A, OWNER1)); + assertFalse(mgr.isLockedBy(RESOURCE_A, OWNER2)); + assertFalse(mgr.isLockedBy(RESOURCE_B, OWNER1)); + } + + @Test + public void testIsLockedBy_ArgEx() { + IllegalArgumentException ex = + expectException(IllegalArgumentException.class, xxx -> mgr.isLockedBy(null, OWNER1)); + assertEquals(NULL_RESOURCE_ID, ex.getMessage()); + + ex = expectException(IllegalArgumentException.class, xxx -> mgr.isLockedBy(RESOURCE_A, null)); + assertEquals(NULL_OWNER, ex.getMessage()); + } + + @Test + public void testIsLockedBy_NotLocked() { + mgr.lock(RESOURCE_A, OWNER1, null); + + // different resource, thus no lock + assertFalse(mgr.isLockedBy(RESOURCE_B, OWNER1)); + } + + @Test + public void testIsLockedBy_LockedButNotOwner() { + mgr.lock(RESOURCE_A, OWNER1, null); + + // different owner + assertFalse(mgr.isLockedBy(RESOURCE_A, OWNER2)); + } + + @Test + public void testMultiThreaded() throws InterruptedException { + int nthreads = 10; + int nlocks = 100; + + LinkedList threads = new LinkedList<>(); + + String[] resources = {RESOURCE_A, RESOURCE_B}; + + AtomicInteger nfail = new AtomicInteger(0); + + CountDownLatch stopper = new CountDownLatch(1); + CountDownLatch completed = new CountDownLatch(nthreads); + + for (int x = 0; x < nthreads; ++x) { + String owner = "owner." + x; + + Thread t = new Thread() { + @Override + public void run() { + + for (int y = 0; y < nlocks; ++y) { + String res = resources[y % resources.length]; + + try { + // some locks will be acquired, some denied + mgr.lock(res, owner, null).get(); + + // do some "work" + stopper.await(1L, TimeUnit.MILLISECONDS); + + mgr.unlock(res, owner); + + } catch (CancellationException | ExecutionException e) { + nfail.incrementAndGet(); + + } catch (InterruptedException expected) { + Thread.currentThread().interrupt(); + break; + } + } + + completed.countDown(); + } + }; + + t.setDaemon(true); + threads.add(t); + } + + // start the threads + for (Thread t : threads) { + t.start(); + } + + // wait for them to complete + completed.await(5000L, TimeUnit.SECONDS); + + // stop the threads from sleeping + stopper.countDown(); + + completed.await(1L, TimeUnit.SECONDS); + + // interrupt those that are still alive + for (Thread t : threads) { + if (t.isAlive()) { + t.interrupt(); + } + } + + assertEquals(0, nfail.get()); + } + +} diff --git a/policy-core/src/test/java/org/onap/policy/drools/core/lock/TestUtils.java b/policy-core/src/test/java/org/onap/policy/drools/core/lock/TestUtils.java new file mode 100644 index 00000000..2e353936 --- /dev/null +++ b/policy-core/src/test/java/org/onap/policy/drools/core/lock/TestUtils.java @@ -0,0 +1,59 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.core.lock; + +public class TestUtils { + + /** + * Invokes a function that is expected to throw an exception. + * + * @param clazz class of exception that is expected + * @param func + * @return + */ + public static T expectException(Class clazz, VoidFunction func) { + try { + func.apply(null); + throw new AssertionError("missing exception"); + + } catch (Exception e) { + try { + return clazz.cast(e); + + } catch (ClassCastException e2) { + throw new AssertionError("incorrect exception type", e2); + } + } + } + + /** + * Void function that may throw an exception. + */ + @FunctionalInterface + public static interface VoidFunction { + + /** + * + * @param arg always {@code null} + */ + public void apply(Void arg) throws Exception; + } +} diff --git a/policy-utils/src/main/java/org/onap/policy/drools/utils/Reference.java b/policy-utils/src/main/java/org/onap/policy/drools/utils/Reference.java new file mode 100644 index 00000000..0c4155fe --- /dev/null +++ b/policy-utils/src/main/java/org/onap/policy/drools/utils/Reference.java @@ -0,0 +1,72 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.utils; + +/** + * Reference to an object. Used within functional methods, where thread-safety is not + * required. + * + * @param type of object contained within the reference + */ +public class Reference { + private T value; + + /** + * + * @param value + */ + public Reference(T value) { + this.value = value; + } + + /** + * @return the current value + */ + public final T get() { + return value; + } + + /** + * Sets the reference to point to a new value. + * + * @param newValue + */ + public final void set(T newValue) { + this.value = newValue; + } + + /** + * Sets the value to a new value, if the value is currently the same as the old value. + * + * @param oldValue + * @param newValue + * @return {@code true} if the value was updated, {@code false} otherwise + */ + public boolean compareAndSet(T oldValue, T newValue) { + if (value == oldValue) { + value = newValue; + return true; + + } else { + return false; + } + } +} diff --git a/policy-utils/src/test/java/org/onap/policy/drools/utils/ReferenceTest.java b/policy-utils/src/test/java/org/onap/policy/drools/utils/ReferenceTest.java new file mode 100644 index 00000000..1d6b111a --- /dev/null +++ b/policy-utils/src/test/java/org/onap/policy/drools/utils/ReferenceTest.java @@ -0,0 +1,84 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import org.onap.policy.drools.utils.Reference; + +public class ReferenceTest { + + @Test + public void testReference() { + Reference val = new Reference<>(null); + assertNull(val.get()); + + val = new Reference<>(10); + assertEquals(10, val.get().intValue()); + } + + @Test + public void testGet_testSet() { + Reference val = new Reference<>(null); + assertNull(val.get()); + + val.set(20); + assertEquals(20, val.get().intValue()); + + val.set(30); + assertEquals(30, val.get().intValue()); + } + + @Test + public void testCompareAndSet() { + Reference val = new Reference<>(null); + + Integer v = 100; + + // try an incorrect value - should fail and leave value unchanged + assertFalse(val.compareAndSet(500, v)); + assertNull(val.get()); + + assertTrue(val.compareAndSet(null, v)); + assertEquals(v, val.get()); + + // try an incorrect value - should fail and leave value unchanged + Integer v2 = 200; + assertFalse(val.compareAndSet(600, v2)); + assertEquals(v, val.get()); + + // now try again, this time with the correct value + assertTrue(val.compareAndSet(v, v2)); + assertEquals(v2, val.get()); + + Integer v3 = 300; + assertTrue(val.compareAndSet(v2, v3)); + assertEquals(v3, val.get()); + + // try setting it back to null + assertTrue(val.compareAndSet(v3, null)); + assertNull(val.get()); + } + +} -- cgit 1.2.3-korg