package com.jivesoftware.os.amzabot.deployable.bot;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.jivesoftware.os.amzabot.deployable.AmzaBotService;
import com.jivesoftware.os.amzabot.deployable.AmzaBotUtil;
import com.jivesoftware.os.amzabot.deployable.AmzaKeyClearingHouse;
import com.jivesoftware.os.mlogger.core.AtomicCounter;
import com.jivesoftware.os.mlogger.core.MetricLogger;
import com.jivesoftware.os.mlogger.core.MetricLoggerFactory;
import com.jivesoftware.os.mlogger.core.ValueType;
import java.util.AbstractMap;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
public class AmzaBotRandomOpService {
private static final MetricLogger LOG = MetricLoggerFactory.getLogger();
private final AmzaBotRandomOpConfig config;
private final AmzaBotService service;
private final AmzaKeyClearingHouse amzaKeyClearingHouse;
private Random RANDOM = new Random();
private ExecutorService processor = Executors.newSingleThreadExecutor(
new ThreadFactoryBuilder().setNameFormat("amzabot-randomops-%d").build());
private final AtomicBoolean running = new AtomicBoolean();
public AmzaBotRandomOpService(AmzaBotRandomOpConfig config,
AmzaBotService service,
AmzaKeyClearingHouse amzaKeyClearingHouse) {
this.config = config;
this.service = service;
this.amzaKeyClearingHouse = amzaKeyClearingHouse;
}
public ConcurrentMap<String, Integer> getKeyMap() {
return amzaKeyClearingHouse.getKeyMap();
}
public void clearKeyMap() {
amzaKeyClearingHouse.clearKeyMap();
}
public ConcurrentMap<String, Entry<Integer, Integer>> getQuarantinedKeyMap() {
return amzaKeyClearingHouse.getQuarantinedKeyMap();
}
public void clearQuarantinedKeyMap() {
amzaKeyClearingHouse.clearQuarantinedKeyMap();
}
public int randomOp(String keySeed) throws Exception {
int op = RANDOM.nextInt(7);
// 0 - read
// 1 - delete
// 2 - batch write
// 3 - batch delete
// 4 - cas
// 5,6 - write
if (op == 0) {
LOG.debug("random operation: read");
Entry<String, Integer> entry = amzaKeyClearingHouse.getRandomEntry();
if (entry == null) {
LOG.debug("Need at least one entry to read");
return op;
}
String value = service.getWithInfiniteRetry(entry.getKey(), config.getRetryWaitMs());
if (value == null) {
LOG.error("Did not find key {}", entry.getKey());
amzaKeyClearingHouse.quarantineEntry(entry, null);
} else if (entry.getValue() != value.hashCode()) {
LOG.error("Found key {}, but value differs. {} != {}",
entry.getKey(),
entry.getValue(),
value.hashCode());
amzaKeyClearingHouse.quarantineEntry(entry, value.hashCode());
service.deleteWithInfiniteRetry(entry.getKey(), config.getRetryWaitMs());
} else {
LOG.debug("Found key {}", entry.getKey());
}
} else if (op == 1) {
LOG.debug("random operation: delete");
Entry<String, Integer> entry = amzaKeyClearingHouse.getRandomEntry();
if (entry == null) {
LOG.debug("Need at least one entry to delete");
return op;
}
service.deleteWithInfiniteRetry(entry.getKey(), config.getRetryWaitMs());
Integer oldValue = amzaKeyClearingHouse.delete(entry.getKey());
if (oldValue == null) {
LOG.error("Did not find existing kv pair: {}", entry.getKey());
amzaKeyClearingHouse.quarantineEntry(
new AbstractMap.SimpleEntry<>(entry.getKey(), entry.getValue().hashCode()), null);
service.deleteWithInfiniteRetry(entry.getKey(), config.getRetryWaitMs());
}
LOG.debug("Deleted {}", entry.getKey());
} else if (op == 2) {
LOG.debug("random operation: write batch; disregard write threshold");
Set<Entry<String, String>> entries = Sets.newHashSet();
for (int i = 0; i < RANDOM.nextInt(config.getBatchFactor()); i++) {
entries.add(amzaKeyClearingHouse.genRandomEntry(keySeed + ":" + i, config.getValueSizeThreshold()));
}
service.multiSetWithInfiniteRetry(entries, config.getRetryWaitMs());
for (Entry<String, String> entry : entries) {
Integer oldValue = amzaKeyClearingHouse.set(entry.getKey(), entry.getValue());
if (oldValue != null) {
LOG.error("Found existing kv pair: {}:{}", entry.getKey(), oldValue);
amzaKeyClearingHouse.quarantineEntry(
new AbstractMap.SimpleEntry<>(entry.getKey(), entry.getValue().hashCode()), oldValue);
service.deleteWithInfiniteRetry(entry.getKey(), config.getRetryWaitMs());
}
}
LOG.debug("Wrote {} entries", entries.size());
} else if (op == 3) {
LOG.debug("random operation: delete batch; disregard write threshold");
Set<Entry<String, Integer>> entries = Sets.newHashSet();
for (int i = 0; i < RANDOM.nextInt(config.getBatchFactor()); i++) {
Entry<String, Integer> entry = amzaKeyClearingHouse.getRandomEntry();
if (entry == null) {
break;
}
entries.add(amzaKeyClearingHouse.getRandomEntry());
}
service.multiDeleteWithInfiniteRetry(entries, config.getRetryWaitMs());
for (Entry<String, Integer> entry : entries) {
Integer oldValue = amzaKeyClearingHouse.delete(entry.getKey());
if (oldValue == null) {
LOG.error("Did not find existing kv pair: {}", entry.getKey());
amzaKeyClearingHouse.quarantineEntry(
new AbstractMap.SimpleEntry<>(entry.getKey(), entry.getValue().hashCode()), null);
service.deleteWithInfiniteRetry(entry.getKey(), config.getRetryWaitMs());
}
}
LOG.debug("Deleted {} entries", entries.size());
} else if (op == 4) {
LOG.debug("random operation: cas");
Entry<String, Integer> entry = amzaKeyClearingHouse.getRandomEntry();
if (entry == null) {
LOG.debug("Need at least one entry for cas");
return op;
}
String oldPartitionValue = service.getWithInfiniteRetry(entry.getKey(), config.getRetryWaitMs());
if (oldPartitionValue == null) {
LOG.error("Did not find key {}", entry.getKey());
amzaKeyClearingHouse.quarantineEntry(entry, null);
} else if (oldPartitionValue.hashCode() != entry.getValue()) {
LOG.error("Found key {}, but value differs. {} != {}",
entry.getKey(),
entry.getValue(),
AmzaBotUtil.truncVal(oldPartitionValue));
amzaKeyClearingHouse.quarantineEntry(entry, oldPartitionValue.hashCode());
service.deleteWithInfiniteRetry(entry.getKey(), config.getRetryWaitMs());
} else {
String newValue = amzaKeyClearingHouse.genRandomValue(config.getValueSizeThreshold());
LOG.debug("Found key {}:{}. Set new value {}",
entry.getKey(), entry.getValue(), newValue.hashCode());
service.setWithInfiniteRetry(entry.getKey(), newValue, config.getRetryWaitMs());
Integer oldValue = amzaKeyClearingHouse.set(entry.getKey(), newValue);
if (oldValue == null) {
LOG.error("Did not find key {}", entry.getKey());
amzaKeyClearingHouse.quarantineEntry(
new AbstractMap.SimpleEntry<>(entry.getKey(), newValue.hashCode()), null);
service.deleteWithInfiniteRetry(entry.getKey(), config.getRetryWaitMs());
}
LOG.debug("CAS {}:{}", entry.getKey(), AmzaBotUtil.truncVal(newValue));
}
} else {
LOG.debug("random operation: write; odds are twice");
if (amzaKeyClearingHouse.getKeyMap().size() < config.getWriteThreshold()) {
Entry<String, String> entry =
amzaKeyClearingHouse.genRandomEntry(keySeed, config.getValueSizeThreshold());
if (entry == null) {
LOG.error("No random entry was generated for {}", keySeed);
} else {
service.setWithInfiniteRetry(entry.getKey(), entry.getValue(), config.getRetryWaitMs());
Integer oldValue = amzaKeyClearingHouse.set(entry.getKey(), entry.getValue());
if (oldValue != null) {
LOG.error("Found existing kv pair: {}:{}", entry.getKey(), oldValue);
amzaKeyClearingHouse.quarantineEntry(
new AbstractMap.SimpleEntry<>(entry.getKey(), entry.getValue().hashCode()), oldValue);
service.deleteWithInfiniteRetry(entry.getKey(), config.getRetryWaitMs());
}
LOG.debug("Wrote {}:{}", entry.getKey(), AmzaBotUtil.truncVal(entry.getValue()));
}
} else {
LOG.debug("Above write threshold of {} for {}.", config.getWriteThreshold(), keySeed);
}
}
return op;
}
public void start() {
LOG.info("Hesitation factor: {}", config.getHesitationFactorMs());
LOG.info("Write threshold: {}", config.getWriteThreshold());
LOG.info("Value size threshold: {}", config.getValueSizeThreshold());
LOG.info("Durability: {}", config.getDurability());
LOG.info("Consistency: {}", config.getConsistency());
LOG.info("Ring size: {}", config.getRingSize());
LOG.info("Retry wait: {}ms", config.getRetryWaitMs());
LOG.info("Snapshot frequency: {}", config.getSnapshotFrequency());
LOG.info("Client ordering: {}", config.getClientOrdering());
LOG.info("Batch factor: {}", config.getBatchFactor());
LOG.info("Tombstone timestamp age: {}ms", config.getTombstoneTimestampAgeInMillis());
LOG.info("Tombstone timestamp interval: {}ms", config.getTombstoneTimestampIntervalMillis());
if (!config.getEnabled()) {
LOG.warn("Not starting random operations; not enabled.");
return;
}
running.set(true);
processor.submit(() -> {
AtomicCounter seq = new AtomicCounter(ValueType.COUNT);
ConcurrentMap<Integer, AtomicCounter> opsCounter = Maps.newConcurrentMap();
for (int i = 0; i < 7; i++) {
opsCounter.put(i, new AtomicCounter());
}
LOG.info("Executing random read/write/delete operations");
while (running.get()) {
try {
int op = randomOp(String.valueOf(seq.getValue()));
opsCounter.get(op).inc();
seq.inc();
if (config.getHesitationFactorMs() > 0) {
Thread.sleep(RANDOM.nextInt(config.getHesitationFactorMs()));
}
if (seq.getValue() % config.getSnapshotFrequency() == 0) {
LOG.info("Executed {} random operations. {} reads, {} deletes, {} batch writes, {} batch deletes, {} cas, {} writes",
seq.getCount(),
opsCounter.get(0).getValue(),
opsCounter.get(1).getValue(),
opsCounter.get(2).getValue(),
opsCounter.get(3).getValue(),
opsCounter.get(4).getValue(),
opsCounter.get(5).getValue() + opsCounter.get(6).getValue());
amzaKeyClearingHouse.verifyKeyMap(service.getAllWithInfiniteRetry(config.getRetryWaitMs()));
}
} catch (Exception e) {
LOG.error("Error occurred running random operation. {}", e.getLocalizedMessage());
running.set(false);
}
}
return null;
});
}
public void stop() throws InterruptedException {
running.set(false);
Thread.sleep(config.getHesitationFactorMs());
processor.shutdownNow();
}
}