package org.corfudb.infrastructure;
import com.github.benmanes.caffeine.cache.*;
import lombok.Getter;
import org.corfudb.util.JSONUtils;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Stores data as JSON.
*
* Handle in-memory and persistent case differently:
*
* In in-memory mode, the "cache" is actually the store, so we never evict anything from it.
*
* In persistent mode, we use a {@link LoadingCache}, where an in-memory map is backed by disk.
* In this scheme, the key for each value is also the name of the file where the value is stored.
* The key is determined as (prefix + "_" + key).
* The cache here serves mostly for easily managed synchronization of in-memory/file.
* <p>
* If 'opts' either has '--memory=true' or a log-path for storing files is not provided,
* the store is just an in memory cache.
* <p>
* Created by mdhawan on 7/27/16.
*/
public class DataStore implements IDataStore {
private final Map<String, Object> opts;
private final boolean isPersistent;
@Getter
private final LoadingCache<String, String> cache;
private final String logDir;
@Getter
private final long DS_CACHE_SZ = 1_000; // size bound for in-memory cache for dataStore
public DataStore(Map<String, Object> opts) {
this.opts = opts;
if ((opts.get("--memory") != null && (Boolean) opts.get("--memory"))
|| opts.get("--log-path") == null) {
// in-memory dataSture case
isPersistent = false;
this.logDir = null;
cache = buildMemoryDS();
} else {
// persistent dataSture case
isPersistent = true;
this.logDir = (String) opts.get("--log-path");
cache = buildPersistentDS();
}
}
/**
* obtain an in-memory cache, no content loader, no writer, no size limit.
* @return
*/
private LoadingCache<String, String> buildMemoryDS() {
LoadingCache<String, String> cache = Caffeine
.newBuilder()
.recordStats()
.build(k -> null);
return cache;
}
/**
* obtain a {@link LoadingCache}.
* The cache is backed up by file-per-key uner {@link DataStore::logDir}.
* The cache size is bounded by {@link DataStore::DS_CACHE_SZ}.
*
* @return the cache object
*/
private LoadingCache<String, String> buildPersistentDS() {
LoadingCache<String, String> cache = Caffeine.newBuilder()
.recordStats()
.writer(new CacheWriter<String, String>() {
@Override
public synchronized void write(@Nonnull String key, @Nonnull String value) {
try {
Path path = Paths.get(logDir + File.separator + key);
Files.write(path, value.getBytes());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public synchronized void delete(@Nonnull String key, @Nullable String value, @Nonnull RemovalCause cause) {
try {
Path path = Paths.get(logDir + File.separator + key);
Files.deleteIfExists(path);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
})
.maximumSize(DS_CACHE_SZ)
.build(key -> {
try {
Path path = Paths.get(logDir + File.separator + key);
if (Files.notExists(path)) {
return null;
}
return new String(Files.readAllBytes(path));
} catch (IOException e) {
throw new RuntimeException(e);
}
});
return cache;
}
@Override
public synchronized <T> void put(Class<T> tClass, String prefix, String key, T value) {
cache.put(getKey(prefix, key), JSONUtils.parser.toJson(value, tClass));
}
@Override
public synchronized <T> T get(Class<T> tClass, String prefix, String key) {
String json = cache.get(getKey(prefix, key));
return getObject(json, tClass);
}
/**
* This is an atomic conditional get/put: If the key is not found, then the specified value is inserted.
* It returns the latest value, either the original one found, or the newly inserted value
* @param tClass type of value
* @param prefix prefix part of key
* @param key suffice part of key
* @param value value to be conditionally accepted
* @param <T> value class
* @return the latest value in the cache
*/
public <T> T get(Class<T> tClass, String prefix, String key, T value) {
String keyString = getKey(prefix, key);
String json = cache.get(keyString, k -> JSONUtils.parser.toJson(value, tClass));
return getObject(json, tClass);
}
@Override
public synchronized <T> List<T> getAll(Class<T> tClass, String prefix) {
List<T> list = new ArrayList<T>();
for (Map.Entry<String, String> entry : cache.asMap().entrySet()) {
if (entry.getKey().startsWith(prefix)) {
list.add(getObject(entry.getValue(), tClass));
}
}
return list;
}
@Override
public synchronized <T> void delete(Class<T> tClass, String prefix, String key) {
cache.invalidate(getKey(prefix, key));
}
// Helper methods
private <T> T getObject(String json, Class<T> tClass) {
return isNotNull(json) ? JSONUtils.parser.fromJson(json, tClass) : null;
}
private String getKey(String prefix, String key) {
return prefix + "_" + key;
}
private boolean isNotNull(String value) {
return value != null && !value.trim().isEmpty();
}
}