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
