package io.nextop.client.retry; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Random; import java.util.concurrent.TimeUnit; /** Functional interface for send retry. * Strategies are immutable and can be shared, used across multiple threads. */ public interface SendStrategy { // FIXME clarify usage of this interface // FIXME - retry // FIXME - isSend() // FIXME - if true, getDelay // FIXME rename onRetry SendStrategy retry(); boolean isSend(); // FIXME rename getSendDelay long getDelay(TimeUnit timeUnit); // common strategies SendStrategy INDEFINITE = new Builder().init(0, TimeUnit.MILLISECONDS ).withUniformRandom(1, TimeUnit.SECONDS ).repeat(5 ).withUniformRandom(10, TimeUnit.SECONDS ).repeatIndefinitely( ).build(); SendStrategy NO_RETRY = new Builder().init(0, TimeUnit.MILLISECONDS ).repeat(0 ).build(); public static final class Builder { private Node head = new Node(); private List<Node> sequence = new ArrayList<Node>(4); public Builder() { } public Builder init(long delay, TimeUnit timeUnit) { head.type = Node.Type.UNIFORM; head.initMs = timeUnit.toMillis(delay); return this; } public Builder max(long delay, TimeUnit timeUnit) { head.maxMs = timeUnit.toMillis(delay); return this; } public Builder withUniformRandom(long delay, TimeUnit timeUnit) { head.type = Node.Type.UNIFORM_RANDOM; head.delayMs = timeUnit.toMillis(delay); return this; } public Builder withLinearRandom(long delayPerStep, TimeUnit timeUnit) { head.type = Node.Type.LINEAR_RANDOM; head.delayMs = timeUnit.toMillis(delayPerStep); return this; } public Builder withExponentialRandom(float factorPerStep) { head.type = Node.Type.LINEAR_RANDOM; head.factor = factorPerStep; return this; } public Builder repeat(int count) { if (count < 0) { throw new IllegalArgumentException(); } head.count = count; sequence.add(new Node(head)); head.count = 0; return this; } public Builder repeatIndefinitely() { // FIXME return repeat(Integer.MAX_VALUE); } public SendStrategy build() { if (sequence.isEmpty()) { throw new IllegalStateException(); } // build in reverse @Nullable SendStrategy tail = null; for (int i = sequence.size() - 1; 0 <= i; --i) { tail = create(sequence.get(i), tail); } return tail; } SendStrategy create(Node node, @Nullable SendStrategy after) { switch (node.type) { case UNIFORM: return new UniformSendStrategy(node.initMs, node.maxMs, node.count, after); case UNIFORM_RANDOM: return new UniformRandomSendStrategy(node.initMs, node.maxMs, node.count, after, node.delayMs, RandomSequence.create(new Random())); case LINEAR_RANDOM: return new LinearRandomSendStrategy(node.initMs, node.maxMs, node.count, after, node.delayMs, RandomSequence.create(new Random())); case EXPONENTIAL_RANDOM: return new ExponentialRandomSendStrategy(node.initMs, node.maxMs, node.count, after, node.factor, RandomSequence.create(new Random())); default: throw new IllegalArgumentException(); } } private static final class Node { static enum Type { UNIFORM, UNIFORM_RANDOM, LINEAR_RANDOM, EXPONENTIAL_RANDOM } Type type; long initMs; long maxMs; long delayMs; float factor; int count; Node() { type = Type.UNIFORM; initMs = 0; maxMs = -1; delayMs = 0; factor = 1.f; count = 0; } Node(Node copy) { type = copy.type; initMs = copy.initMs; maxMs = copy.maxMs; delayMs = copy.delayMs; factor = copy.factor; count = copy.count; } } private static abstract class AbstractSendStrategy implements SendStrategy { int countDown; long initMs; long maxMs; @Nullable SendStrategy after; AbstractSendStrategy(long initMs, long maxMs, int countDown, @Nullable SendStrategy after) { this.initMs = initMs; this.maxMs = maxMs; this.countDown = countDown; this.after = after; } @Override public boolean isSend() { return 0 < countDown; } } private static final class UniformSendStrategy extends AbstractSendStrategy { UniformSendStrategy(long initMs, long maxMs, int countDown, @Nullable SendStrategy after) { super(initMs, maxMs, countDown, after); } @Override public long getDelay(TimeUnit timeUnit) { return Math.min(TimeUnit.MILLISECONDS.convert(initMs, timeUnit), TimeUnit.MILLISECONDS.convert(maxMs, timeUnit)); } @Override public SendStrategy retry() { if (0 < countDown) { return new UniformSendStrategy(initMs, maxMs, countDown - 1, after); } else { return after; } } } private static final class UniformRandomSendStrategy extends AbstractSendStrategy { long delayMs; RandomSequence r; UniformRandomSendStrategy(long initMs, long maxMs, int countDown, @Nullable SendStrategy after, long delayMs, RandomSequence r) { super(initMs, maxMs, countDown, after); this.delayMs = delayMs; this.r = r; } @Override public long getDelay(TimeUnit timeUnit) { return Math.min(TimeUnit.MILLISECONDS.convert(initMs + Math.round(r.floatValue() * delayMs), timeUnit), TimeUnit.MILLISECONDS.convert(maxMs, timeUnit)); } @Override public SendStrategy retry() { if (1 < countDown) { return new UniformRandomSendStrategy(initMs, maxMs, countDown - 1, after, delayMs, r); } else { return after; } } } private static final class LinearRandomSendStrategy extends AbstractSendStrategy { long delayMs; RandomSequence r; LinearRandomSendStrategy(long initMs, long maxMs, int countDown, @Nullable SendStrategy after, long delayMs, RandomSequence r) { super(initMs, maxMs, countDown, after); this.delayMs = delayMs; this.r = r; } @Override public long getDelay(TimeUnit timeUnit) { return Math.min(TimeUnit.MILLISECONDS.convert(initMs + Math.round(r.floatValue() * delayMs), timeUnit), TimeUnit.MILLISECONDS.convert(maxMs, timeUnit)); } @Override public SendStrategy retry() { if (1 < countDown) { return new LinearRandomSendStrategy(initMs + Math.round(r.floatValue() * delayMs), maxMs, countDown - 1, after, delayMs, r); } else { return after; } } } private static final class ExponentialRandomSendStrategy extends AbstractSendStrategy { float factor; RandomSequence r; ExponentialRandomSendStrategy(long initMs, long maxMs, int countDown, @Nullable SendStrategy after, float factor, RandomSequence r) { super(initMs, maxMs, countDown, after); this.factor = factor; this.r = r; } @Override public long getDelay(TimeUnit timeUnit) { return Math.min(TimeUnit.MILLISECONDS.convert(Math.round(r.floatValue() * initMs * factor), timeUnit), TimeUnit.MILLISECONDS.convert(maxMs, timeUnit)); } @Override public SendStrategy retry() { if (1 < countDown) { return new ExponentialRandomSendStrategy(Math.round(r.floatValue() * initMs * factor), maxMs, countDown - 1, after, factor, r); } else { return after; } } } } }