package org.couchbase.mock.memcached.errormap;
import org.couchbase.mock.memcached.MemcachedServer.CommandLogEntry;
import java.util.ArrayList;
import java.util.List;
/**
* Created by mnunberg on 4/12/17.
*/
public abstract class Verifier {
public static final String STRATEGY_EXPONENTIAL = "exponential";
public static final String STRATEGY_LINEAR = "linear";
public static final String STRATEGY_CONSTANT = "constant";
// How much "fuzz" to allow in testing, when considering any slowness in the mock.
public static final long FUZZ_MS = 10;
protected CommandLogEntry firstEntry = null;
public static class VerificationException extends Exception {
public VerificationException(String message) {
super(message);
}
}
static class FirstRetryTooSoonException extends VerificationException {
public FirstRetryTooSoonException() {
super("Initial retry does not honor `after`");
}
}
static class DurationExceededException extends VerificationException {
public DurationExceededException(long expected, long actual) {
super("Retries lasted for too long (`max-duration`). Expected: " + expected + ". Actual: " + actual);
}
}
protected void verify(List<CommandLogEntry> entries, RetrySpec spec) throws VerificationException {
if (entries.size() < 2) {
throw new VerificationException("No commands executed: Log has " + entries.size());
}
firstEntry = entries.get(0);
entries.remove(0);
long beginTime = entries.get(0).getMsTimestamp();
long endTime = entries.get(entries.size()-1).getMsTimestamp();
long duration = endTime - beginTime;
if (duration > spec.getMaxDuration() + FUZZ_MS) {
throw new DurationExceededException(spec.getMaxDuration(), duration);
}
if (entries.get(0).getMsTimestamp() - firstEntry.getMsTimestamp() < spec.getAfter() - 1) {
throw new FirstRetryTooSoonException();
}
verifyImpl(entries, spec);
}
abstract protected void verifyImpl(List<CommandLogEntry> entries, RetrySpec spec) throws VerificationException;
public static class ConstantVerifier extends Verifier {
@Override
protected void verifyImpl(List<CommandLogEntry> entries, RetrySpec spec) throws VerificationException {
CommandLogEntry last = null;
// Iterate through each log entry. There should be _interval_ between retries.
for (CommandLogEntry ent : entries) {
if (last == null) {
last = ent;
continue;
}
long duration = ent.getMsTimestamp() - last.getMsTimestamp();
if (Math.abs(duration - spec.getInterval()) > FUZZ_MS) {
throw new VerificationException("Too much spacing between intervals: " + duration + ". Expected: " + spec.getInterval());
}
last = ent;
}
// Determine when our *last* retry attempt is supposed to be. This is to ensure
// that we're not skimping on retries.
long lastRetryExpected = entries.get(0).getMsTimestamp() + spec.getMaxDuration();
lastRetryExpected -= spec.getInterval();
// We should tolerate the client skipping the last beat
long lastIntervalMaxDiff = FUZZ_MS;
assert last != null;
if (Math.abs(last.getMsTimestamp() - lastRetryExpected) > lastIntervalMaxDiff) {
throw new VerificationException(
String.format("Not enough/too many retries. Last TS=%d. Last expected=%d. Diff=%d. MaxDiff=%d",
last.getMsTimestamp(), lastRetryExpected, Math.abs(lastRetryExpected - last.getMsTimestamp()),
lastIntervalMaxDiff));
}
}
}
public static class LinearVerifier extends Verifier {
@Override
protected void verifyImpl(List<CommandLogEntry> entries, RetrySpec spec) throws VerificationException{
// Iterate through each log entry.
for (int i = 1; i < entries.size(); i++) {
long duration = entries.get(i).getMsTimestamp() - entries.get(i-1).getMsTimestamp();
long expectedDuration = spec.getInterval() * i;
expectedDuration = Math.min(spec.getCeil(), expectedDuration);
if (Math.abs(duration - expectedDuration) > FUZZ_MS) {
throw new VerificationException("Linear backoff failed!. " + " duration: " + duration + ", expected: " + expectedDuration);
}
}
// TODO: I don't know how to calculate the last expected retry
}
}
public static class ExponentialVerifier extends Verifier {
@Override
protected void verifyImpl(List<CommandLogEntry> entries, RetrySpec spec) throws VerificationException {
for (int i = 1; i < entries.size(); i++) {
long duration = entries.get(i).getMsTimestamp() - entries.get(i-1).getMsTimestamp();
long expectedDuration = (long)Math.pow((double)spec.getInterval(), (double)i);
if (spec.getCeil() > 0) {
expectedDuration = Math.min(spec.getCeil(), expectedDuration);
}
if (Math.abs(duration - expectedDuration) > FUZZ_MS) {
throw new VerificationException("Exponential backoff failed. Duration: " + duration + ", expected: " + expectedDuration);
}
}
}
}
public static void verifyThrow(List<CommandLogEntry> allEntries, RetrySpec spec, int opcode) throws VerificationException {
List<CommandLogEntry> entries = new ArrayList<CommandLogEntry>();
for (CommandLogEntry entry : allEntries) {
if (entry.getOpcode() == opcode) {
entries.add(entry);
}
}
Verifier verifier;
if (spec.getStrategy().equals(STRATEGY_CONSTANT)) {
verifier = new ConstantVerifier();
} else if (spec.getStrategy().equals(STRATEGY_LINEAR)) {
verifier = new LinearVerifier();
} else if (spec.getStrategy().equals(STRATEGY_EXPONENTIAL)) {
verifier = new ExponentialVerifier();
} else {
throw new RuntimeException("No such verifier strategy!");
}
verifier.verify(entries, spec);
}
public static boolean verify(List<CommandLogEntry> entries, RetrySpec spec, int opcode) {
try {
verifyThrow(entries, spec, opcode);
return true;
} catch (VerificationException ex) {
return false;
}
}
}