package vroom.optimization.online.jmsa.utils; import java.util.Collections; import java.util.ConcurrentModificationException; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Observable; import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.TimeUnit; import vroom.common.utilities.ExtendedReentrantLock; import vroom.common.utilities.ILockable; import vroom.common.utilities.Stopwatch; import vroom.common.utilities.Utilities; import vroom.optimization.online.jmsa.IActualRequest; import vroom.optimization.online.jmsa.MSABase; import vroom.optimization.online.jmsa.events.MSAEvent; /** * The class <code>MSASimulator</code> is used to simulate a dynamic optimization problem. It uses an event queue and * sends scheduled events to a MSA procedure, and can be used to store the current state of all resources. * <p> * Creation date: Feb 9, 2012 - 5:07:39 PM * * @author Victor Pillac, <a href="http://uniandes.edu.co">Universidad de Los Andes</a>-<a * href="http://copa.uniandes.edu.co">Copa</a> <a href="http://www.emn.fr">Ecole des Mines de Nantes</a>-<a * href="http://www.irccyn.ec-nantes.fr/irccyn/d/en/equipes/Slp">SLP</a> * @version 1.0 */ public class MSASimulator implements Runnable, ILockable { /** The wall time tolerance ({@value} s) */ public static final double WALL_TIME_TOLERANCE = 1d; /** * The enumeration <code>ResourceState</code> describes the different states in which a resource can be. * <p> * Creation date: Mar 16, 2012 - 1:06:09 PM * * @author Victor Pillac, <a href="http://uniandes.edu.co">Universidad de Los Andes</a>-<a * href="http://copa.uniandes.edu.co">Copa</a> <a href="http://www.emn.fr">Ecole des Mines de Nantes</a>-<a * href="http://www.irccyn.ec-nantes.fr/irccyn/d/en/equipes/Slp">SLP</a> * @version 1.0 */ public static enum ResourceStates { /** State of a resource that has not started yet */ NOT_STARTED, /** State of a resource that has been stopped */ STOPPED, /** State of a resource that is waiting before starting servicing its next assigned request */ WAITING, /** State of a resource that has currently no request assigned */ IDLE, /** State of a resource that is currently servicing a request */ SERVICING, /** State of a resource that is currently busy */ BUSY }; private final MSABase<?, ?> mMSA; private final PriorityBlockingQueue<ScheduledEvent> mFutureEvents; private final LinkedList<ScheduledEvent> mExecutedEvents; private final Stopwatch mWallTimer; private final double mSpeed; private boolean mRunning; private boolean mPaused; private final Map<Integer, ResourceState> mResourceStates; /** * Getter for <code>MSA</code> to which this simulator is attached * * @return the MSA */ public MSABase<?, ?> getMSA() { return mMSA; } /** * Returns {@code true} if this simulator is currently running, {@code false} otherwise * * @return {@code true} if this simulator is currently running, {@code false} otherwise */ public boolean isRunning() { return mRunning; } /** * Creates a new <code>MSASimulator</code> * * @param msa * the msa procedure used in this simulation * @param expectedEventCount * the expected number of events * @param speed * the speed of the simulation ({@code simTime=clockTime*speed} ) */ public MSASimulator(MSABase<?, ?> msa, int expectedEventCount, double speed) { mMSA = msa; mSpeed = speed; mWallTimer = new Stopwatch(); mRunning = false; mFutureEvents = new PriorityBlockingQueue<ScheduledEvent>(expectedEventCount); mExecutedEvents = new LinkedList<MSASimulator.ScheduledEvent>(); mResourceStates = new HashMap<>(); mLock = new ExtendedReentrantLock(); } /** * Returns the last event that was pushed to the MSA * * @return the last event that was pushed to the MSA */ public ScheduledEvent getLastEvent() { return mExecutedEvents.isEmpty() ? null : mExecutedEvents.getLast(); } /** * Returns the last but one (penultimate) event that was pushed to the MSA * * @return the last but one event that was pushed to the MSA */ public ScheduledEvent getPenultimateEvent() { Iterator<ScheduledEvent> it = mExecutedEvents.iterator(); if (!it.hasNext()) return null; it.next(); return it.hasNext() ? it.next() : null; } @Override public void run() { start(); while (mRunning) { ScheduledEvent e = null; try { e = pollNextEvent(); acquireLock(); if (e != null) { pause(); // We check that the event is AFTER the last event // We ensure that the simulation time is correct adjustSimulationTime(e.time()); MSALogging.getSimulationLogger().info( "MSASimulator.run: Current time:%.1f - Raising event %s", simulationTime(), e); pushEventToMSA(e); mExecutedEvents.add(e); releaseLock(); } } catch (Exception e1) { MSALogging.getSimulationLogger().exception("MSASimulator.run", e1); getMSA().stop(); stop(); } } stop(); } /** * Run this simulator in a new thread */ public void runInNewThread() { Thread t = new Thread(this, "msa-sim"); t.start(); } private void start() { if (isRunning()) throw new IllegalStateException("Already started"); mWallTimer.start(); mWallTimer.pause(); mRunning = true; mPaused = true; MSALogging.getSimulationLogger().info( "MSASimulator.run: Current time:%.1f - Simulator started (paused)", simulationTime()); } /** * Stop the simulation */ public void stop() { if (isRunning()) { mRunning = false; mWallTimer.stop(); MSALogging.getSimulationLogger().info( "MSASimulator.run: Current time:%.1f - Simulator stopped", simulationTime()); } } /** * Pause the simulation */ public void pause() { checkLock(); if (isRunning() && !mPaused) { mWallTimer.pause(); mPaused = true; MSALogging.getSimulationLogger().info( "MSASimulator.run: Current time:%.1f - Simulator paused", simulationTime()); } } /** * Resume the simulation */ public void resume() { checkLock(); if (isRunning() && mPaused) { mWallTimer.resume(); mPaused = false; MSALogging.getSimulationLogger().info( "MSASimulator.run: Current time:%.1f - Simulator resumed", simulationTime()); } } /** * Sets the wall timer to adjust it to a given simulation time * * @param time * the simulation time to be set */ public void adjustSimulationTime(double time) { checkLock(); ScheduledEvent le = getPenultimateEvent(); if (le != null && time < le.time() - simTimeTolerance()) { throw new IllegalStateException( String.format( "MSASimulator.adjustSimulationTime: the given time is before the last event (time:%.3f last:%.3f) - Stopping the MSA", time, getLastEvent())); } else { mWallTimer.setAccumulatedTime((long) (time / getSpeed() * Stopwatch.NS_IN_S)); } } /** * Schedule an event in the simulation * * @param e * the event to be schedule * @param time * the time at which the event will be scheduled * @throws IllegalStateException * if {@code time} is in the past */ public synchronized void scheduleEvent(MSAEvent e) { if (e.getSimulationTimeStamp() < simulationTime()) throw new IllegalStateException(String.format( "The specified time (%.1f) in in the past (current is:%.1f)", e.getSimulationTimeStamp(), simulationTime())); mFutureEvents.offer(new ScheduledEvent(e)); MSALogging.getSimulationLogger().info( "MSASimulator.schedule event: scheduled %s at %.2f [%s] (%s)", e.getClass().getSimpleName(), e.getSimulationTimeStamp(), Utilities.Time.millisecondsToString( ((long) (e.getSimulationTimeStamp() * 1000 / getSpeed())), 4, true, false), e); } /** * Remove some events from the queue * * @param cleaner * the {@link IEventQueueCleaner} that will be used to determine which events should be removed */ public synchronized void cleanQueue(IEventQueueCleaner cleaner) { if (!mPaused) throw new IllegalStateException("The simulator should be paused"); Iterator<ScheduledEvent> it = mFutureEvents.iterator(); while (it.hasNext()) { ScheduledEvent e = it.next(); if (cleaner.remove(e)) { it.remove(); } } } /** * Wait until the simulation time has reached the next event * * @return the next event * @throws InterruptedException */ private ScheduledEvent pollNextEvent() throws InterruptedException { while (mRunning && (mFutureEvents.isEmpty() || mFutureEvents.peek().time() > simulationTime())) { Thread.sleep(50); } if (!mRunning) return null; else return mFutureEvents.poll(); } /** * Push an event to the MSA * * @param e * the event to be pushed */ private void pushEventToMSA(ScheduledEvent e) { mMSA.getEventFactory().raiseEvent(e.getMSAEvent()); } /** * Returns the speed of the simulation * <p> * Simulation time is defined as follows: {@code simTime=wallTimeSeconds * speed} * * @return the speed of the simulation */ public double getSpeed() { return mSpeed; } /** * Converts a time (in seconds) from wall time to simulation time * * @param time * the wall time * @return the corresponding simulation time */ public double wallToSimTime(double time) { return time * mSpeed; } /** * Converts a time (in seconds) from simulation time to wall time * * @param time * the simulation time * @return the corresponding wall time */ public double simToWallTime(double time) { return time / mSpeed; } /** * Returns the tolerance in simulation time: two events scheduled with a difference of times lower than this value * will be considered as simultaneous. * * @return the tolerance in simulation time * @see #WALL_TIME_TOLERANCE */ public double simTimeTolerance() { return wallToSimTime(WALL_TIME_TOLERANCE); } /** * Returns the current time in the simulation * * @return the current time in the simulation */ public double simulationTime() { return wallToSimTime(wallTime()); } /** * Returns the current time on the wall clock (in seconds) * * @return the current time on the wall clock */ public double wallTime() { return mWallTimer.readTimeS(); } /** * Get the current state of the resource with id {@code resourceId} * * @param resourceId * the id of the considered resource * @return the state associated with the specified resource */ public ResourceState getState(int resourceId) { ResourceState state = mResourceStates.get(resourceId); if (state == null) { state = new ResourceState(resourceId); mResourceStates.put(resourceId, state); } return state; } @Override public String toString() { return String.format("Wall time: %.3f, Sim time: %.3f, %s Pending events, Resources: %s", wallTime(), simulationTime(), mFutureEvents.size(), mResourceStates.toString()); } /** * The class <code>ScheduledEvent</code> contains a {@link MSAEvent} and a time at which the event should be sent to * the MSA procedure * <p> * Creation date: Feb 9, 2012 - 5:03:32 PM * * @author Victor Pillac, <a href="http://uniandes.edu.co">Universidad de Los Andes</a>-<a * href="http://copa.uniandes.edu.co">Copa</a> <a href="http://www.emn.fr">Ecole des Mines de Nantes</a>-<a * href="http://www.irccyn.ec-nantes.fr/irccyn/d/en/equipes/Slp">SLP</a> * @version 1.0 */ public class ScheduledEvent implements Comparable<ScheduledEvent> { private final MSAEvent mMSAEvent; /** * Return the time at which this event is scheduled * * @return the time at which this event is scheduled * @see MSAEvent#getSimulationTimeStamp() */ public double time() { return mMSAEvent.getSimulationTimeStamp(); } public MSAEvent getMSAEvent() { return mMSAEvent; } private ScheduledEvent(MSAEvent event) { mMSAEvent = event; } @Override public int compareTo(ScheduledEvent o) { return Double.compare(time(), o.time()); } @Override public String toString() { return String.format("%.3f:%s", time(), getMSAEvent()); } } /** * The class <code>ResourceState</code> describe the current state of a resource * <p> * Creation date: Mar 16, 2012 - 1:16:18 PM * * @author Victor Pillac, <a href="http://uniandes.edu.co">Universidad de Los Andes</a>-<a * href="http://copa.uniandes.edu.co">Copa</a> <a href="http://www.emn.fr">Ecole des Mines de Nantes</a>-<a * href="http://www.irccyn.ec-nantes.fr/irccyn/d/en/equipes/Slp">SLP</a> * @version 1.0 */ public class ResourceState extends Observable { private final int mResourceId; private ResourceStates mState; private final LinkedList<IActualRequest> mServedRequests; private final List<IActualRequest> mServedRequestsView; private final LinkedList<IActualRequest> mAssignedRequests; private final List<IActualRequest> mAssignedRequestsView; /** * Creates a new <code>ResourceState</code> * * @param resourceId * the id of the resource that will be associated to this state */ public ResourceState(int resourceId) { mResourceId = resourceId; mState = ResourceStates.NOT_STARTED; mServedRequests = new LinkedList<>(); mServedRequestsView = Collections.unmodifiableList(mServedRequests); mAssignedRequests = new LinkedList<>(); mAssignedRequestsView = Collections.unmodifiableList(mAssignedRequests); } /** * Returns the id of the resource which state is describe in this instance * * @return the id of the resource */ public int getResourceId() { return mResourceId; } /** * Returns the current state of the associated resource * * @return the current state of the associated resource */ public ResourceStates getState() { return mState; } /** * Sets the current state of the associated resource * * @param state * the current state of the associated resource */ public void setState(ResourceStates state) { mState = state; setChanged(); notifyObservers(state); } /** * Returns a view of the list of requests served by the associated resource * * @return a view of the list of requests served by the associated resource */ public List<IActualRequest> getServedRequests() { return mServedRequestsView; } /** * Add a request to the list of requests served by this resource * * @param r * the served request */ public void addServedRequest(IActualRequest r) { mServedRequests.add(r); setChanged(); notifyObservers(r); } /** * Returns a view of the list of requests assigned to associated resource * * @return a view of the list of requests assigned to associated resource */ public List<IActualRequest> getAssignedRequests() { return mAssignedRequestsView; } /** * Add a request to the list of requests assigned to this resource * * @param req * the request */ public void addAssignedRequest(IActualRequest req) { mAssignedRequests.add(req); setChanged(); notifyObservers(req); } /** * Removes a request from the list of requests assigned to this resource * * @param req * the request * @return {@code true} if the list contained {@code req} */ public boolean removeAssignedRequest(IActualRequest req) { boolean b = mAssignedRequests.remove(req); setChanged(); notifyObservers(req); return b; } @Override public String toString() { return String.format("%s:%s(served:%s assigned:%s)", getResourceId(), getState(), Utilities.toShortString(getServedRequests()), Utilities.toShortString(getAssignedRequests())); } } /** * The interface <code>IEventQueueCleaner</code> defines the behavior of classes that are used to remove some events * from the event queue. * <p> * Creation date: Apr 25, 2012 - 5:17:56 PM * * @author Victor Pillac, <a href="http://uniandes.edu.co">Universidad de Los Andes</a>-<a * href="http://copa.uniandes.edu.co">Copa</a> <a href="http://www.emn.fr">Ecole des Mines de Nantes</a>-<a * href="http://www.irccyn.ec-nantes.fr/irccyn/d/en/equipes/Slp">SLP</a> * @version 1.0 */ public static interface IEventQueueCleaner { /** * Check if an event should be removed * * @param event * the event to be checked * @return {@code true} if {@code event} should be removed from the queue, {@code false} otherwise */ public boolean remove(ScheduledEvent event); } // ------------------------------------ // ILockable interface implementation // ------------------------------------ /** A lock to be used by this instance */ private final ExtendedReentrantLock mLock; private boolean mSelfLock = false; @Override public boolean tryLock(long timeout) { try { return getLockInstance().tryLock(timeout, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { return false; } } @Override public void acquireLock() { try { if (!getLockInstance().tryLock(TRY_LOCK_TIMOUT, TRY_LOCK_TIMOUT_UNIT)) { throw new IllegalStateException( String.format( "Unable to acquire lock on this instance of %s (%s) after %s %s, owner: %s", this.getClass().getSimpleName(), hashCode(), TRY_LOCK_TIMOUT, TRY_LOCK_TIMOUT_UNIT, getLockInstance().getOwnerName())); } } catch (InterruptedException e) { throw new IllegalStateException(String.format( "Unable to acquire lock on this instance of %s (%s)", this.getClass() .getSimpleName(), hashCode()), e); } ; } @Override public void releaseLock() { if (mLock.isLocked()) { this.mLock.unlock(); } } public void internalReleaseLock() { if (mSelfLock) { releaseLock(); mSelfLock = false; } } @Override public boolean isLockOwnedByCurrentThread() { return this.mLock.isHeldByCurrentThread(); } @Override public ExtendedReentrantLock getLockInstance() { return this.mLock; } /** * Check the lock state of this object * * @return <code>true</code> if there was no previous lock and lock was acquired, <code>false</code> if the lock was * already owned by the current thread * @throws ConcurrentModificationException */ public boolean checkLock() throws ConcurrentModificationException { if (!isLockOwnedByCurrentThread() && mLock.isLocked()) { throw new ConcurrentModificationException( String.format( "The current thread (%s) does not have the lock on this instance of %s, owner: %s", Thread.currentThread(), this.getClass().getSimpleName(), getLockInstance().getOwnerName())); } else if (!mLock.isLocked()) { acquireLock(); mSelfLock = true; return true; } else { mSelfLock = false; return false; } } // ------------------------------------ public static class DuplicateEventCleaner<T extends MSAEvent> implements IEventQueueCleaner { private final Class<T> mClass; private final double mTolerance; private ScheduledEvent mLastEvent; /** * Creates a new <code>DuplicateEventCleaner</code> * * @param clazz * @param tolerance * a tolerance in simulation time units under which two events are considered to happen at the same * time */ public DuplicateEventCleaner(Class<T> clazz, double tolerance) { super(); this.mClass = clazz; this.mTolerance = tolerance; } @Override public boolean remove(ScheduledEvent event) { boolean remove = false; if (event.getMSAEvent() != null && mClass.isAssignableFrom(event.getMSAEvent().getClass())) { if (mLastEvent != null) { if (Math.abs(mLastEvent.time() - event.time()) < mTolerance && isDuplicate(mLastEvent, event)) { remove = true; } } mLastEvent = event; } return remove; } /** * Return {@code true} if {@code event1} and {@code event2} can be considered identical * * @param event1 * @param event2 * @return {@code true} if {@code event1} and {@code event2} can be considered identical */ protected boolean isDuplicate(ScheduledEvent event1, ScheduledEvent event2) { return true; } } }