package com.aol.micro.server.couchbase.base;
import java.util.Date;
import java.util.Optional;
import com.aol.cyclops2.util.ExceptionSoftener;
import cyclops.control.Xor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.aol.micro.server.distributed.DistributedMap;
import com.aol.micro.server.manifest.Data;
import com.aol.micro.server.manifest.ManifestComparator;
import com.aol.micro.server.manifest.ManifestComparatorKeyNotFoundException;
import com.aol.micro.server.manifest.VersionedKey;
import com.aol.micro.server.rest.jackson.JacksonUtil;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.SneakyThrows;
/**
* Manifest comparator for use with a distributed map -assumes single producer /
* multiple consumers
*
* Uses to entries in the map
*
* key : versioned key versioned key : actual data
*
* ManifestComparator stores the current version number, only when the version
* changes is the full data set loaded from the remote store.
*
* Usage as a Spring Bean - inject into the host class, and use withKey to
* customise for the targeted Key.
*
*
* <pre>
* {@code
* @Rest
public class MyDataService {
private final ManifestComparator<DataType> comparator;
@Autowired
public MyDataService(ManifestComparator comparator) {
this.comparator = comparator.withKey("test-key");
}
*
* }
* </pre>
*
* micro-couchbase configures a single ManifestComparator bean that can be
* customized for multiple different keys via withKey
*
* When your bean is injected save via saveAndIncrement, and periodically call
* load() to refresh data if (and only if) it has changed.
*
* ManifestComparator will automatically remove old versions on
* saveAndIncrement, but system outages may occasionally cause old keys to
* linger, you can also use clean & cleanAll to periodically to remove old key
* versions.
*
*
* @author johnmcclean
*
* @param <T>
*/
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class CouchbaseManifestComparator<T> implements ManifestComparator<T> {
private final Logger logger = LoggerFactory.getLogger(getClass());
private final String key;
private volatile Xor<Void, T> data = Xor.secondary(null); // Void represents
// an unitialized
// state
@Getter
private volatile String versionedKey;
private final DistributedMap connection;
/**
* Create a ManifestComparator with the supplied distributed map client Data
* stored by ManifestComparator will be
*
* key : versioned key versioned key : actual data
*
* @param connection
* DistributedMapClient to store comparison data
*/
public CouchbaseManifestComparator(DistributedMap connection) {
this.key = "default";
this.versionedKey = newKey(1L).toJson();
this.connection = connection;
}
/**
* Create a ManifestComparator with the supplied distributed map client
*
* Data stored by ManifestComparator will be
*
* key : versioned key versioned key : actual data
*
* @param key
* To store actual data with
* @param connection
* DistributeMapClient connection
*/
public CouchbaseManifestComparator(String key, DistributedMap connection) {
this.key = key;
this.versionedKey = newKey(1L).toJson();
this.connection = connection;
}
/**
* Create a new ManifestComparator with the same distributed map connection
* that targets a different key
*
* @param key
* Key to store data with
* @return new ManifestComparator that targets specified key
*/
@Override
public <R> CouchbaseManifestComparator<R> withKey(String key) {
return new CouchbaseManifestComparator<>(
key, connection);
}
private VersionedKey newKey(Long version) {
return new VersionedKey(
key, version);
}
private VersionedKey increment() {
VersionedKey currentVersionedKey = loadKeyFromCouchbase();
return currentVersionedKey.withVersion(currentVersionedKey.getVersion() + 1);
}
private VersionedKey loadKeyFromCouchbase() {
Optional<String> optionalKey = connection.get(key);
return optionalKey.flatMap(val -> Optional.of(JacksonUtil.convertFromJson(val, VersionedKey.class)))
.orElse(newKey(0L));
}
@Override
@SneakyThrows
public T getData() {
while (data.isSecondary()) {
Thread.sleep(500);
}
return data.get();
}
@Override
public T getCurrentData() {
return data.visit(present -> present, () -> null);
}
/**
* @return true - if current data is stale and needs refreshed
*/
@Override
public boolean isOutOfDate() {
return !versionedKey.equals(loadKeyFromCouchbase().toJson());
}
/**
* Load data from remote store if stale
*/
@Override
public synchronized boolean load() {
Xor<Void, T> oldData = data;
String oldKey = versionedKey;
try {
if (isOutOfDate()) {
String newVersionedKey = (String) connection.get(key)
.get();
data = Xor.primary((T) nonAtomicload(newVersionedKey));
versionedKey = newVersionedKey;
} else {
return false;
}
} catch (Throwable e) {
data = oldData;
versionedKey = oldKey;
logger.debug(e.getMessage(), e);
throw ExceptionSoftener.throwSoftenedException(e);
}
return true;
}
@SuppressWarnings("unchecked")
private Object nonAtomicload(String newVersionedKey) throws Throwable {
Data data = (Data) connection.get(newVersionedKey)
.orElseThrow(() -> {
return new ManifestComparatorKeyNotFoundException(
"Missing versioned key "
+ newVersionedKey
+ " - likely data changed during read");
});
logger.info("Loaded new data with date {} for key {}, versionedKey {}, versionedKey from data ",
new Object[] { data.getDate(), key, newVersionedKey, data.getVersionedKey() });
return data.getData();
}
/**
* Clean all old (not current) versioned keys
*/
@Override
public void cleanAll() {
clean(-1);
}
/**
* Clean specified number of old (not current) versioned keys)
*
* @param numberToClean
*/
@Override
public void clean(int numberToClean) {
logger.info("Attempting to delete the last {} records for key {}", numberToClean, key);
VersionedKey currentVersionedKey = loadKeyFromCouchbase();
long start = 0;
if (numberToClean != -1)
start = currentVersionedKey.getVersion() - numberToClean;
for (long i = start; i < currentVersionedKey.getVersion(); i++) {
delete(currentVersionedKey.withVersion(i)
.toJson());
}
logger.info("Finished deleting the last {} records for key {}", numberToClean, key);
}
private void delete(String withVersion) {
connection.delete(withVersion);
}
/**
* Save provided data with the key this ManifestComparator manages bump the
* versioned key version.
*
* NB : To avoid race conditions - make sure only one service (an elected
* leader) can write at a time (see micro-mysql for a mysql distributed
* lock, or micro-curator for a curator / zookeeper distributed lock
* implementation).
*
* @param data
* to save
*/
@Override
public void saveAndIncrement(T data) {
Xor<Void, T> oldData = this.data;
VersionedKey newVersionedKey = increment();
logger.info("Saving data with key {}, new version is {}", key, newVersionedKey.toJson());
connection.put(newVersionedKey.toJson(), new Data(
data, new Date(), newVersionedKey.toJson()));
connection.put(key, newVersionedKey.toJson());
try {
this.data = Xor.primary(data);
delete(versionedKey);
} catch (Throwable t) {
this.data = oldData;
} finally {
versionedKey = newVersionedKey.toJson();
}
}
@Override
public String toString() {
return "[CouchbaseManifestComparator:key:" + key + ",versionedKey:" + JacksonUtil.serializeToJson(versionedKey)
+ "]";
}
}