blob: 2e88bb1da609527f569fb9a93030f715af200b17 [file] [log] [blame]
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you under the Apache License, Version 2.0 (the
~ "License"); you may not use this file except in compliance
~ with the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing,
~ software distributed under the License is distributed on an
~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
~ KIND, either express or implied. See the License for the
~ specific language governing permissions and limitations
~ under the License.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
package org.apache.sling.graphql.core.cache;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.commons.metrics.Counter;
import org.apache.sling.commons.metrics.MetricsService;
import org.apache.sling.graphql.api.cache.GraphQLCacheProvider;
import org.apache.sling.graphql.core.hash.SHA256Hasher;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.MetricRegistry;
@Component()
@Designate(ocd = SimpleGraphQLCacheProvider.Config.class)
public class SimpleGraphQLCacheProvider implements GraphQLCacheProvider {
@ObjectClassDefinition(
name = "Apache Sling GraphQL Simple Cache Provider",
description = "The Apache Sling GraphQL Simple Cache Provider provides an in-memory size bound cache for persisted GraphQL " +
"queries."
)
public @interface Config {
@AttributeDefinition(
name = "Capacity",
description = "The number of persisted queries to cache. If the cache size is set to a number greater than 0, then this " +
"parameter will have priority over maxMemory.",
type = AttributeType.INTEGER,
min = "0"
)
int cacheSize() default 0;
@AttributeDefinition(
name = "Max Values in Bytes",
description = "The maximum amount of memory the values stored in the cache can use."
)
long maxMemory() default 10 * FileUtils.ONE_MB;
}
private static final Logger LOGGER = LoggerFactory.getLogger(SimpleGraphQLCacheProvider.class);
@Reference
private MetricsService metricsService;
@Reference(target = "(name=sling)")
private MetricRegistry metricRegistry;
private InMemoryLRUCache persistedQueriesCache;
private Lock readLock;
private Lock writeLock;
private Counter evictions;
private static final String METRIC_NS = SimpleGraphQLCacheProvider.class.getName();
private static final String GAUGE_CACHE_SIZE = METRIC_NS + ".cacheSize";
private static final String GAUGE_ELEMENTS = METRIC_NS + ".elements";
private static final String GAUGE_MAX_MEMORY = METRIC_NS + ".maxMemory";
private static final String GAUGE_CURRENT_MEMORY = METRIC_NS + ".currentMemory";
private static final String COUNTER_EVICTIONS = METRIC_NS + ".evictions";
private static final Set<String> MANUALLY_REGISTERED_METRICS = new HashSet<>(Arrays.asList(GAUGE_CACHE_SIZE, GAUGE_ELEMENTS,
GAUGE_MAX_MEMORY, GAUGE_CURRENT_MEMORY));
@Activate
private void activate(Config config, BundleContext bundleContext) {
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
readLock = readWriteLock.readLock();
writeLock = readWriteLock.writeLock();
int capacity;
if (config.cacheSize() < 0) {
capacity = 0;
LOGGER.debug("Cache capacity set to {}. Defaulting to 0.", config.cacheSize());
} else {
capacity = config.cacheSize();
}
long maxMemory;
if (config.maxMemory() < 0) {
maxMemory = 0;
LOGGER.debug("Cache max memory set to {}. Defaulting to 0.", config.maxMemory());
} else {
maxMemory = config.maxMemory();
}
persistedQueriesCache = new InMemoryLRUCache(capacity, maxMemory);
LOGGER.debug("In-memory cache initialized: capacity={}, maxMemory={}.", capacity, maxMemory);
metricRegistry.register(GAUGE_CACHE_SIZE, (Gauge<Integer>) () -> capacity);
metricRegistry.register(GAUGE_MAX_MEMORY, (Gauge<Long>) () -> maxMemory);
metricRegistry.register(GAUGE_CURRENT_MEMORY, (Gauge<Long>) () -> persistedQueriesCache.currentSizeInBytes);
metricRegistry.register(GAUGE_ELEMENTS, (Gauge<Integer>) () -> persistedQueriesCache.size());
evictions = metricsService.counter(COUNTER_EVICTIONS);
}
@Deactivate
private void deactivate() {
for (String manuallyRegisteredMetric : MANUALLY_REGISTERED_METRICS) {
metricRegistry.remove(manuallyRegisteredMetric);
}
}
@Override
@Nullable
public String getQuery(@NotNull String hash, @NotNull String resourceType, @Nullable String selectorString) {
readLock.lock();
try {
return persistedQueriesCache.get(getCacheKey(hash, resourceType, selectorString));
} finally {
readLock.unlock();
}
}
@Override
@Nullable
public String cacheQuery(@NotNull String query, @NotNull String resourceType, @Nullable String selectorString) {
writeLock.lock();
try {
String hash = SHA256Hasher.getHash(query);
String key = getCacheKey(hash, resourceType, selectorString);
persistedQueriesCache.put(key, query);
if (persistedQueriesCache.containsKey(key)) {
return hash;
}
return null;
} finally {
writeLock.unlock();
}
}
@NotNull
private String getCacheKey(@NotNull String hash, @NotNull String resourceType, @Nullable String selectorString) {
StringBuilder key = new StringBuilder(resourceType);
if (StringUtils.isNotEmpty(selectorString)) {
key.append("_").append(selectorString);
}
key.append("_").append(hash);
return key.toString();
}
/**
* This implementation provides a simple LRU eviction based on either the number of entries or the memory used by the stored values.
* Synchronization has to happen externally.
*/
private class InMemoryLRUCache extends LinkedHashMap<String, String> {
private final int capacity;
private final long maxSizeInBytes;
private long currentSizeInBytes;
public InMemoryLRUCache(int capacity, long maxSizeInBytes) {
this.capacity = Math.max(capacity, 0);
this.maxSizeInBytes = Math.max(maxSizeInBytes, 0);
this.currentSizeInBytes = 0;
}
@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
boolean willRemove = false;
if (capacity > 0) {
willRemove = size() > capacity;
} else if (maxSizeInBytes > 0) {
willRemove = currentSizeInBytes > maxSizeInBytes;
}
if (willRemove) {
evictions.increment();
currentSizeInBytes -= getApproximateStringSizeInBytes(eldest.getValue());
}
return willRemove;
}
@Override
public String put(String key, String value) {
long valueSize = getApproximateStringSizeInBytes(value);
if (capacity <= 0 && maxSizeInBytes > 0) {
long newSizeInBytes;
boolean isReplacement = containsKey(key);
if (isReplacement) {
long oldValueSize = getApproximateStringSizeInBytes(get(key));
newSizeInBytes = currentSizeInBytes - oldValueSize + valueSize;
} else {
// calculate what happens after removing LRU
newSizeInBytes = currentSizeInBytes + valueSize;
Optional<String> head = this.values().stream().findFirst();
if (head.isPresent()) {
newSizeInBytes -= getApproximateStringSizeInBytes(head.get());
}
}
if (newSizeInBytes <= maxSizeInBytes) {
if (isReplacement) {
currentSizeInBytes = newSizeInBytes;
} else {
currentSizeInBytes += valueSize;
}
return super.put(key, value);
}
} else {
currentSizeInBytes += valueSize;
return super.put(key, value);
}
return null;
}
int getApproximateStringSizeInBytes(@NotNull String string) {
return 8 * (((string.length() * 2) + 45) / 8);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof InMemoryLRUCache) {
InMemoryLRUCache other = (InMemoryLRUCache) obj;
return Objects.equals(capacity, other.capacity) && Objects.equals(maxSizeInBytes, other.maxSizeInBytes) &&
Objects.equals(currentSizeInBytes, other.currentSizeInBytes) && super.equals(obj);
}
return false;
}
@Override
public int hashCode() {
return super.hashCode() + capacity + ((int) (maxSizeInBytes + currentSizeInBytes));
}
}
}