dev.galasa.cps.etcd.internal.Etcd3DynamicStatusStore Maven / Gradle / Ivy
The newest version!
/*
* Copyright contributors to the Galasa project
*
* SPDX-License-Identifier: EPL-2.0
*/
package dev.galasa.cps.etcd.internal;
import static com.google.common.base.Charsets.UTF_8;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import javax.validation.constraints.NotNull;
import dev.galasa.framework.spi.DssAdd;
import dev.galasa.framework.spi.DssDelete;
import dev.galasa.framework.spi.DssDeletePrefix;
import dev.galasa.framework.spi.DssSwap;
import dev.galasa.framework.spi.DssUpdate;
import dev.galasa.framework.spi.DynamicStatusStoreException;
import dev.galasa.framework.spi.DynamicStatusStoreMatchException;
import dev.galasa.framework.spi.IDssAction;
import dev.galasa.framework.spi.IDynamicStatusStore;
import dev.galasa.framework.spi.IDynamicStatusStoreWatcher;
import dev.galasa.framework.spi.IDynamicStatusStoreWatcher.Event;
import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.Client;
import io.etcd.jetcd.KV;
import io.etcd.jetcd.KeyValue;
import io.etcd.jetcd.Txn;
import io.etcd.jetcd.Watch;
import io.etcd.jetcd.Watch.Listener;
import io.etcd.jetcd.Watch.Watcher;
import io.etcd.jetcd.kv.DeleteResponse;
import io.etcd.jetcd.kv.GetResponse;
import io.etcd.jetcd.kv.PutResponse;
import io.etcd.jetcd.kv.TxnResponse;
import io.etcd.jetcd.op.Cmp;
import io.etcd.jetcd.op.CmpTarget;
import io.etcd.jetcd.op.Op;
import io.etcd.jetcd.options.DeleteOption;
import io.etcd.jetcd.options.GetOption;
import io.etcd.jetcd.options.OptionsUtil;
import io.etcd.jetcd.options.PutOption;
import io.etcd.jetcd.options.WatchOption;
import io.etcd.jetcd.watch.WatchEvent;
import io.etcd.jetcd.watch.WatchEvent.EventType;
import io.etcd.jetcd.watch.WatchResponse;
/**
* This class implements the DSS store for the use of etcd3 as the k-v store. It
* is interacting with the Jetcd Client offered from coreOs.
*
* @author James Davies
*/
public class Etcd3DynamicStatusStore implements IDynamicStatusStore {
private final Client client;
private final KV kvClient;
private final Watch watchClient;
private final HashMap watchers = new HashMap<>();
/**
* The constructure sets up a private KVClient that can be used by this class to
* interact with the etcd3 cluster.
*
* The URI passed from the registration of the EtcdDSS should check that it is a
* valid URI.
*
* @param dssUri - http:// uri for th etcd cluster.
*/
public Etcd3DynamicStatusStore(URI dssUri) {
client = Client.builder().endpoints(dssUri).build();
kvClient = client.getKVClient();
this.watchClient = client.getWatchClient();
}
/**
* A simple put class that adds a single key value in etcd key value store.
*
* @param key The key to be stored.
* @param value The value to be associated with the specified key.
* @throws DynamicStatusStoreException A failure occurred.
*/
@Override
public void put(@NotNull String key, @NotNull String value) throws DynamicStatusStoreException {
ByteSequence bsKey = ByteSequence.from(key, UTF_8);
ByteSequence bsValue = ByteSequence.from(value, UTF_8);
CompletableFuture response = kvClient.put(bsKey, bsValue);
try {
response.get();
} catch (InterruptedException | ExecutionException e) {
Thread.currentThread().interrupt();
throw new DynamicStatusStoreException("Could not put key-value", e);
}
}
/**
* A map put which allows a map of k-v pairs to the etcd store.
*
* @param keyValues - a map of key value pairs.
* @throws DynamicStatusStoreException A failure occurred.
*/
@Override
public void put(@NotNull Map keyValues) throws DynamicStatusStoreException {
Txn txn = kvClient.txn();
PutOption options = PutOption.DEFAULT;
ArrayList ops = new ArrayList<>();
for (String key : keyValues.keySet()) {
ByteSequence obsKey = ByteSequence.from(key, UTF_8);
ByteSequence obsValue = ByteSequence.from(keyValues.get(key), UTF_8);
ops.add(Op.put(obsKey, obsValue, options));
}
Txn request = txn.Then(ops.toArray(new Op[ops.size()]));
CompletableFuture response = request.commit();
try {
response.get();
} catch (InterruptedException | ExecutionException e) {
Thread.currentThread().interrupt();
throw new DynamicStatusStoreException("", e);
}
}
/**
* A put swap method does a check before setting the key-value.
*
* If the oldValue argument is not the current value of the key the newValue is
* NOT set into the store.
*
* @param key The key whose value will be swapped.
* @param oldValue - the value the key should have for the change to succeeed
* @param newValue - the new value to set too if the old value was correct
* @return boolean - if the swap was successful
* @throws DynamicStatusStoreException A failure occurred.
*/
@Override
public boolean putSwap(@NotNull String key, String oldValue, @NotNull String newValue)
throws DynamicStatusStoreException {
ByteSequence bsKey = ByteSequence.from(key, UTF_8);
ByteSequence bsNewValue = ByteSequence.from(newValue, UTF_8);
Txn txn = kvClient.txn();
Cmp cmp = null;
if (oldValue == null) {
cmp = new Cmp(bsKey, Cmp.Op.EQUAL, CmpTarget.version(0));
} else {
ByteSequence bsOldValue = ByteSequence.from(oldValue, UTF_8);
cmp = new Cmp(bsKey, Cmp.Op.EQUAL, CmpTarget.value(bsOldValue));
}
PutOption option = PutOption.DEFAULT;
Txn check = txn.If(cmp);
Txn request = check.Then(Op.put(bsKey, bsNewValue, option));
CompletableFuture response = request.commit();
try {
return response.get().isSucceeded();
} catch (InterruptedException | ExecutionException e) {
Thread.currentThread().interrupt();
throw new DynamicStatusStoreException("Put Swap failed", e);
}
}
/**
* A put swap method does a check before setting the key-value.
*
* If the oldValue argument is not the current value of the key the newValue is
* NOT set into the store. If the old value was correct then a map of other key
* value pairs can be then set.
*
* @param key The key whose value will be swapped
* @param oldValue - the value the key should have for the change to succeeed
* @param newValue - the new value to set too if the old value was correct
* @param others - a map of all subsequent values to set if the condition was
* satisfied
* @return boolean - if the swap was successful
* @throws DynamicStatusStoreException A failure occurred.
*/
@Override
public boolean putSwap(@NotNull String key, String oldValue, @NotNull String newValue,
@NotNull Map others) throws DynamicStatusStoreException {
ByteSequence bsKey = ByteSequence.from(key, UTF_8);
ByteSequence bsNewValue = ByteSequence.from(newValue, UTF_8);
Txn txn = kvClient.txn();
Cmp cmp = null;
if (oldValue == null) {
cmp = new Cmp(bsKey, Cmp.Op.EQUAL, CmpTarget.version(0));
} else {
ByteSequence bsOldValue = ByteSequence.from(oldValue, UTF_8);
cmp = new Cmp(bsKey, Cmp.Op.EQUAL, CmpTarget.value(bsOldValue));
}
ArrayList ops = new ArrayList<>();
PutOption option = PutOption.DEFAULT;
ops.add(Op.put(bsKey, bsNewValue, option));
for (Entry entry : others.entrySet()) {
ByteSequence obsKey = ByteSequence.from(entry.getKey(), UTF_8);
ByteSequence obsValue = ByteSequence.from(entry.getValue(), UTF_8);
ops.add(Op.put(obsKey, obsValue, option));
}
CompletableFuture response = txn.If(cmp).Then(ops.toArray(new Op[ops.size()])).commit();
try {
return response.get().isSucceeded();
} catch (InterruptedException | ExecutionException e) {
Thread.currentThread().interrupt();
throw new DynamicStatusStoreException("Put Swap failed", e);
}
}
/**
* A simple get method that retrieves on value from one key
*
* @param key The key we wish to query
* @return The value of the key and null if not existing.
* @throws DynamicStatusStoreException A failure occurred.
*/
@Override
public String get(@NotNull String key) throws DynamicStatusStoreException {
ByteSequence bsKey = ByteSequence.from(key, UTF_8);
CompletableFuture getFuture = kvClient.get(bsKey);
try {
GetResponse response = getFuture.get();
List kvs = response.getKvs();
if (kvs.isEmpty()) {
return null;
}
return kvs.get(0).getValue().toString(UTF_8);
} catch (InterruptedException | ExecutionException e) {
Thread.currentThread().interrupt();
throw new DynamicStatusStoreException("Could not retrieve key.", e);
}
}
/**
* A get of all keys and value that start with a specified prefix. They are
* returned in a map of key-value pairs
*
* @param keyPrefix - the prefix for any key(s)
* @return A map of name-value pairs
* @throws DynamicStatusStoreException A failure occurred.
*/
@Override
public @NotNull Map getPrefix(@NotNull String keyPrefix) throws DynamicStatusStoreException {
ByteSequence bsPrefix = ByteSequence.from(keyPrefix, UTF_8);
ByteSequence prefixEnd = OptionsUtil.prefixEndOf(bsPrefix);
GetOption options = GetOption.builder().withRange(prefixEnd).build();
CompletableFuture getFuture = kvClient.get(bsPrefix, options);
Map keyValues = new HashMap<>();
try {
GetResponse response = getFuture.get();
List kvs = response.getKvs();
if (kvs.isEmpty()) {
return new HashMap<>();
}
for (KeyValue kv : kvs) {
keyValues.put(kv.getKey().toString(UTF_8), kv.getValue().toString(UTF_8));
}
return keyValues;
} catch (InterruptedException | ExecutionException e) {
Thread.currentThread().interrupt();
throw new DynamicStatusStoreException("Could not retrieve key.", e);
}
}
/**
* A Simple delete of a singe Key value pair.
*
* @param key - the key to delete
* @throws DynamicStatusStoreException A failure occurred.
*/
@Override
public void delete(@NotNull String key) throws DynamicStatusStoreException {
ByteSequence bsKey = ByteSequence.from(key, UTF_8);
CompletableFuture deleteFuture = kvClient.delete(bsKey);
try {
deleteFuture.get();
} catch (InterruptedException | ExecutionException e) {
Thread.currentThread().interrupt();
throw new DynamicStatusStoreException("Could not delete key.", e);
}
}
/**
* A delete of a set of provided keys and there corresponding values.
*
* @param keys a set of keys to delete
* @throws DynamicStatusStoreException A failure occurred.
*
*/
@Override
public void delete(@NotNull Set keys) throws DynamicStatusStoreException {
Txn txn = kvClient.txn();
DeleteOption options = DeleteOption.DEFAULT;
ArrayList ops = new ArrayList<>();
for (String key : keys) {
ByteSequence obsKey = ByteSequence.from(key, UTF_8);
ops.add(Op.delete(obsKey, options));
}
CompletableFuture response = txn.Then(ops.toArray(new Op[ops.size()])).commit();
try {
response.get();
} catch (InterruptedException | ExecutionException e) {
Thread.currentThread().interrupt();
throw new DynamicStatusStoreException("Could not delete key(s).", e);
}
}
/**
* A delete which removes all keys with a specified prefix and there
* corresponding values from the store.
*
* @param keyPrefix - a string prefix that all the key(s) have in common
* @throws DynamicStatusStoreException A failure occurred.
*/
@Override
public void deletePrefix(@NotNull String keyPrefix) throws DynamicStatusStoreException {
ByteSequence bsKey = ByteSequence.from(keyPrefix, UTF_8);
ByteSequence prefixEnd = OptionsUtil.prefixEndOf(bsKey);
DeleteOption options = DeleteOption.builder().withRange(prefixEnd).build();
CompletableFuture deleteFuture = kvClient.delete(bsKey, options);
try {
deleteFuture.get();
} catch (InterruptedException | ExecutionException e) {
Thread.currentThread().interrupt();
throw new DynamicStatusStoreException("Could not delete key(s).", e);
}
}
// TODO Test and document
@Override
public UUID watch(IDynamicStatusStoreWatcher watcher, String key) throws DynamicStatusStoreException {
ByteSequence bsKey = ByteSequence.from(key, UTF_8);
PassthroughWatcher passWatcher = new PassthroughWatcher(watcher);
passWatcher.setEtcdWatcher(watchClient.watch(bsKey, passWatcher));
watchers.put(passWatcher.getID(), passWatcher);
return passWatcher.getID();
}
// TODO Test and document
@Override
public UUID watchPrefix(IDynamicStatusStoreWatcher watcher, String keyPrefix) throws DynamicStatusStoreException {
ByteSequence bsKey = ByteSequence.from(keyPrefix, UTF_8);
PassthroughWatcher passWatcher = new PassthroughWatcher(watcher);
ByteSequence prefixEnd = OptionsUtil.prefixEndOf(bsKey);
WatchOption watchOption = WatchOption.builder().withRange(prefixEnd).build();
Watcher etcdWatcher = watchClient.watch(bsKey, watchOption, passWatcher);
passWatcher.setEtcdWatcher(etcdWatcher);
watchers.put(passWatcher.getID(), passWatcher);
return passWatcher.getID();
}
// TODO Test and document
@Override
public void unwatch(UUID watchId) throws DynamicStatusStoreException {
PassthroughWatcher passWatcher = watchers.remove(watchId);
if (passWatcher == null) {
return;
}
passWatcher.getEtcdWatcher().close();
}
private class PassthroughWatcher implements Listener {
private final UUID id = UUID.randomUUID();
private final IDynamicStatusStoreWatcher watcher;
private Watcher etcdWatcher;
public PassthroughWatcher(IDynamicStatusStoreWatcher watcher) {
this.watcher = watcher;
}
@Override
public void onNext(WatchResponse response) {
if (response == null) {
return;
}
List events = response.getEvents();
if (events == null) {
return;
}
for (WatchEvent event : events) {
EventType eventType = event.getEventType();
KeyValue eventKey = event.getKeyValue();
KeyValue eventPrev = event.getPrevKV();
if (eventType == null || eventKey == null) {
continue;
}
switch (eventType) {
case DELETE:
watcher.propertyModified(eventKey.getKey().toString(UTF_8), Event.DELETE, null, null);
break;
case PUT:
if (eventPrev != null) {
watcher.propertyModified(eventKey.getKey().toString(UTF_8), Event.MODIFIED,
eventPrev.getValue().toString(UTF_8), eventKey.getValue().toString(UTF_8));
} else {
watcher.propertyModified(eventKey.getKey().toString(UTF_8), Event.NEW, null,
eventKey.getValue().toString(UTF_8));
}
break;
case UNRECOGNIZED:
default:
continue;
}
}
}
@Override
public void onError(Throwable throwable) {
}
@Override
public void onCompleted() {
}
public UUID getID() {
return this.id;
}
public void setEtcdWatcher(Watcher etcdWatcher) {
this.etcdWatcher = etcdWatcher;
}
public Watcher getEtcdWatcher() {
return this.etcdWatcher;
}
}
@Override
public void shutdown() throws DynamicStatusStoreException {
watchClient.close();
kvClient.close();
client.close();
}
@Override
public void performActions(IDssAction... actions) throws DynamicStatusStoreException, DynamicStatusStoreMatchException {
Txn txn = kvClient.txn();
// Go through the actions and collect all the IFs
for(IDssAction action : actions) {
if (action instanceof DssAdd) {
txn = performActionsAddIf(txn, (DssAdd) action);
} else if (action instanceof DssDelete) {
txn = performActionsDeleteIf(txn, (DssDelete) action);
} else if (action instanceof DssDeletePrefix) {
txn = performActionsDeletePrefixIf(txn, (DssDeletePrefix) action);
} else if (action instanceof DssUpdate) {
txn = performActionsUpdateIf(txn, (DssUpdate) action);
} else if (action instanceof DssSwap) {
txn = performActionsSwapIf(txn, (DssSwap) action);
} else {
throw new DynamicStatusStoreException("Unrecognised DSS Action - " + action.getClass().getName());
}
}
// Now get the Thens
for(IDssAction action : actions) {
if (action instanceof DssAdd) {
txn = performActionsAddThen(txn, (DssAdd) action);
} else if (action instanceof DssDelete) {
txn = performActionsDeleteThen(txn, (DssDelete) action);
} else if (action instanceof DssDeletePrefix) {
txn = performActionsDeletePrefixThen(txn, (DssDeletePrefix) action);
} else if (action instanceof DssUpdate) {
txn = performActionsUpdateThen(txn, (DssUpdate) action);
} else if (action instanceof DssSwap) {
txn = performActionsSwapThen(txn, (DssSwap) action);
} else {
throw new DynamicStatusStoreException("Unrecognised DSS Action - " + action.getClass().getName());
}
}
CompletableFuture response = txn.commit();
try {
if (!response.get().isSucceeded()) {
throw new DynamicStatusStoreMatchException("DSS transaction failed - matches failed");
}
} catch (InterruptedException | ExecutionException e) {
Thread.currentThread().interrupt();
throw new DynamicStatusStoreException("DSS transaction failed", e);
}
}
private Txn performActionsAddIf(Txn txn, DssAdd action) {
ByteSequence bsKey = ByteSequence.from(action.getKey(), UTF_8);
txn = txn.If(new Cmp(bsKey, Cmp.Op.EQUAL, CmpTarget.version(0)));
return txn;
}
private Txn performActionsAddThen(Txn txn, DssAdd action) {
ByteSequence bsKey = ByteSequence.from(action.getKey(), UTF_8);
ByteSequence bsNewValue = ByteSequence.from(action.getValue(), UTF_8);
PutOption option = PutOption.DEFAULT;
txn = txn.Then(Op.put(bsKey, bsNewValue, option));
return txn;
}
private Txn performActionsUpdateIf(Txn txn, DssUpdate action) {
return txn;
}
private Txn performActionsUpdateThen(Txn txn, DssUpdate action) {
ByteSequence bsKey = ByteSequence.from(action.getKey(), UTF_8);
ByteSequence bsNewValue = ByteSequence.from(action.getValue(), UTF_8);
PutOption option = PutOption.DEFAULT;
txn = txn.Then(Op.put(bsKey, bsNewValue, option));
return txn;
}
private Txn performActionsSwapIf(Txn txn, DssSwap action) {
ByteSequence bsKey = ByteSequence.from(action.getKey(), UTF_8);
ByteSequence bsOldValue = ByteSequence.from(action.getOldValue(), UTF_8);
txn = txn.If(new Cmp(bsKey, Cmp.Op.EQUAL, CmpTarget.value(bsOldValue)));
return txn;
}
private Txn performActionsSwapThen(Txn txn, DssSwap action) {
ByteSequence bsKey = ByteSequence.from(action.getKey(), UTF_8);
ByteSequence bsNewValue = ByteSequence.from(action.getNewValue(), UTF_8);
PutOption option = PutOption.DEFAULT;
txn = txn.Then(Op.put(bsKey, bsNewValue, option));
return txn;
}
private Txn performActionsDeleteIf(Txn txn, DssDelete action) {
ByteSequence bsKey = ByteSequence.from(action.getKey(), UTF_8);
if (action.getOldValue() != null) {
ByteSequence bsOldValue = ByteSequence.from(action.getOldValue(), UTF_8);
txn = txn.If(new Cmp(bsKey, Cmp.Op.EQUAL, CmpTarget.value(bsOldValue)));
}
return txn;
}
private Txn performActionsDeleteThen(Txn txn, DssDelete action) {
ByteSequence bsKey = ByteSequence.from(action.getKey(), UTF_8);
DeleteOption option = DeleteOption.DEFAULT;
txn = txn.Then(Op.delete(bsKey, option));
return txn;
}
private Txn performActionsDeletePrefixIf(Txn txn, DssDeletePrefix action) {
return txn;
}
private Txn performActionsDeletePrefixThen(Txn txn, DssDeletePrefix action) {
ByteSequence bsKey = ByteSequence.from(action.getPrefix(), UTF_8);
ByteSequence prefixEnd = OptionsUtil.prefixEndOf(bsKey);
DeleteOption option = DeleteOption.builder().withRange(prefixEnd).build();
txn = txn.Then(Op.delete(bsKey, option));
return txn;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy