001package com.identityworksllc.iiq.common.threads;
002
003import bsh.This;
004import org.apache.commons.logging.Log;
005import org.apache.commons.logging.LogFactory;
006import sailpoint.tools.GeneralException;
007
008import java.io.Closeable;
009import java.io.IOException;
010import java.util.UUID;
011import java.util.concurrent.TimeUnit;
012import java.util.concurrent.locks.ReentrantLock;
013
014/**
015 * An object reference wrapped by a ReentrantLock. This allows either directly
016 * invoking a function on the locked object or a more procedural lock/unlock
017 * pattern. This is fairer and safer than synchronizing on the object, since
018 * it can be interrupted (via Thread.interrupt(), e.g. on a task termination)
019 * and fairly chooses the longest-waiting thread to execute next.
020 *
021 * In either usage, threads will wait forever for the lock. Threads will emit
022 * an INFO level log message every 30 seconds indicating that they are still
023 * waiting. Additionally, threads will emit a DEBUG message upon acquiring
024 * the lock. Both messages will have a UUID indicating a unique lock name,
025 * for tracing purposes.
026 *
027 * If you are calling this from Beanshell, you can directly pass the name of a
028 * Beanshell method, along with the 'this' reference, such as:
029 *
030 *   objReference.lockAndAct(this, "beanshellMethodName");
031 *
032 * On lock acquisition, the specified Beanshell method in the 'this' scope will
033 * be invoked, passing the object as the only argument. The object will be
034 * automatically freed after the method completes.
035 *
036 * You can also use this class via a try/finally structure, such as:
037 *
038 *   Object lockedObject = objReference.lock();
039 *   try {
040 *       // do stuff here
041 *   } finally {
042 *       objReference.unlock();
043 *   }
044 *
045 * @param <T> The type of the contained object
046 */
047public final class LockingObjectReference<T> implements AutoCloseable {
048    /**
049     * The interface to be implemented by any locked object actions
050     * @param <T> The object type
051     */
052    @FunctionalInterface
053    public interface LockedObjectAction<T> {
054        /**
055         * The action to take for locking the object
056         * @param object The object to act on
057         * @throws GeneralException if any failures occur
058         */
059        void accept(T object) throws GeneralException;
060    }
061
062    /**
063     * A special instance of LockedObjectAction for Beanshell execution purposes
064     * @param <T> The object type
065     */
066    private static class BeanshellLockedObjectAction<T> implements LockedObjectAction<T> {
067
068        private final String methodName;
069        private final This thisObject;
070
071        public BeanshellLockedObjectAction(bsh.This thisObject, String methodName) {
072            this.thisObject = thisObject;
073            this.methodName = methodName;
074        }
075
076        @Override
077        public void accept(T object) throws GeneralException {
078            try {
079                Object[] inputs = new Object[] { object };
080                thisObject.invokeMethod(methodName, inputs);
081            } catch(Exception e) {
082                throw new GeneralException(e);
083            }
084        }
085    }
086
087    private final ReentrantLock lock;
088    private final Log log;
089    private final T object;
090    private final String uuid;
091
092    public LockingObjectReference(T object) {
093        this.lock = new ReentrantLock(true);
094        this.object = object;
095        this.log = LogFactory.getLog(this.getClass());
096        this.uuid = UUID.randomUUID().toString();
097    }
098
099    /**
100     * Unlocks the object, allowing use of this class in a try-with-resources context
101     */
102    @Override
103    public void close() {
104        unlockObject();
105    }
106
107    /**
108     * Locks the object, then passes it to {@link LockedObjectAction#accept(Object)}.
109     * @param action The function to execute against the object after it is locked
110     * @throws GeneralException if any failures occur
111     */
112    public void lockAndAct(LockedObjectAction<T> action) throws GeneralException {
113        T theObject = lockObject();
114        try {
115            action.accept(theObject);
116        } finally {
117            unlockObject();
118        }
119    }
120
121    /**
122     * Locks the object, then passes it to the given Beanshell method.
123     * @param beanshell The Beanshell context ('this' in a script)
124     * @param methodName The name of a Beanshell method in the current 'this' context or a parent
125     * @throws GeneralException if any failures occur
126     */
127    public void lockAndAct(bsh.This beanshell, String methodName) throws GeneralException {
128        lockAndAct(new BeanshellLockedObjectAction<>(beanshell, methodName));
129    }
130
131    /**
132     * Locks the object (waiting forever if necessary), then returns the object
133     * @return The object, now exclusive to this thread
134     * @throws GeneralException if any failures occur
135     */
136    public T lockObject() throws GeneralException {
137        boolean locked = false;
138        final long startTime = System.currentTimeMillis();
139        long lastNotification = startTime;
140        try {
141            while (!locked) {
142                locked = lock.tryLock(10, TimeUnit.SECONDS);
143                if (!locked) {
144                    long notificationElapsed = System.currentTimeMillis() - lastNotification;
145                    long totalElapsed = System.currentTimeMillis() - startTime;
146                    if (notificationElapsed > (1000L * 30)) {
147                        lastNotification = System.currentTimeMillis();
148                        if (log.isInfoEnabled()) {
149                            log.info("Thread " + Thread.currentThread().getName() + " has been waiting " + (totalElapsed / 1000L) + " seconds for lock " + uuid);
150                        }
151                    }
152                }
153            }
154            if (log.isDebugEnabled()) {
155                log.debug("Thread " + Thread.currentThread().getName() + " acquired the lock " + uuid);
156            }
157            return object;
158        } catch(InterruptedException e) {
159            throw new GeneralException(e);
160        }
161    }
162
163    /**
164     * Unlocks the object
165     */
166    public void unlockObject() {
167        if (lock.isLocked()) {
168            lock.unlock();
169        }
170    }
171}