Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ dependencies {
implementation 'org.antlr:antlr4-runtime:' + antlrVersion
implementation 'com.google.guava:guava:' + guavaVersion

// we can compile against caffeine but its not shipped as a runtime dependency
compileOnly 'com.github.ben-manes.caffeine:caffeine:3.1.8'
// we need caffeine to write tests however
testImplementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'


testImplementation group: 'junit', name: 'junit', version: '4.13.2'
testImplementation 'org.spockframework:spock-core:2.3-groovy-4.0'
testImplementation 'net.bytebuddy:byte-buddy:1.17.7'
Expand Down
83 changes: 83 additions & 0 deletions src/jmh/java/benchmark/CachingDocumentBenchmark.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package benchmark;

import graphql.GraphQL;
import graphql.StarWarsSchema;
import graphql.execution.preparsed.NoOpPreparsedDocumentProvider;
import graphql.execution.preparsed.PreparsedDocumentProvider;
import graphql.execution.preparsed.caching.CachingDocumentProvider;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;

import java.util.concurrent.TimeUnit;

@State(Scope.Benchmark)
@Warmup(iterations = 2, time = 5)
@Measurement(iterations = 3, time = 2)
@Fork(2)
public class CachingDocumentBenchmark {

@Param({"50", "500", "5000"})
public int querySize;

@Param({"10", "50", "500"})
public int queryCount;

@Setup(Level.Trial)
public void setUp() {
}

private static final GraphQL GRAPHQL_CACHING_ON = buildGraphQL(true);
private static final GraphQL GRAPHQL_CACHING_OFF = buildGraphQL(false);

@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public void benchMarkCachingOnAvgTime() {
executeQuery(true);
}

@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public void benchMarkCachingOffAvgTime() {
executeQuery(false);
}

public void executeQuery(boolean cachingOn) {
String query = buildQuery(querySize);
GraphQL graphQL = cachingOn ? GRAPHQL_CACHING_ON : GRAPHQL_CACHING_OFF;

for (int i = 0; i < queryCount; i++) {
graphQL.execute(query);
}
}

private static String buildQuery(int howManyAliases) {
StringBuilder query = new StringBuilder("query q { hero { \n");
for (int i = 0; i < howManyAliases; i++) {
query.append("nameAlias").append(i).append(" : name\n");
}
query.append("}}");
return query.toString();
}

private static GraphQL buildGraphQL(boolean cachingOn) {
PreparsedDocumentProvider documentProvider = NoOpPreparsedDocumentProvider.INSTANCE;
if (cachingOn) {
documentProvider = new CachingDocumentProvider();
}
return GraphQL.newGraphQL(StarWarsSchema.starWarsSchema)
.preparsedDocumentProvider(documentProvider)
.build();
}
}
28 changes: 27 additions & 1 deletion src/main/java/graphql/GraphQL.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import graphql.execution.preparsed.NoOpPreparsedDocumentProvider;
import graphql.execution.preparsed.PreparsedDocumentEntry;
import graphql.execution.preparsed.PreparsedDocumentProvider;
import graphql.execution.preparsed.caching.CachingDocumentProvider;
import graphql.language.Document;
import graphql.schema.GraphQLSchema;
import graphql.validation.ValidationError;
Expand Down Expand Up @@ -279,7 +280,8 @@ public static class Builder {
private DataFetcherExceptionHandler defaultExceptionHandler = new SimpleDataFetcherExceptionHandler();
private ExecutionIdProvider idProvider = DEFAULT_EXECUTION_ID_PROVIDER;
private Instrumentation instrumentation = null; // deliberate default here
private PreparsedDocumentProvider preparsedDocumentProvider = NoOpPreparsedDocumentProvider.INSTANCE;
private PreparsedDocumentProvider preparsedDocumentProvider = null;
private boolean doNotCacheOperationDocuments = false;
private boolean doNotAutomaticallyDispatchDataLoader = false;
private ValueUnboxer valueUnboxer = ValueUnboxer.DEFAULT;

Expand Down Expand Up @@ -325,11 +327,32 @@ public Builder instrumentation(Instrumentation instrumentation) {
return this;
}

/**
* A {@link PreparsedDocumentProvider} allows you to provide a custom implementation of how
* operation documents are retrieved and possibly cached. By default, the inbuilt {@link CachingDocumentProvider}
* will be used, but you can replace that with your own implementation
*
* @param preparsedDocumentProvider the provider to use
*
* @return this builder
*/
public Builder preparsedDocumentProvider(PreparsedDocumentProvider preparsedDocumentProvider) {
this.preparsedDocumentProvider = assertNotNull(preparsedDocumentProvider, () -> "PreparsedDocumentProvider must be non null");
return this;
}


/**
* Deactivates the caching of operation documents via the inbuilt {@link graphql.execution.preparsed.caching.CachingDocumentProvider}
* If deactivated, no caching will be performed
*
* @return this builder
*/
public Builder doNotCacheOperationDocuments() {
this.doNotCacheOperationDocuments = true;
return this;
}

public Builder executionIdProvider(ExecutionIdProvider executionIdProvider) {
this.idProvider = assertNotNull(executionIdProvider, () -> "ExecutionIdProvider must be non null");
return this;
Expand Down Expand Up @@ -367,6 +390,9 @@ public GraphQL build() {
if (instrumentation == null) {
this.instrumentation = SimplePerformantInstrumentation.INSTANCE;
}
if (preparsedDocumentProvider == null) {
preparsedDocumentProvider = (doNotCacheOperationDocuments ? NoOpPreparsedDocumentProvider.INSTANCE : new CachingDocumentProvider());
}
return new GraphQL(this);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@


import graphql.ExecutionInput;
import graphql.Internal;
import graphql.PublicApi;
import org.jspecify.annotations.NullMarked;

import java.util.concurrent.CompletableFuture;
import java.util.function.Function;

@Internal
/**
* A {@link PreparsedDocumentProvider that does nothing}
*/
@PublicApi
@NullMarked
public class NoOpPreparsedDocumentProvider implements PreparsedDocumentProvider {
public static final NoOpPreparsedDocumentProvider INSTANCE = new NoOpPreparsedDocumentProvider();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package graphql.execution.preparsed.caching;

import com.github.benmanes.caffeine.cache.Caffeine;
import graphql.ExecutionInput;
import graphql.PublicApi;
import graphql.execution.preparsed.PreparsedDocumentEntry;
import graphql.execution.preparsed.PreparsedDocumentProvider;
import org.jspecify.annotations.NullMarked;

import java.util.concurrent.CompletableFuture;
import java.util.function.Function;

import static graphql.execution.Async.toCompletableFuture;

/**
* The CachingDocumentProvider allows previously parsed and validated operations to be cached and
* hence re-used. This can lead to significant time savings, especially for large operations.
* <p>
* By default, graphql-java will cache the parsed {@link PreparsedDocumentEntry} that represents
* a parsed and validated graphql query IF {@link Caffeine} is present on the class path
* at runtime. If it's not then no caching takes place.
* <p>
* You can provide your own {@link DocumentCache} implementation and hence use any cache
* technology you like.
*/
@PublicApi
@NullMarked
public class CachingDocumentProvider implements PreparsedDocumentProvider {
private final DocumentCache documentCache;

/**
* By default, it will try to use a {@link Caffeine} backed implementation if it's on the class
* path otherwise it will become a non caching mechanism.
*
* @see CaffeineDocumentCache
*/
public CachingDocumentProvider() {
this(new CaffeineDocumentCache());
}

/**
* You can use your own cache implementation and provide that to this class to use
*
* @param documentCache the cache to use
*/
public CachingDocumentProvider(DocumentCache documentCache) {
this.documentCache = documentCache;
}

/**
* @return the {@link DocumentCache} being used
*/
public DocumentCache getDocumentCache() {
return documentCache;
}

@Override
public CompletableFuture<PreparsedDocumentEntry> getDocumentAsync(ExecutionInput executionInput, Function<ExecutionInput, PreparsedDocumentEntry> parseAndValidateFunction) {
if (documentCache.isNoop()) {
// saves creating keys and doing a lookup that will just call this function anyway
return toCompletableFuture(parseAndValidateFunction.apply(executionInput));
}
DocumentCache.DocumentCacheKey cacheKey = new DocumentCache.DocumentCacheKey(executionInput.getQuery(), executionInput.getOperationName(), executionInput.getLocale());
Object cacheEntry = documentCache.get(cacheKey, key -> parseAndValidateFunction.apply(executionInput));
return toCompletableFuture(cacheEntry);
}

}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its surprisingly little code

Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package graphql.execution.preparsed.caching;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import graphql.PublicApi;
import graphql.execution.preparsed.PreparsedDocumentEntry;
import graphql.util.ClassKit;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

import java.util.function.Function;

import static java.util.Objects.requireNonNull;

@PublicApi
@NullMarked
public class CaffeineDocumentCache implements DocumentCache {

private final static boolean isCaffeineAvailable = ClassKit.isClassAvailable("com.github.benmanes.caffeine.cache.Caffeine");

@Nullable
private final Object caffeineCacheObj;

CaffeineDocumentCache(boolean isCaffeineAvailable) {
if (isCaffeineAvailable) {
CaffeineDocumentCacheOptions options = CaffeineDocumentCacheOptions.getDefaultJvmOptions();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice it never loads an Caffeine code unless Caffeine is on the class path

caffeineCacheObj = Caffeine.newBuilder()
.expireAfterAccess(options.getExpireAfterAccess())
.maximumSize(options.getMaxSize())
.build();
} else {
caffeineCacheObj = null;
}
}

/**
* Creates a cache that works if Caffeine is on the class path otherwise its
* a no op.
*/
public CaffeineDocumentCache() {
this(isCaffeineAvailable);
}

/**
* If you want to control the {@link Caffeine} configuration, using this constructor and pass in your own {@link Caffeine} cache
*
* @param caffeineCache the custom {@link Caffeine} cache to use
*/
public CaffeineDocumentCache(Cache<DocumentCache.DocumentCacheKey, PreparsedDocumentEntry> caffeineCache) {
this.caffeineCacheObj = caffeineCache;
}

@Override
public PreparsedDocumentEntry get(DocumentCacheKey key, Function<DocumentCacheKey, PreparsedDocumentEntry> mappingFunction) {
if (isNoop()) {
return mappingFunction.apply(key);
}
return cache().get(key, mappingFunction);
}

private Cache<DocumentCache.DocumentCacheKey, PreparsedDocumentEntry> cache() {
//noinspection unchecked
return (Cache<DocumentCacheKey, PreparsedDocumentEntry>) requireNonNull(caffeineCacheObj);
}

@Override
public boolean isNoop() {
return caffeineCacheObj == null;
}

@Override
public void invalidateAll() {
if (!isNoop()) {
cache().invalidateAll();
}
}
}
Loading