Skip to content

Commit

Permalink
Merge pull request #247 from donbeave/master
Browse files Browse the repository at this point in the history
Added ability to cache the whole graphql response
  • Loading branch information
oliemansm authored Nov 19, 2020
2 parents 88a7614 + 2902ee7 commit 074ed04
Show file tree
Hide file tree
Showing 13 changed files with 482 additions and 17 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ target/
.project
.settings
bin
.DS_Store
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import graphql.kickstart.execution.GraphQLQueryInvoker;
import graphql.kickstart.execution.GraphQLRequest;
import graphql.kickstart.execution.input.GraphQLSingleInvocationInput;
import graphql.kickstart.servlet.cache.CachingHttpRequestHandlerImpl;
import graphql.schema.GraphQLFieldDefinition;
import graphql.kickstart.servlet.core.GraphQLMBean;
import graphql.kickstart.servlet.core.GraphQLServletListener;
Expand Down Expand Up @@ -83,7 +84,11 @@ protected GraphQLConfiguration getConfiguration() {
public void init() {
if (configuration == null) {
this.configuration = getConfiguration();
this.requestHandler = HttpRequestHandlerFactory.create(configuration);
if (configuration.getResponseCacheManager() != null) {
this.requestHandler = new CachingHttpRequestHandlerImpl(configuration);
} else {
this.requestHandler = HttpRequestHandlerFactory.create(configuration);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@

import graphql.ExecutionResult;
import graphql.kickstart.execution.GraphQLObjectMapper;
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 {

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

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

response.setContentLength(contentBytes.length);
response.getOutputStream().write(contentBytes);
}
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.GraphQLResponseCacheManager;
import graphql.kickstart.servlet.config.DefaultGraphQLSchemaServletProvider;
import graphql.kickstart.servlet.config.GraphQLSchemaServletProvider;
import graphql.kickstart.servlet.context.GraphQLServletContextBuilder;
Expand Down Expand Up @@ -36,12 +37,13 @@ public class GraphQLConfiguration {
@Getter
private final long asyncTimeout;
private final ContextSetting contextSetting;
private final GraphQLResponseCacheManager responseCacheManager;

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

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

public GraphQLResponseCacheManager getResponseCacheManager() {
return responseCacheManager;
}

public static class Builder {

private GraphQLInvocationInputFactory.Builder invocationInputFactoryBuilder;
Expand All @@ -126,6 +133,7 @@ public static class Builder {
private long asyncTimeout = 30;
private ContextSetting contextSetting = ContextSetting.PER_QUERY_WITH_INSTRUMENTATION;
private Supplier<BatchInputPreProcessor> batchInputPreProcessorSupplier = NoOpBatchInputPreProcessor::new;
private GraphQLResponseCacheManager responseCacheManager;

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

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

public GraphQLConfiguration build() {
return new GraphQLConfiguration(
this.invocationInputFactory != null ? this.invocationInputFactory : invocationInputFactoryBuilder.build(),
Expand All @@ -220,7 +233,8 @@ public GraphQLConfiguration build() {
subscriptionTimeout,
asyncTimeout,
contextSetting,
batchInputPreProcessorSupplier
batchInputPreProcessorSupplier,
responseCacheManager
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
import lombok.extern.slf4j.Slf4j;

@Slf4j
class HttpRequestHandlerImpl implements HttpRequestHandler {
public class HttpRequestHandlerImpl implements HttpRequestHandler {

private final GraphQLConfiguration configuration;
private final GraphQLInvoker graphQLInvoker;

HttpRequestHandlerImpl(GraphQLConfiguration configuration) {
public HttpRequestHandlerImpl(GraphQLConfiguration configuration) {
this.configuration = configuration;
graphQLInvoker = configuration.getGraphQLInvoker();
}
Expand All @@ -46,15 +46,19 @@ public void handle(HttpServletRequest request, HttpServletResponse response) thr
}
}

private void execute(GraphQLInvocationInput invocationInput, HttpServletRequest request,
protected void execute(GraphQLInvocationInput invocationInput, HttpServletRequest request,
HttpServletResponse response) throws IOException {
GraphQLQueryResult queryResult = invoke(invocationInput, request, response);

QueryResponseWriter queryResponseWriter = QueryResponseWriter.createWriter(queryResult, configuration.getObjectMapper(),
configuration.getSubscriptionTimeout());
QueryResponseWriter queryResponseWriter = createWriter(invocationInput, queryResult);
queryResponseWriter.write(request, response);
}

protected QueryResponseWriter createWriter(GraphQLInvocationInput invocationInput, GraphQLQueryResult queryResult) {
return QueryResponseWriter.createWriter(queryResult, configuration.getObjectMapper(),
configuration.getSubscriptionTimeout());
}

private GraphQLQueryResult invoke(GraphQLInvocationInput invocationInput, HttpServletRequest request,
HttpServletResponse response) {
if (invocationInput instanceof GraphQLSingleInvocationInput) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package graphql.kickstart.servlet;

import graphql.kickstart.execution.GraphQLQueryResult;
import graphql.kickstart.execution.GraphQLObjectMapper;
import java.io.IOException;
import java.util.Objects;
import graphql.kickstart.execution.GraphQLQueryResult;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;

interface QueryResponseWriter {
public interface QueryResponseWriter {

static QueryResponseWriter createWriter(
GraphQLQueryResult result,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
import graphql.kickstart.execution.GraphQLObjectMapper;
import lombok.RequiredArgsConstructor;

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

@RequiredArgsConstructor
class SingleQueryResponseWriter implements QueryResponseWriter {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package graphql.kickstart.servlet.cache;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;

@Slf4j
public class BufferedHttpServletResponse extends HttpServletResponseWrapper {

private static final class BufferedOutputStream extends ServletOutputStream {

private final OutputStream delegate;
private final ByteArrayOutputStream buf = new ByteArrayOutputStream();

public BufferedOutputStream(OutputStream delegate) {
this.delegate = delegate;
}

public void write(int b) throws IOException {
buf.write(b);
delegate.write(b);
}

@Override
public void flush() throws IOException {
buf.flush();
delegate.flush();
}

@Override
public void close() throws IOException {
buf.close();
delegate.close();
}

@Override
public boolean isReady() {
return true;
}

@Override
public void setWriteListener(WriteListener writeListener) {
}

public byte[] toByteArray() {
return buf.toByteArray();
}

}

private BufferedOutputStream copier;

private ServletOutputStream outputStream;
private PrintWriter writer;
private String errorMessage;

public BufferedHttpServletResponse(HttpServletResponse response) {
super(response);
}

@Override
public void sendError(int sc, String msg) throws IOException {
errorMessage = msg;
super.sendError(sc, msg);
}

@Override
public void sendError(int sc) throws IOException {
sendError(sc, null);
}

@Override
public ServletOutputStream getOutputStream() throws IOException {
if (writer != null) {
throw new IllegalStateException("getWriter() has already been called on this response.");
}

if (outputStream == null) {
outputStream = getResponse().getOutputStream();
copier = new BufferedOutputStream(outputStream);
}

return copier;
}

@Override
public PrintWriter getWriter() throws IOException {
if (outputStream != null) {
throw new IllegalStateException("getOutputStream() has already been called on this response.");
}

if (writer == null) {
copier = new BufferedOutputStream(getResponse().getOutputStream());
writer = new PrintWriter(new OutputStreamWriter(copier, getResponse().getCharacterEncoding()), true);
}

return writer;
}

@Override
public void flushBuffer() throws IOException {
if (writer != null) {
writer.flush();
} else if (copier != null) {
copier.flush();
}
}

@Override
public boolean isCommitted() {
return false;
}

public void close() throws IOException {
if (writer != null) {
writer.close();
} else if (copier != null) {
copier.close();
}
}

public String getErrorMessage() {
return errorMessage;
}

public byte[] getContentAsByteArray() {
if (copier != null) {
return copier.toByteArray();
} else {
return new byte[0];
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package graphql.kickstart.servlet.cache;

import graphql.kickstart.execution.input.GraphQLInvocationInput;
import graphql.kickstart.servlet.HttpRequestHandler;
import lombok.extern.slf4j.Slf4j;

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

@Slf4j
public final class CacheReader {

private CacheReader() {
}

/**
* Response from cache if possible, if nothing in cache will not produce any response
*
* @return {@literal true} if response was fulfilled from cache, {@literal false} is cache not found or an error
* occurred while reading value from cache
* @throws IOException if can not read value from the cache
*/
public static boolean responseFromCache(GraphQLInvocationInput invocationInput,
HttpServletRequest request,
HttpServletResponse response,
GraphQLResponseCacheManager cacheManager) throws IOException {
CachedResponse cachedResponse = null;
try {
cachedResponse = cacheManager.get(request, invocationInput);
} catch (Throwable t) {
log.warn("Ignore read from cache, unexpected error happened", t);
}

if (cachedResponse != null) {
write(response, cachedResponse);
return true;
}

return false;
}

private static void write(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

0 comments on commit 074ed04

Please sign in to comment.