package games.strategy.engine.delegate; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; import java.util.logging.Logger; import games.strategy.engine.GameOverException; import games.strategy.engine.framework.headlessGameServer.HeadlessGameServer; import games.strategy.engine.message.MessengerException; import games.strategy.triplea.util.WrappedInvocationHandler; /** * Manages when delegates are allowed to execute. * * <p> * When saving a game, we want to ensure that no delegate is executing, otherwise the delegate could modify the state of * the game while the * game is being saved, resulting in an invalid save game. * </p> * * <p> * This class effectivly keeps a count of how many threads are executing in the delegates, and provides a way of * blocking further threads * from starting execution in a delegate. * </p> */ public class DelegateExecutionManager { private final Logger sm_logger = Logger.getLogger(DelegateExecutionManager.class.getName()); /* * Delegate execution can be thought of as a read/write lock. * Many delegates can be executing at one time (to execute you acquire the read lock), but * only 1 block can be held (the block is equivalent to the read lock). */ private final ReentrantReadWriteLock m_readWriteLock = new ReentrantReadWriteLock(); private final ThreadLocal<Boolean> m_currentThreadHasReadLock = new ThreadLocal<>(); private volatile boolean m_isGameOver = false; public void setGameOver() { m_isGameOver = true; } /** * When this method returns true, threads will not be able to enter delegates until * a call to resumeDelegateExecution is made. * * <p> * When delegateExecution is blocked, it also blocks subsequent cals to blockDelegateExecution(...) * </p> * * <p> * If timeToWaitMS is > 0, we will give up trying to block delegate execution after timeTiWaitMS has elapsed. * </p> */ public boolean blockDelegateExecution(final int timeToWaitMS) throws InterruptedException { final boolean rVal = m_readWriteLock.writeLock().tryLock(timeToWaitMS, TimeUnit.MILLISECONDS); if (!rVal) { if (sm_logger.isLoggable(Level.FINE)) { sm_logger.fine("Could not block delegate execution. Read Lock count: " + m_readWriteLock.getReadLockCount() + " Write Hold count: " + m_readWriteLock.getWriteHoldCount() + " Queue Length: " + m_readWriteLock.getQueueLength() + " Current Thread Has Lock: " + m_readWriteLock.isWriteLockedByCurrentThread() + " Has Queued Threads: " + m_readWriteLock.hasQueuedThreads() + " Is Write Locked: " + m_readWriteLock.isWriteLocked() + " toString: " + m_readWriteLock.toString()); } else { HeadlessGameServer.log("Could not block delegate execution. Read Lock count: " + m_readWriteLock.getReadLockCount() + " Write Hold count: " + m_readWriteLock.getWriteHoldCount() + " Queue Length: " + m_readWriteLock.getQueueLength() + " Current Thread Has Lock: " + m_readWriteLock.isWriteLockedByCurrentThread() + " Has Queued Threads: " + m_readWriteLock.hasQueuedThreads() + " Is Write Locked: " + m_readWriteLock.isWriteLocked() + " toString: " + m_readWriteLock.toString()); } } else { if (sm_logger.isLoggable(Level.FINE)) { sm_logger.fine(Thread.currentThread().getName() + " block delegate execution."); } } return rVal; } /** * Allow delegate execution to resume. */ public void resumeDelegateExecution() { if (sm_logger.isLoggable(Level.FINE)) { sm_logger.fine(Thread.currentThread().getName() + " resumes delegate execution."); } m_readWriteLock.writeLock().unlock(); } private boolean currentThreadHasReadLock() { return m_currentThreadHasReadLock.get() == Boolean.TRUE; } /** * Used to create an object the exits delegate execution. * * <p> * Objects on this method will decrement the thread lock count when called, and will increment it again when execution * is finished. * </p> */ public Object createOutboundImplementation(final Object implementor, final Class<?>[] interfaces) { assertGameNotOver(); final InvocationHandler ih = (proxy, method, args) -> { assertGameNotOver(); final boolean threadLocks = currentThreadHasReadLock(); if (threadLocks) { leaveDelegateExecution(); } try { return method.invoke(implementor, args); } catch (final MessengerException me) { throw new GameOverException("Game Over!"); } catch (final InvocationTargetException ite) { assertGameNotOver(); throw ite; } finally { if (threadLocks) { enterDelegateExecution(); } } }; return Proxy.newProxyInstance(implementor.getClass().getClassLoader(), interfaces, ih); } private void assertGameNotOver() { if (m_isGameOver) { throw new GameOverException("Game Over"); } } /** * Use to create an object that begins delegate execution. * * <p> * Objects on this method will increment the thread lock count when called, and will decrement it again when execution * is finished. * </p> */ public Object createInboundImplementation(final Object implementor, final Class<?>[] interfaces) { assertGameNotOver(); final InvocationHandler ih = new WrappedInvocationHandler(implementor) { @Override public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { if (super.shouldHandle(method, args)) { return super.handle(method, args); } assertGameNotOver(); enterDelegateExecution(); try { return method.invoke(implementor, args); } catch (final InvocationTargetException ite) { assertGameNotOver(); throw ite.getCause(); } catch (final RuntimeException re) { assertGameNotOver(); throw re; } finally { leaveDelegateExecution(); } } }; return Proxy.newProxyInstance(implementor.getClass().getClassLoader(), interfaces, ih); } public void leaveDelegateExecution() { if (sm_logger.isLoggable(Level.FINE)) { sm_logger.fine(Thread.currentThread().getName() + " leaves delegate execution."); } m_readWriteLock.readLock().unlock(); m_currentThreadHasReadLock.set(null); } public void enterDelegateExecution() { if (sm_logger.isLoggable(Level.FINE)) { sm_logger.fine(Thread.currentThread().getName() + " enters delegate execution."); } if (currentThreadHasReadLock()) { throw new IllegalStateException("Already locked?"); } m_readWriteLock.readLock().lock(); m_currentThreadHasReadLock.set(Boolean.TRUE); } }