You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ra...@apache.org on 2021/01/22 17:37:25 UTC

[sling-org-apache-sling-graphql-core] branch master updated: SLING-10085 - Cache the GraphQL schemas in the DefaultQueryExecutor

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

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


The following commit(s) were added to refs/heads/master by this push:
     new f83f930  SLING-10085 - Cache the GraphQL schemas in the DefaultQueryExecutor
f83f930 is described below

commit f83f9303d04ef979ab3c1304e9106948af6f27c3
Author: Radu Cotescu <17...@users.noreply.github.com>
AuthorDate: Fri Jan 22 18:37:18 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  | 183 +++++++++++++++++----
 .../sling/graphql/core/hash/SHA256Hasher.java      |  53 ++++++
 .../core/cache/SimpleGraphQLCacheProviderTest.java |   3 +-
 .../core/engine/DefaultQueryExecutorTest.java      |  61 ++++++-
 5 files changed, 265 insertions(+), 61 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..063f3ff 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,39 @@ 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 compiled GraphQL 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 128;
+    }
+
+    @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 +177,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 +196,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 +231,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 +260,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 +291,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 +307,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 +320,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 +331,79 @@ 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);
+        /*
+        Since the SchemaProviders that generate the SDL can dynamically change, but also since the resource is passed to the RuntimeWiring,
+        there's a two stage cache:
+
+        1. a mapping between the resource, selectors and the SDL's hash
+        2. a mapping between the hash and the compiled GraphQL schema
+         */
+        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();
+                    GraphQLSchema schema = schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
+                    hashToSchemaMap.put(newHash, schema);
+                    return schema;
+                }
+                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 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
diff --git a/src/test/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutorTest.java b/src/test/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutorTest.java
index 1ff6b1c..cbad4f2 100644
--- a/src/test/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutorTest.java
+++ b/src/test/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutorTest.java
@@ -18,11 +18,14 @@
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
 package org.apache.sling.graphql.core.engine;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Dictionary;
+import java.util.HashMap;
 import java.util.Hashtable;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 
 import org.apache.sling.graphql.api.SchemaProvider;
@@ -32,20 +35,24 @@ import org.apache.sling.graphql.api.SlingDataFetcher;
 import org.apache.sling.graphql.api.SlingGraphQLException;
 import org.apache.sling.graphql.api.engine.QueryExecutor;
 import org.apache.sling.graphql.api.engine.ValidationResult;
+import org.apache.sling.graphql.core.mocks.CharacterTypeResolver;
 import org.apache.sling.graphql.core.mocks.DigestDataFetcher;
+import org.apache.sling.graphql.core.mocks.DroidDTO;
 import org.apache.sling.graphql.core.mocks.DummyTypeResolver;
 import org.apache.sling.graphql.core.mocks.EchoDataFetcher;
 import org.apache.sling.graphql.core.mocks.FailingDataFetcher;
+import org.apache.sling.graphql.core.mocks.HumanDTO;
 import org.apache.sling.graphql.core.mocks.MockSchemaProvider;
 import org.apache.sling.graphql.core.mocks.TestUtil;
-import org.apache.sling.graphql.core.mocks.DroidDTO;
-import org.apache.sling.graphql.core.mocks.HumanDTO;
-import org.apache.sling.graphql.core.mocks.CharacterTypeResolver;
+import org.apache.sling.graphql.core.schema.RankedSchemaProviders;
+import org.apache.sling.testing.mock.osgi.MockOsgi;
 import org.junit.Test;
 import org.osgi.framework.Constants;
 import org.osgi.framework.ServiceReference;
 import org.osgi.framework.ServiceRegistration;
 
+import graphql.schema.GraphQLSchema;
+
 import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
@@ -53,6 +60,7 @@ import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.nullValue;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
@@ -321,4 +329,51 @@ public class DefaultQueryExecutorTest extends ResourceQueryTestBase {
         }
     }
 
+    @Test
+    public void testCachedSchemas() throws IOException {
+        // by default we'll get the test-schema
+        final DefaultQueryExecutor queryExecutor = (DefaultQueryExecutor) context.getService(QueryExecutor.class);
+        final RankedSchemaProviders schemaProvider = context.getService(RankedSchemaProviders.class);
+        assertNotNull(queryExecutor);
+        assertNotNull(schemaProvider);
+        String[] selectors = new String[]{};
+        GraphQLSchema schema1 = queryExecutor.getSchema(schemaProvider.getSchema(resource, selectors), resource, selectors);
+        GraphQLSchema schema2 = queryExecutor.getSchema(schemaProvider.getSchema(resource, selectors), resource, selectors);
+        assertEquals(schema1, schema2);
+
+        // change the schema provider
+        context.registerService(SchemaProvider.class, new MockSchemaProvider("test-schema-selected-foryou"), Constants.SERVICE_RANKING,
+                Integer.MAX_VALUE);
+        GraphQLSchema schema3 = queryExecutor.getSchema(schemaProvider.getSchema(resource, selectors), resource, selectors);
+        GraphQLSchema schema4 = queryExecutor.getSchema(schemaProvider.getSchema(resource, selectors), resource, selectors);
+        assertEquals(schema3, schema4);
+        assertNotEquals(schema1, schema3);
+    }
+
+    @Test
+    public void testSchemasWithTheCacheDisabled() throws IOException {
+        Map<String, Object> properties = new HashMap<>();
+        properties.put("schemaCacheSize", 0);
+
+        DefaultQueryExecutor queryExecutor = (DefaultQueryExecutor) context.registerService(QueryExecutor.class, new DefaultQueryExecutor(),
+                properties);
+        MockOsgi.injectServices(queryExecutor, context.bundleContext(), properties);
+        MockOsgi.activate(queryExecutor, context.bundleContext(), properties);
+
+        final RankedSchemaProviders schemaProvider = context.getService(RankedSchemaProviders.class);
+        assertNotNull(queryExecutor);
+        assertNotNull(schemaProvider);
+        String[] selectors = new String[]{};
+        GraphQLSchema schema1 = queryExecutor.getSchema(schemaProvider.getSchema(resource, selectors), resource, selectors);
+        GraphQLSchema schema2 = queryExecutor.getSchema(schemaProvider.getSchema(resource, selectors), resource, selectors);
+        assertNotEquals(schema1, schema2);
+
+        // change the schema provider
+        context.registerService(SchemaProvider.class, new MockSchemaProvider("test-schema-selected-foryou"), Constants.SERVICE_RANKING,
+                Integer.MAX_VALUE);
+        GraphQLSchema schema3 = queryExecutor.getSchema(schemaProvider.getSchema(resource, selectors), resource, selectors);
+        GraphQLSchema schema4 = queryExecutor.getSchema(schemaProvider.getSchema(resource, selectors), resource, selectors);
+        assertNotEquals(schema3, schema4);
+    }
+
 }