AdaptiveTimeout.java

package de.uka.ipd.sdq.beagle.core.timeout;

/**
 * Implements an adaptive timeout based on the ageing algorithm. This means that later
 * calls to {@link #isReached()} will return {@code false} if the previous calls took a
 * long time, too.
 *
 * @author Christoph Michelbach
 */
public class AdaptiveTimeout extends ExecutionTimeBasedTimeout {

	/**
	 * The α value of the ageing algorithm. The higher this value, the bigger the
	 * influence of the newly measured value. Must be ∈ (0,1).
	 */
	private static final double AGEING_ALPHA = 0.5;

	/**
	 * How much additional time is always tolerated. Stated in milliseconds.
	 */
	private static final long CONSTANT_ADDITIONAL_TIME_TOLEARANCE = 5 * 60 * 1000;

	/**
	 * How much additional time is always tolerated. Relative to the calculated time
	 * tolerance (without {@link #CONSTANT_ADDITIONAL_TIME_TOLEARANCE}).
	 */
	private static final double MULTIPLICATIVE_ADDITIONAL_TIME_TOLEARANCE = 0.3;

	/**
	 * The start value for the ageing algorithm.
	 */
	private static final long AGEING_START_VALUE = 3600 * 1000;

	/**
	 * The previous maximally tolerable time in milliseconds or {@code -1} to indicate
	 * that there is none.
	 */
	private long previousMaximallyTolerableTime = -1;

	/**
	 * The maximally tolerable time in milliseconds or {@code -1} to indicate that there
	 * is none.
	 */
	private long maximallyTolerableTime = AGEING_START_VALUE;

	/**
	 * When {@link #isReached()} has been called the last time. Stated in milliseconds.
	 */
	private long timeOfPreviousCall;

	@Override
	public boolean isReached() {
		return this.timeOfPreviousCall + this.maximallyTolerableTime - System.currentTimeMillis() < 0;
	}

	@Override
	public synchronized void reportOneStepProgress() {
		this.previousMaximallyTolerableTime = this.maximallyTolerableTime;
		final long tellingTime = this.timeOfPreviousCall - System.currentTimeMillis();
		this.timeOfPreviousCall = System.currentTimeMillis();

		final long preliminaryMaximallyTolerableTimeWithoutAdditionalTime =
			(long) (this.previousMaximallyTolerableTime * (1 - AGEING_ALPHA) + tellingTime * AGEING_ALPHA);
		this.maximallyTolerableTime = (long) (preliminaryMaximallyTolerableTimeWithoutAdditionalTime
			* (1 + MULTIPLICATIVE_ADDITIONAL_TIME_TOLEARANCE)) + CONSTANT_ADDITIONAL_TIME_TOLEARANCE;

	}

	@Override
	protected void implementationInit() {
		assert AGEING_ALPHA > 0 && AGEING_ALPHA < 1;

		this.timeOfPreviousCall = System.currentTimeMillis();
		new Thread(this::notifyOnReachedTimeout).start();
	}

	/**
	 * Calls the callback handlers once the timeout is reached.
	 */
	private void notifyOnReachedTimeout() {
		assert this.maximallyTolerableTime != -1;

		long timeToSleep = this.timeOfPreviousCall + this.maximallyTolerableTime - System.currentTimeMillis();

		// Wait until the timeout is up.
		while (!this.isReached()) {
			assert timeToSleep > 0;

			try {
				Thread.sleep(timeToSleep);
			} catch (final InterruptedException exception) {
				// Retry on interrupt. No handling is needed because the loop just tries
				// again.
			}

			/**
			 * This has to be done at the end of the loop, not at the beginning. Otherwise
			 * the timeout can be reached right before the first instruction in the loop
			 * body but not in the loop header causing {@code timeToSleep} to become
			 * negative. This would be an illegal argument for {@link Thread#sleep(long)}.
			 */
			timeToSleep = this.timeOfPreviousCall + this.maximallyTolerableTime - System.currentTimeMillis();
		}

		for (final Runnable callback : this.callbacks) {
			new Thread(callback).start();
		}
	}
}