/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
* or http://forgerock.org/license/CDDLv1.0.html.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at legal-notices/CDDLv1_0.txt.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
* Copyright 2015 ForgeRock AS
*/
package org.opends.server.util;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import org.forgerock.util.Reject;
/**
* Timer useful for testing: it helps to write loops that repeatedly runs code until some condition
* is met.
*/
public interface TestTimer
{
/**
* Constant that can be used at the end of {@code Callable<Void>.call()} to better explicit this
* is the end of the method.
*/
Void END_RUN = null;
/**
* Repeatedly call the supplied callable (respecting a sleep interval) until:
* <ul>
* <li>it returns,</li>
* <li>it throws an exception other than {@link AssertionError},</li>
* <li>the current timer times out.</li>
* </ul>
* If the current timer times out, then it will:
* <ul>
* <li>either rethrow an {@link AssertionError} thrown by the callable,</li>
* <li>or return {@code null}.</li>
* </ul>
* <p>
* Note: The test code in the callable can be written as any test code outside a callable. In
* particular, asserts can and should be used inside the {@link Callable#call()}.
*
* @param callable
* the callable to repeat until success
* @param <R>
* The return type of the callable
* @return the value returned by the callable (may be {@code null}), or {@code null} if the timer
* times out
* @throws Exception
* The exception thrown by the provided callable
* @throws InterruptedException
* If the thread is interrupted while sleeping
*/
<R> R repeatUntilSuccess(Callable<R> callable) throws Exception, InterruptedException;
/** Builder for a {@link TestTimer}. */
public static final class Builder
{
private long maxSleepTimeInMillis;
private long sleepTimes;
/**
* Configures the maximum sleep duration.
*
* @param time
* the duration
* @param unit
* the time unit for the duration
* @return this builder
*/
public Builder maxSleep(long time, TimeUnit unit)
{
Reject.ifFalse(time > 0, "time must be positive");
this.maxSleepTimeInMillis = unit.toMillis(time);
return this;
}
/**
* Configures the duration for sleep times.
*
* @param time
* the duration
* @param unit
* the time unit for the duration
* @return this builder
*/
public Builder sleepTimes(long time, TimeUnit unit)
{
Reject.ifFalse(time > 0, "time must be positive");
this.sleepTimes = unit.toMillis(time);
return this;
}
/**
* Creates a new timer and start it.
*
* @return a new timer
*/
public TestTimer toTimer()
{
return new SteppingTimer(this);
}
}
/** A {@link TestTimer} that sleeps in steps and sleeps at maximum {@code nbSteps * sleepTimes}. */
public static class SteppingTimer implements TestTimer
{
private final long sleepTime;
private final long totalNbSteps;
private long nbStepsRemaining;
private SteppingTimer(Builder builder)
{
this.sleepTime = builder.sleepTimes;
this.totalNbSteps = sleepTime > 0 ? builder.maxSleepTimeInMillis / sleepTime : 0;
this.nbStepsRemaining = totalNbSteps;
}
/**
* Returns whether the timer has reached the timeout. This method may block by sleeping.
*
* @return {@code true} if the timer has reached the timeout, {@code false} otherwise
* @throws InterruptedException if the thread has been interrupted
*/
private boolean hasTimedOut() throws InterruptedException
{
final boolean done = hasTimedOutNoSleep();
if (!done)
{
Thread.sleep(sleepTime);
nbStepsRemaining--;
}
return done;
}
/**
* Returns whether the timer has reached the timeout, without sleep.
*
* @return {@code true} if the timer has reached the timeout, {@code false} otherwise
*/
private boolean hasTimedOutNoSleep()
{
return nbStepsRemaining <= 0;
}
@Override
public <R> R repeatUntilSuccess(Callable<R> callable) throws Exception, InterruptedException
{
do
{
try
{
return callable.call();
}
catch (AssertionError e)
{
if (hasTimedOutNoSleep())
{
throw e;
}
}
}
while (!hasTimedOut());
return null;
}
@Override
public String toString()
{
return totalNbSteps * sleepTime + " ms max sleep time"
+ " (" + totalNbSteps + " steps x " + sleepTime + " ms)"
+ ", remaining = " + nbStepsRemaining + " steps";
}
}
}