From 78c570790aa4010613756e9d66f632dd500de091 Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Sun, 26 Aug 2018 09:31:03 +1200 Subject: [PATCH] Add query cache This commit adds a query cache with a configurable maximum size. Past this size, queries are evicted from the cache on an LRU basis. The default cache size is 1000, chosen fairly arbitrarily. If the size is configured with a non-positive value, then the cache is disabled. Also ran `dep ensure` to add the new dependency to `Gopkg.lock`. --- Gopkg.lock | 26 +++++++++++++++++++------- handler/graphql.go | 41 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index cc59153f348..a10062414a5 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -26,7 +26,7 @@ version = "v3.3.2" [[projects]] - digest = "1:78907d832e27dbfc6e3fdfc52bd2e5e2e05c1d0e3789d4825b824489fbeab233" + digest = "1:f3df613325a793ffb3d0ce7644a3bb6f62db45ac744dafe20172fe999c61cdbf" name = "github.com/gogo/protobuf" packages = [ "io", @@ -60,6 +60,17 @@ revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b" version = "v1.2.0" +[[projects]] + branch = "master" + digest = "1:cf296baa185baae04a9a7004efee8511d08e2f5f51d4cbe5375da89722d681db" + name = "github.com/hashicorp/golang-lru" + packages = [ + ".", + "simplelru", + ] + pruneopts = "UT" + revision = "0fb14efe8c47ae851c0034ed7a448854d3d34cf3" + [[projects]] digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be" name = "github.com/inconshreveable/mousetrap" @@ -96,7 +107,7 @@ version = "v1.0.0" [[projects]] - digest = "1:27af6024faa3c28426a698b8c653be0fd908bc96e25b7d76f2192eb342427db6" + digest = "1:450b7623b185031f3a456801155c8320209f75d0d4c4e633c6b1e59d44d6e392" name = "github.com/opentracing/opentracing-go" packages = [ ".", @@ -140,7 +151,7 @@ revision = "ffb13db8def02f545acc58bd288ec6057c2bbfb9" [[projects]] - digest = "1:872fa275c31e1f9db31d66fa9b1d4a7bb9a080ff184e6977da01f36bfbe07f11" + digest = "1:645cabccbb4fa8aab25a956cbcbdf6a6845ca736b2c64e197ca7cbb9d210b939" name = "github.com/spf13/cobra" packages = ["."] pruneopts = "UT" @@ -156,7 +167,7 @@ version = "v1.0.1" [[projects]] - digest = "1:73697231b93fb74a73ebd8384b68b9a60c57ea6b13c56d2425414566a72c8e6d" + digest = "1:7e8d267900c7fa7f35129a2a37596e38ed0f11ca746d6d9ba727980ee138f9f6" name = "github.com/stretchr/testify" packages = [ "assert", @@ -200,7 +211,7 @@ [[projects]] branch = "master" - digest = "1:77fe642412bfed48743e2b75163e3ab5c430cfe22dd488788647b89b28794635" + digest = "1:3cbc05413b8aac22b1f6d4350ed696b5a83a8515a4136db8f1ec3a0aee3d76e1" name = "golang.org/x/tools" packages = [ "go/ast/astutil", @@ -221,7 +232,7 @@ [[projects]] branch = "master" - digest = "1:7ddb3a7b35cc853fe0db36a1b2473bdff03f28add7d28e4725e692603111266e" + digest = "1:741ebea9214cc226789d3003baeca9b169e04b5b336fb1a3b2c16e75bd296bb5" name = "sourcegraph.com/sourcegraph/appdash" packages = [ ".", @@ -237,7 +248,7 @@ [[projects]] branch = "master" - digest = "1:be108b48d79c3b3c345811a57a47ee87fdbe895beb4bb56239da71d4943e5be7" + digest = "1:8e0a2957fe342f22d70a543c3fcdf390f7627419c3d82d87ab4fd715a9ef5716" name = "sourcegraph.com/sourcegraph/appdash-data" packages = ["."] pruneopts = "UT" @@ -249,6 +260,7 @@ input-imports = [ "github.com/go-chi/chi", "github.com/gorilla/websocket", + "github.com/hashicorp/golang-lru", "github.com/mitchellh/mapstructure", "github.com/opentracing-contrib/go-stdlib/nethttp", "github.com/opentracing/opentracing-go", diff --git a/handler/graphql.go b/handler/graphql.go index 0485af865b8..97ee744494c 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -10,6 +10,7 @@ import ( "github.com/99designs/gqlgen/graphql" "github.com/gorilla/websocket" + "github.com/hashicorp/golang-lru" "github.com/vektah/gqlparser" "github.com/vektah/gqlparser/ast" "github.com/vektah/gqlparser/gqlerror" @@ -23,6 +24,7 @@ type params struct { } type Config struct { + cacheSize int upgrader websocket.Upgrader recover graphql.RecoverFunc errorPresenter graphql.ErrorPresenterFunc @@ -110,8 +112,19 @@ func RequestMiddleware(middleware graphql.RequestMiddleware) Option { } } +// CacheSize sets the maximum size of the query cache. +// If size is less than or equal to 0, the cache is disabled. +func CacheSize(size int) Option { + return func(cfg *Config) { + cfg.cacheSize = size + } +} + +const DefaultCacheSize = 1000 + func GraphQL(exec graphql.ExecutableSchema, options ...Option) http.HandlerFunc { cfg := Config{ + cacheSize: DefaultCacheSize, upgrader: websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, @@ -122,6 +135,13 @@ func GraphQL(exec graphql.ExecutableSchema, options ...Option) http.HandlerFunc option(&cfg) } + var cache *lru.Cache + if cfg.cacheSize > 0 { + // An error is only returned for non-positive cache size + // and we already checked for that. + cache, _ = lru.New(DefaultCacheSize) + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodOptions { w.Header().Set("Allow", "OPTIONS, GET, POST") @@ -157,10 +177,23 @@ func GraphQL(exec graphql.ExecutableSchema, options ...Option) http.HandlerFunc } w.Header().Set("Content-Type", "application/json") - doc, qErr := gqlparser.LoadQuery(exec.Schema(), reqParams.Query) - if len(qErr) > 0 { - sendError(w, http.StatusUnprocessableEntity, qErr...) - return + var doc *ast.QueryDocument + if cache != nil { + val, ok := cache.Get(reqParams.Query) + if ok { + doc = val.(*ast.QueryDocument) + } + } + if doc == nil { + var qErr gqlerror.List + doc, qErr = gqlparser.LoadQuery(exec.Schema(), reqParams.Query) + if len(qErr) > 0 { + sendError(w, http.StatusUnprocessableEntity, qErr...) + return + } + if cache != nil { + cache.Add(reqParams.Query, doc) + } } op := doc.Operations.ForName(reqParams.OperationName)