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}