SLING-9655 - Caching support for the GraphQL core
* switched the default GraphQLCacheProvider implementation
to an in-memory LRU cache
diff --git a/src/main/java/org/apache/sling/graphql/core/cache/GraphQLCacheProviderImpl.java b/src/main/java/org/apache/sling/graphql/core/cache/GraphQLCacheProviderImpl.java
deleted file mode 100644
index dcfb4ae..0000000
--- a/src/main/java/org/apache/sling/graphql/core/cache/GraphQLCacheProviderImpl.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- ~ 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.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.Hashtable;
-
-import javax.cache.Cache;
-import javax.cache.CacheManager;
-import javax.cache.configuration.MutableConfiguration;
-import javax.cache.spi.CachingProvider;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.sling.graphql.api.cache.GraphQLCacheProvider;
-import org.apache.sling.graphql.core.engine.SlingGraphQLException;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.osgi.framework.BundleContext;
-import org.osgi.framework.ServiceRegistration;
-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.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-@Component(
- immediate = true
-)
-public class GraphQLCacheProviderImpl implements GraphQLCacheProvider {
-
- private static final Logger LOGGER = LoggerFactory.getLogger(GraphQLCacheProviderImpl.class);
-
- @Reference
- private CachingProvider cachingProvider;
-
- private Cache<String, String> persistedQueriesCache;
-
- private ServiceRegistration<GraphQLCacheProvider> registration;
-
- @Override
- @Nullable
- public String getQuery(@NotNull String hash, @NotNull String resourceType, @Nullable String selectorString) {
- return persistedQueriesCache.get(getCacheKey(hash, resourceType, selectorString));
- }
-
- @Override
- @NotNull
- public String cacheQuery(@NotNull String query, @NotNull String resourceType, @Nullable String selectorString) {
- String hash = getHash(query);
- String key = getCacheKey(hash, resourceType, selectorString);
- persistedQueriesCache.put(key, query);
- return hash;
- }
-
-
- @Activate
- private void activate(BundleContext bundleContext) {
- if (cachingProvider != null) {
- try {
- CacheManager cacheManager = cachingProvider.getCacheManager();
- persistedQueriesCache = cacheManager
- .createCache(bundleContext.getBundle().getSymbolicName() + "_persistedQueries", new MutableConfiguration<>());
- registration = bundleContext.registerService(GraphQLCacheProvider.class, this, new Hashtable<>());
- } catch (Exception e) {
- LOGGER.error("Unable to configure a cache for persisted queries.", e);
- }
- } else {
- LOGGER.error("Cannot activate the " + GraphQLCacheProvider.class.getName() + " without a cachingProvider.");
- }
- }
-
- @Deactivate
- private void deactivate() {
- if (registration != null) {
- registration.unregister();
- }
- cachingProvider.close();
- }
-
- @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();
- }
-
- @NotNull String getHash(@NotNull String query) {
- StringBuilder buffer = new StringBuilder();
- try {
- MessageDigest digest = MessageDigest.getInstance("SHA-256");
- byte[] hash = digest.digest(query.getBytes(StandardCharsets.UTF_8));
-
- for (byte b : hash) {
- String hex = Integer.toHexString(0xff & b);
- if (hex.length() == 1) {
- buffer.append('0');
- }
- buffer.append(hex);
- }
- } catch (NoSuchAlgorithmException e) {
- throw new SlingGraphQLException("Failed hashing query - " + e.getMessage());
- }
- return buffer.toString();
- }
-
-}
diff --git a/src/main/java/org/apache/sling/graphql/core/cache/SimpleGraphQLCacheProvider.java b/src/main/java/org/apache/sling/graphql/core/cache/SimpleGraphQLCacheProvider.java
new file mode 100644
index 0000000..e68977d
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/core/cache/SimpleGraphQLCacheProvider.java
@@ -0,0 +1,155 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.graphql.api.cache.GraphQLCacheProvider;
+import org.apache.sling.graphql.core.engine.SlingGraphQLException;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+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;
+
+@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.",
+ type = AttributeType.INTEGER,
+ min = "0"
+ )
+ int capacity() default DEFAULT_CACHE_SIZE;
+ }
+
+ private static final int DEFAULT_CACHE_SIZE = 1024;
+ private static final Logger LOGGER = LoggerFactory.getLogger(SimpleGraphQLCacheProvider.class);
+
+ private InMemoryLRUCache persistedQueriesCache;
+ private ReadWriteLock readWriteLock;
+ private Lock readLock;
+ private Lock writeLock;
+
+ @Activate
+ public SimpleGraphQLCacheProvider(Config config) {
+ readWriteLock = new ReentrantReadWriteLock();
+ readLock = readWriteLock.readLock();
+ writeLock = readWriteLock.writeLock();
+ int cacheSize = config.capacity();
+ if (cacheSize < 0) {
+ cacheSize = 0;
+ LOGGER.debug("Cache capacity set to {}. Defaulting to 0.", config.capacity());
+ }
+ persistedQueriesCache = new InMemoryLRUCache(cacheSize);
+ LOGGER.debug("Initialized the in-memory cache for a maximum of {} queries.", config.capacity());
+ }
+
+
+ @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
+ @NotNull
+ public String cacheQuery(@NotNull String query, @NotNull String resourceType, @Nullable String selectorString) {
+ writeLock.lock();
+ try {
+ String hash = getHash(query);
+ String key = getCacheKey(hash, resourceType, selectorString);
+ persistedQueriesCache.put(key, query);
+ return hash;
+ } 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();
+ }
+
+ @NotNull String getHash(@NotNull String query) {
+ StringBuilder buffer = new StringBuilder();
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hash = digest.digest(query.getBytes(StandardCharsets.UTF_8));
+
+ for (byte b : hash) {
+ String hex = Integer.toHexString(0xff & b);
+ if (hex.length() == 1) {
+ buffer.append('0');
+ }
+ buffer.append(hex);
+ }
+ } catch (NoSuchAlgorithmException e) {
+ throw new SlingGraphQLException("Failed hashing query - " + e.getMessage());
+ }
+ return buffer.toString();
+ }
+
+ private static class InMemoryLRUCache extends LinkedHashMap<String, String> {
+
+ private final int capacity;
+
+ public InMemoryLRUCache(int capacity) {
+ this.capacity = capacity;
+ }
+
+ @Override
+ protected boolean removeEldestEntry(Map.Entry eldest) {
+ return size() > capacity;
+ }
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/graphql/core/servlet/GraphQLServlet.java b/src/main/java/org/apache/sling/graphql/core/servlet/GraphQLServlet.java
index a54b0e4..537875e 100644
--- a/src/main/java/org/apache/sling/graphql/core/servlet/GraphQLServlet.java
+++ b/src/main/java/org/apache/sling/graphql/core/servlet/GraphQLServlet.java
@@ -21,7 +21,6 @@
package org.apache.sling.graphql.core.servlet;
import java.io.IOException;
-import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
diff --git a/src/test/java/org/apache/sling/graphql/core/cache/GraphQLCacheProviderImplTest.java b/src/test/java/org/apache/sling/graphql/core/cache/SimpleGraphQLCacheProviderTest.java
similarity index 71%
rename from src/test/java/org/apache/sling/graphql/core/cache/GraphQLCacheProviderImplTest.java
rename to src/test/java/org/apache/sling/graphql/core/cache/SimpleGraphQLCacheProviderTest.java
index 19c0d3a..8da286a 100644
--- a/src/test/java/org/apache/sling/graphql/core/cache/GraphQLCacheProviderImplTest.java
+++ b/src/test/java/org/apache/sling/graphql/core/cache/SimpleGraphQLCacheProviderTest.java
@@ -18,15 +18,28 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
package org.apache.sling.graphql.core.cache;
+import java.lang.annotation.Annotation;
+
import org.junit.Test;
import static org.junit.Assert.assertEquals;
-public class GraphQLCacheProviderImplTest {
+public class SimpleGraphQLCacheProviderTest {
@Test
- public void getHash() throws Exception {
- GraphQLCacheProviderImpl provider = new GraphQLCacheProviderImpl();
+ public void getHash() {
+ SimpleGraphQLCacheProvider provider = new SimpleGraphQLCacheProvider(new SimpleGraphQLCacheProvider.Config() {
+ @Override
+ public Class<? extends Annotation> annotationType() {
+ return null;
+ }
+
+ @Override
+ public int capacity() {
+ return 0;
+ }
+ });
+
assertEquals("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", provider.getHash("hello world"));
}
}
diff --git a/src/test/java/org/apache/sling/graphql/core/it/GraphQLCoreTestSupport.java b/src/test/java/org/apache/sling/graphql/core/it/GraphQLCoreTestSupport.java
index 604f218..60d10f0 100644
--- a/src/test/java/org/apache/sling/graphql/core/it/GraphQLCoreTestSupport.java
+++ b/src/test/java/org/apache/sling/graphql/core/it/GraphQLCoreTestSupport.java
@@ -94,10 +94,6 @@
.asOption(),
mavenBundle().groupId("org.apache.sling").artifactId("org.apache.sling.servlet-helpers").versionAsInProject(),
mavenBundle().groupId("com.cedarsoftware").artifactId("json-io").versionAsInProject(),
- mavenBundle().groupId("javax.cache").artifactId("cache-api").versionAsInProject(),
- mavenBundle().groupId("com.typesafe").artifactId("config").versionAsInProject(),
- mavenBundle().groupId("com.github.ben-manes.caffeine").artifactId("caffeine").versionAsInProject(),
- mavenBundle().groupId("com.github.ben-manes.caffeine").artifactId("jcache").versionAsInProject(),
slingResourcePresence(),
jsonPath(),
junitBundles()
diff --git a/src/test/java/org/apache/sling/graphql/core/it/GraphQLServletIT.java b/src/test/java/org/apache/sling/graphql/core/it/GraphQLServletIT.java
index 9f2d45e..e110f45 100644
--- a/src/test/java/org/apache/sling/graphql/core/it/GraphQLServletIT.java
+++ b/src/test/java/org/apache/sling/graphql/core/it/GraphQLServletIT.java
@@ -26,7 +26,6 @@
import org.apache.sling.graphql.core.mocks.ReplacingSchemaProvider;
import org.apache.sling.resource.presence.ResourcePresence;
import org.apache.sling.servlethelpers.MockSlingHttpServletResponse;
-import org.apache.sling.servlethelpers.internalrequests.SlingInternalRequest;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.ops4j.pax.exam.Configuration;