/*
* Copyright 2013-2017 the original author or authors.
*
* 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 org.cloudfoundry.util;
import org.atteo.evo.inflector.English;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.time.Instant;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* Utilities for delaying progress
*/
public final class DelayUtils {
private static final Logger LOGGER = LoggerFactory.getLogger("cloudfoundry-client.delay");
private DelayUtils() {
}
/**
* Implements an exponential backoff delay for use with {@link Mono#repeatWhenEmpty(Function)}
*
* @param minimum the minimum duration
* @param maximum the maximum duration
* @param timeout the maximum amount of time to delay for
* @return a delayed {@link Publisher}
*/
public static Function<Flux<Long>, Publisher<?>> exponentialBackOff(Duration minimum, Duration maximum, Duration timeout) {
Instant finish = Instant.now().plus(timeout);
return iterations -> getDelay(minimum, maximum, finish, iterations);
}
/**
* Implements an exponential backoff delay for use with {@link Mono#retryWhen(Function)}
*
* @param minimum the minimum duration
* @param maximum the maximum duration
* @param timeout the maximum amount of time to delay for
* @return a delayed {@link Publisher}
*/
public static Function<Flux<Throwable>, Publisher<?>> exponentialBackOffError(Duration minimum, Duration maximum, Duration timeout) {
Instant finish = Instant.now().plus(timeout);
return errors -> getDelay(minimum, maximum, finish, errors.zipWith(Flux.range(0, Integer.MAX_VALUE), (error, iteration) -> iteration.longValue()));
}
/**
* Implements an fixed delay for use with {@link Mono#repeatWhenEmpty(Function)}
*
* @param duration the duration of the delay
* @return a delayed {@link Publisher}
*/
public static Function<Flux<Long>, Publisher<?>> fixed(Duration duration) {
return iterations -> iterations
.flatMap(iteration -> Mono
.delay(duration)
.doOnSubscribe(logDelay(duration)), 1);
}
/**
* Implements an instant (no delay) for use with {@link Mono#repeatWhenEmpty(Function)}
*
* @return an instant (no delay) {@link Publisher}
*/
public static Function<Flux<Long>, Publisher<?>> instant() {
return iterations -> iterations
.flatMap(iteration -> Mono
.just(0L)
.doOnSubscribe(logDelay(Duration.ZERO)), 1);
}
private static Duration calculateDuration(Duration minimum, Duration maximum, Long iteration) {
Duration candidate = minimum.multipliedBy((long) Math.pow(2, iteration));
return min(candidate, maximum);
}
private static Flux<?> getDelay(Duration minimum, Duration maximum, Instant finish, Flux<Long> iterations) {
return iterations
.map(iteration -> calculateDuration(minimum, maximum, iteration))
.flatMap(delay -> {
if (Instant.now().isAfter(finish)) {
return Mono.error(new DelayTimeoutException());
}
return Mono
.delay(delay)
.doOnSubscribe(logDelay(delay));
}, 1);
}
private static Consumer<Subscription> logDelay(Duration delay) {
return subscription -> {
int seconds = (int) delay.getSeconds();
if (seconds > 0) {
LOGGER.debug("Delaying {} {}", seconds, English.plural("second", seconds));
return;
}
int milliseconds = (int) delay.toMillis();
LOGGER.debug("Delaying {} {}", milliseconds, English.plural("millisecond", milliseconds));
};
}
private static Duration min(Duration a, Duration b) {
return (a.compareTo(b) <= 0) ? a : b;
}
}