This is an automated email from the ASF dual-hosted git repository.

radu pushed a commit to branch issue/SLING-10085
in repository 
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-graphql-core.git

commit 8ab360f1d5af9fbc1054bb70849d81daeee0e40d
Author: Radu Cotescu <[email protected]>
AuthorDate: Fri Jan 22 14:08:38 2021 +0100

    SLING-10085 - Cache the GraphQL schemas in the DefaultQueryExecutor
    
    * implemented a LRU cache for the GraphQL schemas
---
 .../core/cache/SimpleGraphQLCacheProvider.java     |  26 +--
 .../graphql/core/engine/DefaultQueryExecutor.java  | 176 +++++++++++++++++----
 .../sling/graphql/core/hash/SHA256Hasher.java      |  53 +++++++
 .../core/cache/SimpleGraphQLCacheProviderTest.java |   3 +-
 4 files changed, 200 insertions(+), 58 deletions(-)

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
index a2411a3..2e88bb1 100644
--- 
a/src/main/java/org/apache/sling/graphql/core/cache/SimpleGraphQLCacheProvider.java
+++ 
b/src/main/java/org/apache/sling/graphql/core/cache/SimpleGraphQLCacheProvider.java
@@ -18,9 +18,6 @@
  
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
 package org.apache.sling.graphql.core.cache;
 
