/*
* Copyright 2015 Mark Michaelis
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.github.mmichaelis.hamcrest.nextdeed.concurrent;
import static java.util.Objects.requireNonNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.MoreObjects;
import com.google.common.base.Predicate;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.TimeUnit;
/**
* <p>
* A function that waits before returning its result until a given
* predicate is met. The predicate will be applied to the return value
* of delegate function which will be queried continuously for the expected
* outcome.
* </p>
*
* @since 1.0.0
*/
public class WaitFunction<T, R> implements Function<T, R> {
/**
* Minimum time to sleep between polls.
*
* @since 1.0.0
*/
private static final int MINIMUM_SLEEP_TIME_MS = 1;
/**
* Function to continuously determine a result until it matches the
* expectations denoted by {@link #predicate}.
*
* @since 1.0.0
*/
@NotNull
private final Function<T, R> delegateFunction;
/**
* Predicate to determine if the retrieved result by the given function meets
* the expectations.
*
* @since 1.0.0
*/
@NotNull
private final Predicate<? super R> predicate;
/**
* Function to determine what to do on timeout.
*
* @since 1.0.0
*/
@NotNull
private final Function<WaitTimeoutEvent<T, R>, R> onTimeoutFunction;
/**
* Amount of timeout.
*
* @see #timeoutTimeUnit
* @since 1.0.0
*/
private final long timeout;
/**
* Unit for timeout.
*
* @see #timeout
* @since 1.0.0
*/
@NotNull
private final TimeUnit timeoutTimeUnit;
private final long gracePeriod;
@NotNull
private final TimeUnit gracePeriodTimeUnit;
private final long initialDelay;
@NotNull
private final TimeUnit initialDelayTimeUnit;
private final double decelerationFactor;
WaitFunction(@NotNull Function<T, R> delegateFunction,
@NotNull Predicate<? super R> predicate,
@NotNull Function<WaitTimeoutEvent<T, R>, R> onTimeoutFunction,
long timeout,
@NotNull TimeUnit timeoutTimeUnit,
long gracePeriod,
@NotNull TimeUnit gracePeriodTimeUnit,
long initialDelay,
@NotNull TimeUnit initialDelayTimeUnit,
double decelerationFactor) {
this.delegateFunction = requireNonNull(delegateFunction, "delegateFunction must not be null.");
this.predicate = requireNonNull(predicate, "predicate must not be null.");
this.onTimeoutFunction = requireNonNull(onTimeoutFunction,
"onTimeoutFunction must not be null.");
this.timeout = timeout;
this.timeoutTimeUnit = requireNonNull(timeoutTimeUnit, "timeoutTimeUnit must not be null.");
this.gracePeriod = gracePeriod;
this.gracePeriodTimeUnit = requireNonNull(gracePeriodTimeUnit,
"gracePeriodTimeUnit must not be null.");
this.initialDelay = initialDelay;
this.initialDelayTimeUnit = requireNonNull(initialDelayTimeUnit,
"initialDelayTimeUnit must not be null.");
this.decelerationFactor = decelerationFactor;
}
public static <T, R> WaitFunctionBuilder<T, R> waitFor(@NotNull Function<T, R> delegateFunction) {
return new WaitFunctionBuilderImpl<>(delegateFunction);
}
@Override
public R apply(T item) {
long timeoutMs = TimeUnit.MILLISECONDS.convert(timeout, timeoutTimeUnit);
long startMs = nowMillis();
long deadlineTimeMs = startMs + timeoutMs;
// At first, wait some initial delay between checks.
long delay = TimeUnit.MILLISECONDS.convert(initialDelay, initialDelayTimeUnit);
R result;
while (true) {
long beforeEvaluationTimeMs = nowMillis();
result = delegateFunction.apply(item);
long afterEvaluationTimeMs = nowMillis();
if (predicate.apply(result)) {
break;
}
if (afterEvaluationTimeMs > deadlineTimeMs) {
return onTimeoutFunction.apply(
new WaitTimeoutEventImpl<>(this,
afterEvaluationTimeMs - startMs,
item,
result
)
);
}
delay =
sleepAndRecalculateDelay(delay, deadlineTimeMs, beforeEvaluationTimeMs,
afterEvaluationTimeMs);
}
return result;
}
@NotNull
public Function<T, R> getDelegateFunction() {
return delegateFunction;
}
@NotNull
public Predicate<? super R> getPredicate() {
return predicate;
}
public long getTimeout() {
return timeout;
}
@NotNull
public TimeUnit getTimeoutTimeUnit() {
return timeoutTimeUnit;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("hash", Integer.toHexString(System.identityHashCode(this)))
.add("decelerationFactor", decelerationFactor)
.add("delegateFunction", delegateFunction)
.add("gracePeriod", gracePeriod)
.add("gracePeriodTimeUnit", gracePeriodTimeUnit)
.add("initialDelay", initialDelay)
.add("initialDelayTimeUnit", initialDelayTimeUnit)
.add("onTimeoutFunction", onTimeoutFunction)
.add("predicate", predicate)
.add("timeout", timeout)
.add("timeoutTimeUnit", timeoutTimeUnit)
.toString();
}
/**
* Sleep the given number of milliseconds.
*
* @param millis how long to sleep
* @throws InterruptedException if the current thread has been interrupted
* @since 0.1.0
*/
@VisibleForTesting
void sleep(long millis) throws InterruptedException {
Thread.sleep(millis);
}
/**
* Retrieve the current time in milliseconds. Especially allows to override this behavior for
* testing purpose.
*
* @return time in milliseconds
* @since 1.0.0
*/
@VisibleForTesting
long nowMillis() {
return System.currentTimeMillis();
}
/**
* <p>
* Decelerating wait. Decreases the polling interval over time to give the system under test a
* chance to
* actually reach the desired state.
* </p>
*
* @since 1.0.0
*/
private long sleepAndRecalculateDelay(long previousDelay,
long deadlineTimeMs,
long beforeEvaluationTimeMs,
long afterEvaluationTimeMs) {
long newDelay = previousDelay;
// Leave at least as much time between two checks as the check itself took.
long lastDuration = afterEvaluationTimeMs - beforeEvaluationTimeMs;
if (lastDuration > newDelay) {
newDelay = lastDuration;
}
// Wait, but not much longer than until the deadlineTimeMillis and at least a millisecond.
try {
long gracePeriodMs = TimeUnit.MILLISECONDS.convert(gracePeriod, gracePeriodTimeUnit);
long timeLeftMs = deadlineTimeMs - afterEvaluationTimeMs;
long sleepTimeMs = Math.max(MINIMUM_SLEEP_TIME_MS,
Math.min(
newDelay,
timeLeftMs + gracePeriodMs
)
);
sleep(sleepTimeMs);
} catch (InterruptedException e) {
throw new IllegalStateException("Unexpected interruption.", e);
}
// Make checks less and less frequently.
// Increase the wait period using the deceleration factor, but
// wait at least one millisecond longer next time.
newDelay = Math.max(newDelay + 1, (long) (newDelay * decelerationFactor));
return newDelay;
}
}