package com.linkedin.thirdeye.client;
import com.google.common.cache.Weigher;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;
import com.linkedin.pinot.client.ResultSetGroup;
import com.linkedin.thirdeye.client.cache.CollectionMaxDataTimeCacheLoader;
import com.linkedin.thirdeye.client.cache.CollectionsCache;
import com.linkedin.thirdeye.client.cache.DashboardConfigCacheLoader;
import com.linkedin.thirdeye.client.cache.DashboardsCacheLoader;
import com.linkedin.thirdeye.client.cache.DatasetConfigCacheLoader;
import com.linkedin.thirdeye.client.cache.DimensionFiltersCacheLoader;
import com.linkedin.thirdeye.client.cache.MetricConfigCacheLoader;
import com.linkedin.thirdeye.client.cache.MetricDataset;
import com.linkedin.thirdeye.client.cache.QueryCache;
import com.linkedin.thirdeye.client.cache.ResultSetGroupCacheLoader;
import com.linkedin.thirdeye.client.pinot.PinotQuery;
import com.linkedin.thirdeye.client.pinot.PinotThirdEyeClient;
import com.linkedin.thirdeye.client.pinot.PinotThirdEyeClientConfig;
import com.linkedin.thirdeye.common.ThirdEyeConfiguration;
import com.linkedin.thirdeye.dashboard.resources.CacheResource;
import com.linkedin.thirdeye.datalayer.bao.DashboardConfigManager;
import com.linkedin.thirdeye.datalayer.bao.DatasetConfigManager;
import com.linkedin.thirdeye.datalayer.bao.MetricConfigManager;
import com.linkedin.thirdeye.datalayer.dto.DashboardConfigDTO;
import com.linkedin.thirdeye.datalayer.dto.DatasetConfigDTO;
import com.linkedin.thirdeye.datalayer.dto.MetricConfigDTO;
public class ThirdEyeCacheRegistry {
private LoadingCache<String, DatasetConfigDTO> datasetConfigCache;
private LoadingCache<MetricDataset, MetricConfigDTO> metricConfigCache;
private LoadingCache<String, List<DashboardConfigDTO>> dashboardConfigsCache;
private LoadingCache<PinotQuery, ResultSetGroup> resultSetGroupCache;
private LoadingCache<String, Long> collectionMaxDataTimeCache;
private LoadingCache<String, String> dashboardsCache;
private LoadingCache<String, String> dimensionFiltersCache;
private CollectionsCache collectionsCache;
private QueryCache queryCache;
private static DatasetConfigManager datasetConfigDAO;
private static MetricConfigManager metricConfigDAO;
private static DashboardConfigManager dashboardConfigDAO;
private static PinotThirdEyeClientConfig pinotThirdeyeClientConfig;
private static ThirdEyeClient thirdEyeClient;
private static final DAORegistry DAO_REGISTRY = DAORegistry.getInstance();
private static final Logger LOGGER = LoggerFactory.getLogger(ThirdEyeCacheRegistry.class);
// TODO: make default cache size configurable
private static final int DEFAULT_HEAP_PERCENTAGE_FOR_RESULTSETGROUP_CACHE = 50;
private static final int DEFAULT_LOWER_BOUND_OF_RESULTSETGROUP_CACHE_SIZE_IN_MB = 100;
private static final int DEFAULT_UPPER_BOUND_OF_RESULTSETGROUP_CACHE_SIZE_IN_MB = 8192;
private static class Holder {
static final ThirdEyeCacheRegistry INSTANCE = new ThirdEyeCacheRegistry();
}
public static ThirdEyeCacheRegistry getInstance() {
return Holder.INSTANCE;
}
private static void init(ThirdEyeConfiguration config) {
try {
// TODO: initialize all clients from clients.yml
// Create client map and register it with QueryCache
pinotThirdeyeClientConfig = PinotThirdEyeClientConfig.createThirdEyeClientConfig(config);
thirdEyeClient = PinotThirdEyeClient.fromClientConfig(pinotThirdeyeClientConfig);
datasetConfigDAO = DAO_REGISTRY.getDatasetConfigDAO();
metricConfigDAO = DAO_REGISTRY.getMetricConfigDAO();
dashboardConfigDAO = DAO_REGISTRY.getDashboardConfigDAO();
} catch (Exception e) {
LOGGER.info("Caught exception while initializing caches", e);
}
}
private static void init(ThirdEyeConfiguration config, PinotThirdEyeClientConfig pinotThirdEyeClientConfig) {
try {
pinotThirdeyeClientConfig = pinotThirdEyeClientConfig;
thirdEyeClient = PinotThirdEyeClient.fromClientConfig(pinotThirdeyeClientConfig);
datasetConfigDAO = DAO_REGISTRY.getDatasetConfigDAO();
metricConfigDAO = DAO_REGISTRY.getMetricConfigDAO();
dashboardConfigDAO = DAO_REGISTRY.getDashboardConfigDAO();
} catch (Exception e) {
LOGGER.info("Caught exception while initializing caches", e);
}
}
/**
* Initializes webapp caches
* @param config
*/
public static void initializeCaches(ThirdEyeConfiguration config) {
init(config);
initCaches(config);
initPeriodicCacheRefresh();
}
private static void initCaches(ThirdEyeConfiguration config) {
ThirdEyeCacheRegistry cacheRegistry = ThirdEyeCacheRegistry.getInstance();
RemovalListener<PinotQuery, ResultSetGroup> listener = new RemovalListener<PinotQuery, ResultSetGroup>() {
@Override
public void onRemoval(RemovalNotification<PinotQuery, ResultSetGroup> notification) {
LOGGER.info("Expired {}", notification.getKey().getPql());
}
};
// ResultSetGroup Cache. The size of this cache is limited by the total number of buckets in all ResultSetGroup.
// We estimate that 1 bucket (including overhead) consumes 1KB and this cache is allowed to use up to 50% of max
// heap space.
long maxBucketNumber = getApproximateMaxBucketNumber(DEFAULT_HEAP_PERCENTAGE_FOR_RESULTSETGROUP_CACHE);
LoadingCache<PinotQuery, ResultSetGroup> resultSetGroupCache = CacheBuilder.newBuilder()
.removalListener(listener)
.expireAfterAccess(1, TimeUnit.HOURS)
.maximumWeight(maxBucketNumber)
.weigher(new Weigher<PinotQuery, ResultSetGroup>() {
@Override public int weigh(PinotQuery pinotQuery, ResultSetGroup resultSetGroup) {
int resultSetCount = resultSetGroup.getResultSetCount();
int weight = 0;
for (int idx = 0; idx < resultSetCount; ++idx) {
com.linkedin.pinot.client.ResultSet resultSet = resultSetGroup.getResultSet(idx);
weight += (resultSet.getColumnCount() * resultSet.getRowCount());
}
return weight;
}
})
.build(new ResultSetGroupCacheLoader(pinotThirdeyeClientConfig));
cacheRegistry.registerResultSetGroupCache(resultSetGroupCache);
LOGGER.info("Max bucket number for ResultSetGroup cache is set to {}", maxBucketNumber);
// CollectionMaxDataTime Cache
LoadingCache<String, Long> collectionMaxDataTimeCache = CacheBuilder.newBuilder()
.refreshAfterWrite(5, TimeUnit.MINUTES)
.build(new CollectionMaxDataTimeCacheLoader(resultSetGroupCache, datasetConfigDAO));
cacheRegistry.registerCollectionMaxDataTimeCache(collectionMaxDataTimeCache);
// Query Cache
QueryCache queryCache = new QueryCache(thirdEyeClient, Executors.newFixedThreadPool(10));
cacheRegistry.registerQueryCache(queryCache);
// Dimension Filter cache
LoadingCache<String, String> dimensionFiltersCache = CacheBuilder.newBuilder()
.build(new DimensionFiltersCacheLoader(cacheRegistry.getQueryCache()));
cacheRegistry.registerDimensionFiltersCache(dimensionFiltersCache);
// Dashboards cache
LoadingCache<String, String> dashboardsCache = CacheBuilder.newBuilder()
.build(new DashboardsCacheLoader(dashboardConfigDAO));
cacheRegistry.registerDashboardsCache(dashboardsCache);
// Collections cache
CollectionsCache collectionsCache = new CollectionsCache(datasetConfigDAO, config);
cacheRegistry.registerCollectionsCache(collectionsCache);
// DatasetConfig cache
LoadingCache<String, DatasetConfigDTO> datasetConfigCache = CacheBuilder.newBuilder()
.build(new DatasetConfigCacheLoader(datasetConfigDAO));
cacheRegistry.registerDatasetConfigCache(datasetConfigCache);
// MetricConfig cache
LoadingCache<MetricDataset, MetricConfigDTO> metricConfigCache = CacheBuilder.newBuilder()
.build(new MetricConfigCacheLoader(metricConfigDAO));
cacheRegistry.registerMetricConfigCache(metricConfigCache);
// DashboardConfigs cache
LoadingCache<String, List<DashboardConfigDTO>> dashboardConfigsCache = CacheBuilder.newBuilder()
.build(new DashboardConfigCacheLoader(dashboardConfigDAO));
cacheRegistry.registerDashboardConfigsCache(dashboardConfigsCache);
}
private static void initPeriodicCacheRefresh() {
final CacheResource cacheResource = new CacheResource();
// manually refreshing on startup, and setting delay
// as weeklyService starts before hourlyService finishes,
// causing NPE in reading collectionsCache
// Start initial cache loading asynchronously to reduce application start time
Executors.newSingleThreadExecutor().submit(new Runnable() {
@Override public void run() {
cacheResource.refreshCollections();
cacheResource.refreshDatasetConfigCache();
cacheResource.refreshDashoardConfigsCache();
cacheResource.refreshDashboardsCache();
cacheResource.refreshMetricConfigCache();
cacheResource.refreshMaxDataTimeCache();
cacheResource.refreshDimensionFiltersCache();
}
});
ScheduledExecutorService minuteService = Executors.newSingleThreadScheduledExecutor();
minuteService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
cacheResource.refreshMaxDataTimeCache();
} catch (Exception e) {
LOGGER.error("Exception while loading collections", e);
}
}
}, 30, 30, TimeUnit.MINUTES);
ScheduledExecutorService hourlyService = Executors.newSingleThreadScheduledExecutor();
hourlyService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
cacheResource.refreshCollections();
} catch (Exception e) {
LOGGER.error("Exception while loading collections", e);
}
}
}, 1, 1, TimeUnit.HOURS);
ScheduledExecutorService weeklyService = Executors.newSingleThreadScheduledExecutor();
weeklyService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
cacheResource.refreshDimensionFiltersCache();
} catch (Exception e) {
LOGGER.error("Exception while loading filter caches", e);
}
}
}, 7, 7, TimeUnit.DAYS);
}
public LoadingCache<PinotQuery, ResultSetGroup> getResultSetGroupCache() {
return resultSetGroupCache;
}
public void registerResultSetGroupCache(LoadingCache<PinotQuery, ResultSetGroup> resultSetGroupCache) {
this.resultSetGroupCache = resultSetGroupCache;
}
public LoadingCache<String, Long> getCollectionMaxDataTimeCache() {
return collectionMaxDataTimeCache;
}
public void registerCollectionMaxDataTimeCache(LoadingCache<String, Long> collectionMaxDataTimeCache) {
this.collectionMaxDataTimeCache = collectionMaxDataTimeCache;
}
public CollectionsCache getCollectionsCache() {
return collectionsCache;
}
public void registerCollectionsCache(CollectionsCache collectionsCache) {
this.collectionsCache = collectionsCache;
}
public LoadingCache<String, String> getDimensionFiltersCache() {
return dimensionFiltersCache;
}
public void registerDimensionFiltersCache(LoadingCache<String, String> dimensionFiltersCache) {
this.dimensionFiltersCache = dimensionFiltersCache;
}
public LoadingCache<String, String> getDashboardsCache() {
return dashboardsCache;
}
public void registerDashboardsCache(LoadingCache<String, String> dashboardsCache) {
this.dashboardsCache = dashboardsCache;
}
public QueryCache getQueryCache() {
return queryCache;
}
public void registerQueryCache(QueryCache queryCache) {
this.queryCache = queryCache;
}
public LoadingCache<String, DatasetConfigDTO> getDatasetConfigCache() {
return datasetConfigCache;
}
public void registerDatasetConfigCache(LoadingCache<String, DatasetConfigDTO> datasetConfigCache) {
this.datasetConfigCache = datasetConfigCache;
}
public LoadingCache<MetricDataset, MetricConfigDTO> getMetricConfigCache() {
return metricConfigCache;
}
public void registerMetricConfigCache(LoadingCache<MetricDataset, MetricConfigDTO> metricConfigCache) {
this.metricConfigCache = metricConfigCache;
}
public LoadingCache<String, List<DashboardConfigDTO>> getDashboardConfigsCache() {
return dashboardConfigsCache;
}
public void registerDashboardConfigsCache(LoadingCache<String, List<DashboardConfigDTO>> dashboardConfigsCache) {
this.dashboardConfigsCache = dashboardConfigsCache;
}
/**
* Returns the suggested max weight for LoadingCache according to the given percentage of max heap space.
*
* The approximate weight is calculated by following rules:
* 1. We estimate that a bucket, including its overhead, occupies 1 KB.
* 2. Cache size (in bytes) = System's maxMemory * percentage
* 3. We also bound the cache size between DEFAULT_LOWER_BOUND_OF_RESULTSETGROUP_CACHE_SIZE_IN_MB and
* DEFAULT_UPPER_BOUND_OF_RESULTSETGROUP_CACHE_SIZE_IN_MB if max heap size is unavailable.
* 4. Weight (number of buckets) = cache size / 1KB.
*
* @param percentage the percentage of JVM max heap space
* @return the suggested max weight for LoadingCache
*/
private static long getApproximateMaxBucketNumber(int percentage) {
long jvmMaxMemoryInBytes = Runtime.getRuntime().maxMemory();
if (jvmMaxMemoryInBytes == Long.MAX_VALUE) { // Check upper bound
jvmMaxMemoryInBytes = DEFAULT_UPPER_BOUND_OF_RESULTSETGROUP_CACHE_SIZE_IN_MB * 1048576L; // MB to Bytes
} else { // Check lower bound
long lowerBoundInBytes = DEFAULT_LOWER_BOUND_OF_RESULTSETGROUP_CACHE_SIZE_IN_MB * 1048576L; // MB to Bytes
if (jvmMaxMemoryInBytes < lowerBoundInBytes) {
jvmMaxMemoryInBytes = lowerBoundInBytes;
}
}
return (jvmMaxMemoryInBytes / 102400) * percentage;
}
}