Skip to content

Added ability to cache the whole graphql response #247

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Nov 19, 2020
Merged
Show file tree
Hide file tree
Changes from 7 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
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,29 @@

import graphql.ExecutionResult;
import graphql.kickstart.execution.GraphQLObjectMapper;
import graphql.kickstart.execution.input.GraphQLInvocationInput;
import graphql.kickstart.servlet.cache.CachedResponse;
import graphql.kickstart.servlet.cache.GraphQLResponseCache;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;

@Slf4j
@RequiredArgsConstructor
class BatchedQueryResponseWriter implements QueryResponseWriter {

private final List<ExecutionResult> results;
private final GraphQLObjectMapper graphQLObjectMapper;
private final GraphQLInvocationInput invocationInput;

@Override
public void write(HttpServletRequest request, HttpServletResponse response) throws IOException {
public void write(HttpServletRequest request, HttpServletResponse response, GraphQLResponseCache responseCache) throws IOException {
response.setContentType(HttpRequestHandler.APPLICATION_JSON_UTF8);
response.setStatus(HttpRequestHandler.STATUS_OK);

Expand All @@ -34,6 +41,16 @@ public void write(HttpServletRequest request, HttpServletResponse response) thro

String responseContent = responseBuilder.toString();
byte[] contentBytes = responseContent.getBytes(StandardCharsets.UTF_8);

if (responseCache != null && responseCache.isCacheable(request, invocationInput)) {
Copy link
Member

Choose a reason for hiding this comment

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

This block or something nearly identical is repeated in every QueryResponseWriter implementation. Instead it would be better if you'd apply the decorator pattern. If somebody enabled caching then have the QueryResponseWriter factory method create some CachingResponseWriter (which should implement QueryResponseWriter too obviously) wrapping the other implementations. That way you have one class with this logic and you don't pollute the other classes with caching logic. Ideally they're not aware of that at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

try {
responseCache.put(request, invocationInput, CachedResponse.ofContent(contentBytes));
} catch (Throwable t) {
log.warn(t.getMessage(), t);
Copy link
Member

Choose a reason for hiding this comment

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

I'd loose this log statement and just add the stacktrace to the log statement on the next line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

log.warn("Ignore read from cache, unexpected error happened");
}
}

response.setContentLength(contentBytes.length);
response.getOutputStream().write(contentBytes);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,31 @@
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import graphql.kickstart.execution.input.GraphQLInvocationInput;
import graphql.kickstart.servlet.cache.CachedResponse;
import graphql.kickstart.servlet.cache.GraphQLResponseCache;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RequiredArgsConstructor
class ErrorQueryResponseWriter implements QueryResponseWriter {

private final int statusCode;
private final String message;
private final GraphQLInvocationInput invocationInput;

@Override
public void write(HttpServletRequest request, HttpServletResponse response) throws IOException {
public void write(HttpServletRequest request, HttpServletResponse response, GraphQLResponseCache responseCache) throws IOException {
if (responseCache != null && responseCache.isCacheable(request, invocationInput)) {
try {
responseCache.put(request, invocationInput, CachedResponse.ofError(statusCode, message));
} catch (Throwable t) {
log.warn(t.getMessage(), t);
log.warn("Ignore read from cache, unexpected error happened");
}
}
response.sendError(statusCode, message);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import graphql.kickstart.execution.GraphQLObjectMapper;
import graphql.kickstart.execution.GraphQLQueryInvoker;
import graphql.kickstart.execution.context.ContextSetting;
import graphql.kickstart.servlet.cache.GraphQLResponseCache;
import graphql.kickstart.servlet.config.DefaultGraphQLSchemaServletProvider;
import graphql.kickstart.servlet.config.GraphQLSchemaServletProvider;
import graphql.kickstart.servlet.context.GraphQLServletContextBuilder;
Expand Down Expand Up @@ -33,12 +34,13 @@ public class GraphQLConfiguration {
private final Executor asyncExecutor;
private final long subscriptionTimeout;
private final ContextSetting contextSetting;
private final GraphQLResponseCache responseCache;

private GraphQLConfiguration(GraphQLInvocationInputFactory invocationInputFactory,
GraphQLQueryInvoker queryInvoker,
GraphQLObjectMapper objectMapper, List<GraphQLServletListener> listeners, boolean asyncServletModeEnabled,
Executor asyncExecutor, long subscriptionTimeout, ContextSetting contextSetting,
Supplier<BatchInputPreProcessor> batchInputPreProcessor) {
Supplier<BatchInputPreProcessor> batchInputPreProcessor, GraphQLResponseCache responseCache) {
this.invocationInputFactory = invocationInputFactory;
this.queryInvoker = queryInvoker;
this.graphQLInvoker = queryInvoker.toGraphQLInvoker();
Expand All @@ -49,6 +51,7 @@ private GraphQLConfiguration(GraphQLInvocationInputFactory invocationInputFactor
this.subscriptionTimeout = subscriptionTimeout;
this.contextSetting = contextSetting;
this.batchInputPreProcessor = batchInputPreProcessor;
this.responseCache = responseCache;
}

public static GraphQLConfiguration.Builder with(GraphQLSchema schema) {
Expand Down Expand Up @@ -109,6 +112,10 @@ public BatchInputPreProcessor getBatchInputPreProcessor() {
return batchInputPreProcessor.get();
}

public GraphQLResponseCache getResponseCache() {
return responseCache;
}

public static class Builder {

private GraphQLInvocationInputFactory.Builder invocationInputFactoryBuilder;
Expand All @@ -121,6 +128,7 @@ public static class Builder {
private long subscriptionTimeout = 0;
private ContextSetting contextSetting = ContextSetting.PER_QUERY_WITH_INSTRUMENTATION;
private Supplier<BatchInputPreProcessor> batchInputPreProcessorSupplier = () -> new NoOpBatchInputPreProcessor();
private GraphQLResponseCache responseCache;

private Builder(GraphQLInvocationInputFactory.Builder invocationInputFactoryBuilder) {
this.invocationInputFactoryBuilder = invocationInputFactoryBuilder;
Expand Down Expand Up @@ -199,6 +207,11 @@ public Builder with(Supplier<BatchInputPreProcessor> batchInputPreProcessor) {
return this;
}

public Builder with(GraphQLResponseCache responseCache) {
this.responseCache = responseCache;
return this;
}

public GraphQLConfiguration build() {
return new GraphQLConfiguration(
this.invocationInputFactory != null ? this.invocationInputFactory : invocationInputFactoryBuilder.build(),
Expand All @@ -209,7 +222,8 @@ public GraphQLConfiguration build() {
asyncExecutor,
subscriptionTimeout,
contextSetting,
batchInputPreProcessorSupplier
batchInputPreProcessorSupplier,
responseCache
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

interface HttpRequestHandler {
public interface HttpRequestHandler {

String APPLICATION_JSON_UTF8 = "application/json;charset=UTF-8";
String APPLICATION_EVENT_STREAM_UTF8 = "text/event-stream;charset=UTF-8";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@
import graphql.GraphQLException;
import graphql.kickstart.execution.GraphQLInvoker;
import graphql.kickstart.execution.GraphQLQueryResult;
import graphql.kickstart.servlet.input.BatchInputPreProcessResult;
import graphql.kickstart.servlet.input.BatchInputPreProcessor;
import graphql.kickstart.execution.input.GraphQLBatchedInvocationInput;
import graphql.kickstart.execution.input.GraphQLInvocationInput;
import graphql.kickstart.execution.input.GraphQLSingleInvocationInput;
import java.io.IOException;
import graphql.kickstart.servlet.cache.CacheResponseWriter;
import graphql.kickstart.servlet.cache.CachedResponse;
import graphql.kickstart.servlet.input.BatchInputPreProcessResult;
import graphql.kickstart.servlet.input.BatchInputPreProcessor;
import lombok.extern.slf4j.Slf4j;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;

@Slf4j
class HttpRequestHandlerImpl implements HttpRequestHandler {
Expand Down Expand Up @@ -49,11 +52,31 @@ public void handle(HttpServletRequest request, HttpServletResponse response) thr
private void execute(GraphQLInvocationInput invocationInput, HttpServletRequest request,
HttpServletResponse response) {
try {
if (configuration.getResponseCache() != null) {
Copy link
Member

Choose a reason for hiding this comment

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

Bit too much logic here imo. Would be better if you'd move this part into a separate class and just call its method here. If you let it return a boolean then you can get rid of that return; at line 67 too and it'll be a bit easier to follow.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

CachedResponse cachedResponse = null;
try {
cachedResponse = configuration.getResponseCache().get(request, invocationInput);
} catch (Throwable t) {
log.warn(t.getMessage(), t);
log.warn("Ignore read from cache, unexpected error happened");
}

if (cachedResponse != null) {
CacheResponseWriter cacheResponseWriter = new CacheResponseWriter();
cacheResponseWriter.write(request, response, cachedResponse);
return;
}
}

GraphQLQueryResult queryResult = invoke(invocationInput, request, response);

QueryResponseWriter queryResponseWriter = QueryResponseWriter.createWriter(queryResult, configuration.getObjectMapper(),
configuration.getSubscriptionTimeout());
queryResponseWriter.write(request, response);
QueryResponseWriter queryResponseWriter = QueryResponseWriter.createWriter(
Copy link
Member

Choose a reason for hiding this comment

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

I'd add the ResponseCache in this factory method instead of adding it to the method signature of the writer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

queryResult,
configuration.getObjectMapper(),
configuration.getSubscriptionTimeout(),
invocationInput
);
queryResponseWriter.write(request, response, configuration.getResponseCache());
} catch (Throwable t) {
response.setStatus(STATUS_BAD_REQUEST);
log.info("Bad GET request: path was not \"/schema.json\" or no query variable named \"query\" given");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import graphql.kickstart.execution.GraphQLQueryResult;
import graphql.kickstart.execution.GraphQLObjectMapper;
import graphql.kickstart.execution.input.GraphQLInvocationInput;
import graphql.kickstart.servlet.cache.GraphQLResponseCache;

import java.io.IOException;
import java.util.Objects;
import javax.servlet.http.HttpServletRequest;
Expand All @@ -12,20 +15,21 @@ interface QueryResponseWriter {
static QueryResponseWriter createWriter(
GraphQLQueryResult result,
GraphQLObjectMapper graphQLObjectMapper,
long subscriptionTimeout
long subscriptionTimeout,
GraphQLInvocationInput invocationInput
) {
Objects.requireNonNull(result, "GraphQL query result cannot be null");

if (result.isBatched()) {
return new BatchedQueryResponseWriter(result.getResults(), graphQLObjectMapper);
return new BatchedQueryResponseWriter(result.getResults(), graphQLObjectMapper, invocationInput);
} else if (result.isAsynchronous()) {
return new SingleAsynchronousQueryResponseWriter(result.getResult(), graphQLObjectMapper, subscriptionTimeout);
return new SingleAsynchronousQueryResponseWriter(result.getResult(), graphQLObjectMapper, subscriptionTimeout, invocationInput);
} else if (result.isError()) {
return new ErrorQueryResponseWriter(result.getStatusCode(), result.getMessage());
return new ErrorQueryResponseWriter(result.getStatusCode(), result.getMessage(), invocationInput);
}
return new SingleQueryResponseWriter(result.getResult(), graphQLObjectMapper);
return new SingleQueryResponseWriter(result.getResult(), graphQLObjectMapper, invocationInput);
}

void write(HttpServletRequest request, HttpServletResponse response) throws IOException;
void write(HttpServletRequest request, HttpServletResponse response, GraphQLResponseCache responseCache) throws IOException;

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,31 @@
import javax.servlet.AsyncContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import graphql.kickstart.execution.input.GraphQLInvocationInput;
import graphql.kickstart.servlet.cache.GraphQLResponseCache;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscription;

@Slf4j
@RequiredArgsConstructor
class SingleAsynchronousQueryResponseWriter implements QueryResponseWriter {

@Getter
private final ExecutionResult result;
private final GraphQLObjectMapper graphQLObjectMapper;
private final long subscriptionTimeout;
private final GraphQLInvocationInput invocationInput;

@Override
public void write(HttpServletRequest request, HttpServletResponse response) {
public void write(HttpServletRequest request, HttpServletResponse response, GraphQLResponseCache responseCache) {
if (responseCache != null) {
log.warn("Response cache for asynchronous query are not implemented yet");
}

Objects.requireNonNull(request, "Http servlet request cannot be null");
response.setContentType(HttpRequestHandler.APPLICATION_EVENT_STREAM_UTF8);
response.setStatus(HttpRequestHandler.STATUS_OK);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,42 @@

import graphql.ExecutionResult;
import graphql.kickstart.execution.GraphQLObjectMapper;
import graphql.kickstart.execution.input.GraphQLInvocationInput;
import graphql.kickstart.servlet.cache.CachedResponse;
import graphql.kickstart.servlet.cache.GraphQLResponseCache;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
@RequiredArgsConstructor
class SingleQueryResponseWriter implements QueryResponseWriter {

private final ExecutionResult result;
private final GraphQLObjectMapper graphQLObjectMapper;
private final GraphQLInvocationInput invocationInput;

@Override
public void write(HttpServletRequest request, HttpServletResponse response) throws IOException {
public void write(HttpServletRequest request, HttpServletResponse response, GraphQLResponseCache responseCache) throws IOException {
response.setContentType(HttpRequestHandler.APPLICATION_JSON_UTF8);
response.setStatus(HttpRequestHandler.STATUS_OK);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
String responseContent = graphQLObjectMapper.serializeResultAsJson(result);
byte[] contentBytes = responseContent.getBytes(StandardCharsets.UTF_8);

if (responseCache != null && responseCache.isCacheable(request, invocationInput)) {
try {
responseCache.put(request, invocationInput, CachedResponse.ofContent(contentBytes));
} catch (Throwable t) {
log.warn(t.getMessage(), t);
log.warn("Ignore read from cache, unexpected error happened");
}
}

response.setContentLength(contentBytes.length);
response.getOutputStream().write(contentBytes);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package graphql.kickstart.servlet.cache;

import graphql.kickstart.servlet.HttpRequestHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class CacheResponseWriter {

public void write(HttpServletRequest request, HttpServletResponse response, CachedResponse cachedResponse)
throws IOException {
if (cachedResponse.isError()) {
response.sendError(cachedResponse.getErrorStatusCode(), cachedResponse.getErrorMessage());
} else {
response.setContentType(HttpRequestHandler.APPLICATION_JSON_UTF8);
response.setStatus(HttpRequestHandler.STATUS_OK);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentLength(cachedResponse.getContentBytes().length);
response.getOutputStream().write(cachedResponse.getContentBytes());
}
}

}
Loading