-import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -37,7 +34,7 @@ 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.api.SlingGraphQLException;
+import org.apache.sling.graphql.core.hash.SHA256Hasher;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import org.osgi.framework.BundleContext;
@@ -157,7 +154,7 @@ public class SimpleGraphQLCacheProvider implements 
GraphQLCacheProvider {
     public String cacheQuery(@NotNull String query, @NotNull String 
resourceType, @Nullable String selectorString) {
         writeLock.lock();
         try {
-            String hash = getHash(query);
+            String hash = SHA256Hasher.getHash(query);
             String key = getCacheKey(hash, resourceType, selectorString);
             persistedQueriesCache.put(key, query);
             if (persistedQueriesCache.containsKey(key)) {
@@ -179,25 +176,6 @@ public class SimpleGraphQLCacheProvider implements 
GraphQLCacheProvider {
         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();
-    }
-
     /**
      * 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.
diff --git 
a/src/main/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutor.java 
b/src/main/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutor.java
index bed63df..1552ef1 100644
--- 
a/src/main/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutor.java
+++ 
b/src/main/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutor.java
@@ -18,30 +18,37 @@
  
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
 package org.apache.sling.graphql.core.engine;
 
+import java.util.Arrays;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
 
 import javax.script.ScriptException;
 
-import graphql.language.InterfaceTypeDefinition;
-import graphql.language.TypeDefinition;
-import graphql.language.UnionTypeDefinition;
-import graphql.schema.TypeResolver;
 import org.apache.sling.api.resource.Resource;
 import org.apache.sling.graphql.api.SchemaProvider;
 import org.apache.sling.graphql.api.SlingDataFetcher;
 import org.apache.sling.graphql.api.SlingGraphQLException;
+import org.apache.sling.graphql.api.SlingTypeResolver;
 import org.apache.sling.graphql.api.engine.QueryExecutor;
 import org.apache.sling.graphql.api.engine.ValidationResult;
-import org.apache.sling.graphql.api.SlingTypeResolver;
-import org.apache.sling.graphql.core.util.LogSanitizer;
+import org.apache.sling.graphql.core.hash.SHA256Hasher;
 import org.apache.sling.graphql.core.scalars.SlingScalarsProvider;
 import org.apache.sling.graphql.core.schema.RankedSchemaProviders;
+import org.apache.sling.graphql.core.util.LogSanitizer;
 import org.apache.sling.graphql.core.util.SlingGraphQLErrorHelper;
 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.component.annotations.Reference;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -54,12 +61,16 @@ import graphql.ParseAndValidateResult;
 import graphql.language.Argument;
 import graphql.language.Directive;
 import graphql.language.FieldDefinition;
+import graphql.language.InterfaceTypeDefinition;
 import graphql.language.ObjectTypeDefinition;
 import graphql.language.SourceLocation;
 import graphql.language.StringValue;
+import graphql.language.TypeDefinition;
+import graphql.language.UnionTypeDefinition;
 import graphql.schema.DataFetcher;
 import graphql.schema.GraphQLScalarType;
 import graphql.schema.GraphQLSchema;
+import graphql.schema.TypeResolver;
 import graphql.schema.idl.RuntimeWiring;
 import graphql.schema.idl.SchemaGenerator;
 import graphql.schema.idl.SchemaParser;
@@ -68,6 +79,7 @@ import graphql.schema.idl.TypeDefinitionRegistry;
 @Component(
         service = QueryExecutor.class
 )
+@Designate(ocd = DefaultQueryExecutor.Config.class)
 public class DefaultQueryExecutor implements QueryExecutor {
 
     private static final Logger LOGGER = 
LoggerFactory.getLogger(DefaultQueryExecutor.class);
@@ -84,6 +96,12 @@ public class DefaultQueryExecutor implements QueryExecutor {
 
     private static final LogSanitizer cleanLog = new LogSanitizer();
 
+    private Map<String, String> resourceToHashMap;
+    private Map<String, GraphQLSchema> hashToSchemaMap;
+    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
+    private final Lock readLock = readWriteLock.readLock();
+    private final Lock writeLock = readWriteLock.writeLock();
+
     @Reference
     private RankedSchemaProviders schemaProvider;
 
@@ -96,13 +114,40 @@ public class DefaultQueryExecutor implements QueryExecutor 
{
     @Reference
     private SlingScalarsProvider scalarsProvider;
 
+    @ObjectClassDefinition(
+            name = "Apache Sling Default GraphQL Query Executor"
+    )
+    @interface Config {
+        @AttributeDefinition(
+                name = "Schema Cache Size",
+                description = "The number of schemas to cache. Since a schema 
normally doesn't change often, they can be cached and " +
+                        "reused, rather than parsed by the engine all the 
time. The cache is a LRU and will store up to this number of " +
+                        "schemas."
+        )
+        int schemaCacheSize() default 512;
+    }
+
+    @Activate
+    public void activate(Config config) {
+        int schemaCacheSize = config.schemaCacheSize();
+        if (schemaCacheSize < 0) {
+            schemaCacheSize = 0;
+        }
+        resourceToHashMap = new LRUCache<>(schemaCacheSize);
+        hashToSchemaMap = new LRUCache<>(schemaCacheSize);
+    }
+
     @Override
     public ValidationResult validate(@NotNull String query, @NotNull 
Map<String, Object> variables, @NotNull Resource queryResource,
                                      @NotNull String[] selectors) {
         try {
             String schemaDef = prepareSchemaDefinition(schemaProvider, 
queryResource, selectors);
+            if (schemaDef == null) {
+                throw new SlingGraphQLException(String.format("Cannot get a 
schema for resource %s and selectors %s.", queryResource,
+                        Arrays.toString(selectors)));
+            }
             LOGGER.debug("Resource {} maps to GQL schema {}", 
queryResource.getPath(), schemaDef);
-            final GraphQLSchema schema = buildSchema(schemaDef, queryResource);
+            final GraphQLSchema schema = getSchema(schemaDef, queryResource, 
selectors);
             ExecutionInput executionInput = ExecutionInput.newExecutionInput()
                     .query(query)
                     .variables(variables)
@@ -133,12 +178,16 @@ public class DefaultQueryExecutor implements 
QueryExecutor {
         String schemaDef = null;
         try {
             schemaDef = prepareSchemaDefinition(schemaProvider, queryResource, 
selectors);
+            if (schemaDef == null) {
+                throw new SlingGraphQLException(String.format("Cannot get a 
schema for resource %s and selectors %s.", queryResource,
+                        Arrays.toString(selectors)));
+            }
             LOGGER.debug("Resource {} maps to GQL schema {}", 
queryResource.getPath(), schemaDef);
-            final GraphQLSchema schema = buildSchema(schemaDef, queryResource);
+            final GraphQLSchema schema = getSchema(schemaDef, queryResource, 
selectors);
             final GraphQL graphQL = GraphQL.newGraphQL(schema).build();
-            if(LOGGER.isDebugEnabled()) {
+            if (LOGGER.isDebugEnabled()) {
                 LOGGER.debug("Executing query\n[{}]\nat [{}] with variables 
[{}]",
-                    cleanLog.sanitize(query), queryResource.getPath(), 
cleanLog.sanitize(variables.toString()));
+                        cleanLog.sanitize(query), queryResource.getPath(), 
cleanLog.sanitize(variables.toString()));
             }
             ExecutionInput ei = ExecutionInput.newExecutionInput()
                     .query(query)
@@ -148,35 +197,29 @@ public class DefaultQueryExecutor implements 
QueryExecutor {
             if (!result.getErrors().isEmpty()) {
                 StringBuilder errors = new StringBuilder();
                 for (GraphQLError error : result.getErrors()) {
-                    errors.append("Error: 
type=").append(error.getErrorType().toString()).append("; 
message=").append(error.getMessage()).append(System.lineSeparator());
+                    errors.append("Error: 
type=").append(error.getErrorType().toString()).append("; 
message=").append(error.getMessage())
+                            .append(System.lineSeparator());
                     if (error.getLocations() != null) {
                         for (SourceLocation location : error.getLocations()) {
                             
errors.append("location=").append(location.getLine()).append(",").append(location.getColumn()).append(";");
                         }
                     }
                 }
-                if(LOGGER.isErrorEnabled()) {
+                if (LOGGER.isErrorEnabled()) {
                     LOGGER.error("Query failed for Resource {}: query={} 
Errors:{}, schema={}",
-                        queryResource.getPath(), cleanLog.sanitize(query), 
errors, schemaDef);
+                            queryResource.getPath(), cleanLog.sanitize(query), 
errors, schemaDef);
                 }
             }
             LOGGER.debug("ExecutionResult.isDataPresent={}", 
result.isDataPresent());
             return result.toSpecification();
         } catch (Exception e) {
-            final String message = String.format("Query failed for Resource 
%s: query=%s", queryResource.getPath(), cleanLog.sanitize(query));
+            final String message =
+                    String.format("Query failed for Resource %s: query=%s", 
queryResource.getPath(), cleanLog.sanitize(query));
             LOGGER.error(String.format("%s, schema=%s", message, schemaDef), 
e);
             return SlingGraphQLErrorHelper.toSpecification(message, e);
         }
     }
 
-    private GraphQLSchema buildSchema(String sdl, Resource currentResource) {
-        TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);
-        Iterable<GraphQLScalarType> scalars = 
scalarsProvider.getCustomScalars(typeRegistry.scalars());
-        RuntimeWiring runtimeWiring = buildWiring(typeRegistry, scalars, 
currentResource);
-        SchemaGenerator schemaGenerator = new SchemaGenerator();
-        return schemaGenerator.makeExecutableSchema(typeRegistry, 
runtimeWiring);
-    }
-
     private RuntimeWiring buildWiring(TypeDefinitionRegistry typeRegistry, 
Iterable<GraphQLScalarType> scalars, Resource r) {
         List<ObjectTypeDefinition> types = 
typeRegistry.getTypes(ObjectTypeDefinition.class);
         RuntimeWiring.Builder builder = RuntimeWiring.newRuntimeWiring();
@@ -189,7 +232,7 @@ public class DefaultQueryExecutor implements QueryExecutor {
                             typeWiring.dataFetcher(field.getName(), fetcher);
                         }
                     } catch (SlingGraphQLException e) {
-                        throw  e;
+                        throw e;
                     } catch (Exception e) {
                         throw new SlingGraphQLException("Exception while 
building wiring.", e);
                     }
@@ -218,15 +261,15 @@ public class DefaultQueryExecutor implements 
QueryExecutor {
             }
         } catch (SlingGraphQLException e) {
             throw e;
-        } catch(Exception e) {
+        } catch (Exception e) {
             throw new SlingGraphQLException("Exception while building 
wiring.", e);
         }
     }
 
     private String getDirectiveArgumentValue(Directive d, String name) {
         final Argument a = d.getArgument(name);
-        if(a != null && a.getValue() instanceof StringValue) {
-            return ((StringValue)a.getValue()).getValue();
+        if (a != null && a.getValue() instanceof StringValue) {
+            return ((StringValue) a.getValue()).getValue();
         }
         return null;
     }
@@ -249,13 +292,13 @@ public class DefaultQueryExecutor implements 
QueryExecutor {
 
     private DataFetcher<Object> getDataFetcher(FieldDefinition field, Resource 
currentResource) {
         DataFetcher<Object> result = null;
-        final Directive d =field.getDirective(FETCHER_DIRECTIVE);
-        if(d != null) {
+        final Directive d = field.getDirective(FETCHER_DIRECTIVE);
+        if (d != null) {
             final String name = 
validateFetcherName(getDirectiveArgumentValue(d, FETCHER_NAME));
             final String options = getDirectiveArgumentValue(d, 
FETCHER_OPTIONS);
             final String source = getDirectiveArgumentValue(d, FETCHER_SOURCE);
             SlingDataFetcher<Object> f = 
dataFetcherSelector.getSlingFetcher(name);
-            if(f != null) {
+            if (f != null) {
                 result = new SlingDataFetcherWrapper<>(f, currentResource, 
options, source);
             }
         }
@@ -265,12 +308,12 @@ public class DefaultQueryExecutor implements 
QueryExecutor {
     private <T extends TypeDefinition<T>> TypeResolver 
getTypeResolver(TypeDefinition<T> typeDefinition, Resource currentResource) {
         TypeResolver resolver = null;
         final Directive d = typeDefinition.getDirective(RESOLVER_DIRECTIVE);
-        if(d != null) {
+        if (d != null) {
             final String name = 
validateResolverName(getDirectiveArgumentValue(d, RESOLVER_NAME));
             final String options = getDirectiveArgumentValue(d, 
RESOLVER_OPTIONS);
             final String source = getDirectiveArgumentValue(d, 
RESOLVER_SOURCE);
             SlingTypeResolver<Object> r = 
typeResolverSelector.getSlingTypeResolver(name);
-            if(r != null) {
+            if (r != null) {
                 resolver = new SlingTypeResolverWrapper(r, currentResource, 
options, source);
             }
         }
@@ -278,8 +321,8 @@ public class DefaultQueryExecutor implements QueryExecutor {
     }
 
     private @Nullable String prepareSchemaDefinition(@NotNull SchemaProvider 
schemaProvider,
-                                                            @NotNull 
org.apache.sling.api.resource.Resource resource,
-                                                            @NotNull String[] 
selectors) throws ScriptException {
+                                                     @NotNull 
org.apache.sling.api.resource.Resource resource,
+                                                     @NotNull String[] 
selectors) throws ScriptException {
         try {
             return schemaProvider.getSchema(resource, selectors);
         } catch (Exception e) {
@@ -289,4 +332,71 @@ public class DefaultQueryExecutor implements QueryExecutor 
{
             throw up;
         }
     }
+
+    GraphQLSchema getSchema(@NotNull String sdl, @NotNull Resource 
currentResource, @NotNull String[] selectors) {
+        readLock.lock();
+        String newHash = SHA256Hasher.getHash(sdl);
+        String resourceToHashMapKey = getCacheKey(currentResource, selectors);
+        String oldHash = resourceToHashMap.get(resourceToHashMapKey);
+        if (!newHash.equals(oldHash)) {
+            readLock.unlock();
+            writeLock.lock();
+            try {
+                oldHash = resourceToHashMap.get(resourceToHashMapKey);
+                if (!newHash.equals(oldHash)) {
+                    resourceToHashMap.put(resourceToHashMapKey, newHash);
+                    TypeDefinitionRegistry typeRegistry = new 
SchemaParser().parse(sdl);
+                    Iterable<GraphQLScalarType> scalars = 
scalarsProvider.getCustomScalars(typeRegistry.scalars());
+                    RuntimeWiring runtimeWiring = buildWiring(typeRegistry, 
scalars, currentResource);
+                    SchemaGenerator schemaGenerator = new SchemaGenerator();
+                    hashToSchemaMap.put(newHash, 
schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring));
+                }
+                readLock.lock();
+            } finally {
+                writeLock.unlock();
+            }
+        }
+        try {
+            return hashToSchemaMap.get(newHash);
+        } finally {
+            readLock.unlock();
+        }
+
+    }
+
+    private String getCacheKey(@NotNull Resource resource, @NotNull String[] 
selectors) {
+        return resource.getPath() + ":" + String.join(".", selectors);
+    }
+
+    private static class LRUCache<T> extends LinkedHashMap<String, T> {
+
+        private final int capacity;
+
+        public LRUCache(int capacity) {
+            this.capacity = capacity;
+        }
+
+        @Override
+        protected boolean removeEldestEntry(Map.Entry<String, T> eldest) {
+            return capacity > 0 && size() == capacity;
+        }
+
+        @Override
+        public int hashCode() {
+            return super.hashCode() + Objects.hashCode(capacity);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o == this) {
+                return true;
+            }
+            if (o instanceof LRUCache) {
+                LRUCache<T> other = (LRUCache<T>) o;
+                return super.equals(o) && capacity == other.capacity;
+            }
+            return false;
+        }
+    }
+
 }
diff --git a/src/main/java/org/apache/sling/graphql/core/hash/SHA256Hasher.java 
b/src/main/java/org/apache/sling/graphql/core/hash/SHA256Hasher.java
new file mode 100644
index 0000000..c853034
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/core/hash/SHA256Hasher.java
@@ -0,0 +1,53 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ 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.hash;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import org.apache.sling.graphql.api.SlingGraphQLException;
+import org.jetbrains.annotations.NotNull;
+
+public class SHA256Hasher {
+
+    private SHA256Hasher() {
+    }
+
+    @NotNull
+    public static String getHash(@NotNull String message) {
+        StringBuilder buffer = new StringBuilder();
+        try {
+            MessageDigest digest = MessageDigest.getInstance("SHA-256");
+            byte[] hash = 
digest.digest(message.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 message.", e);
+        }
+        return buffer.toString();
+    }
+}
+
diff --git 
a/src/test/java/org/apache/sling/graphql/core/cache/SimpleGraphQLCacheProviderTest.java
 
b/src/test/java/org/apache/sling/graphql/core/cache/SimpleGraphQLCacheProviderTest.java
index d69c19e..31a76eb 100644
--- 
a/src/test/java/org/apache/sling/graphql/core/cache/SimpleGraphQLCacheProviderTest.java
+++ 
b/src/test/java/org/apache/sling/graphql/core/cache/SimpleGraphQLCacheProviderTest.java
@@ -22,6 +22,7 @@ import org.apache.sling.commons.metrics.Counter;
 import org.apache.sling.commons.metrics.MetricsService;
 import org.apache.sling.commons.metrics.Timer;
 import org.apache.sling.graphql.api.cache.GraphQLCacheProvider;
+import org.apache.sling.graphql.core.hash.SHA256Hasher;
 import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
 import org.junit.Before;
 import org.junit.Rule;
@@ -60,7 +61,7 @@ public class SimpleGraphQLCacheProviderTest {
         context.registerInjectActivateService(new 
SimpleGraphQLCacheProvider());
         SimpleGraphQLCacheProvider provider = (SimpleGraphQLCacheProvider) 
context.getService(GraphQLCacheProvider.class);
         assertNotNull(provider);
-        
assertEquals("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
 provider.getHash("hello world"));
+        
assertEquals("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
 SHA256Hasher.getHash("hello world"));
     }
 
     @Test

Reply via email to