From 9d975eb9454a309694961c0c6045921c014c1c6c Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Thu, 25 Sep 2025 00:24:21 +0800 Subject: [PATCH 1/2] feat(graphql): implement GraphQL APQ --- cmd/backend/dependencies.go | 25 ++++++++++++++++++++++--- cmd/backend/server.go | 1 + internal/graphql/apq/apq.go | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 internal/graphql/apq/apq.go diff --git a/cmd/backend/dependencies.go b/cmd/backend/dependencies.go index 15f4a28..e133230 100644 --- a/cmd/backend/dependencies.go +++ b/cmd/backend/dependencies.go @@ -9,6 +9,7 @@ import ( "time" "entgo.io/contrib/entgql" + "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/handler/extension" "github.com/99designs/gqlgen/graphql/handler/lru" @@ -21,6 +22,7 @@ import ( "github.com/database-playground/backend-v2/internal/auth" "github.com/database-playground/backend-v2/internal/config" "github.com/database-playground/backend-v2/internal/events" + "github.com/database-playground/backend-v2/internal/graphql/apq" "github.com/database-playground/backend-v2/internal/httputils" "github.com/database-playground/backend-v2/internal/sqlrunner" "github.com/database-playground/backend-v2/internal/submission" @@ -44,8 +46,20 @@ func SqlRunner(cfg config.Config) *sqlrunner.SqlRunner { return sqlrunner.NewSqlRunner(cfg.SqlRunner) } +func ApqCache(redisClient rueidis.Client) graphql.Cache[string] { + return apq.NewCache(redisClient, 24*time.Hour) +} + // GqlgenHandler creates a gqlgen handler. -func GqlgenHandler(entClient *ent.Client, storage auth.Storage, sqlrunner *sqlrunner.SqlRunner, useraccount *useraccount.Context, eventService *events.EventService, submissionService *submission.SubmissionService) *handler.Server { +func GqlgenHandler( + entClient *ent.Client, + storage auth.Storage, + sqlrunner *sqlrunner.SqlRunner, + useraccount *useraccount.Context, + eventService *events.EventService, + submissionService *submission.SubmissionService, + apqCache graphql.Cache[string], +) *handler.Server { srv := handler.New(graph.NewSchema(entClient, storage, sqlrunner, useraccount, eventService, submissionService)) srv.AddTransport(transport.Options{}) @@ -57,7 +71,7 @@ func GqlgenHandler(entClient *ent.Client, storage auth.Storage, sqlrunner *sqlru srv.Use(entgql.Transactioner{TxOpener: entClient}) srv.Use(extension.Introspection{}) srv.Use(extension.AutomaticPersistedQuery{ - Cache: lru.New[string](100), + Cache: apqCache, }) srv.SetErrorPresenter(graph.NewErrorPresenter()) @@ -86,7 +100,12 @@ func AuthService(entClient *ent.Client, storage auth.Storage, config config.Conf } // GinEngine creates a gin engine. -func GinEngine(services []httpapi.Service, authStorage auth.Storage, gqlgenHandler *handler.Server, cfg config.Config) *gin.Engine { +func GinEngine( + services []httpapi.Service, + authStorage auth.Storage, + gqlgenHandler *handler.Server, + cfg config.Config, +) *gin.Engine { engine := gin.New() if err := engine.SetTrustedProxies(cfg.TrustProxies); err != nil { diff --git a/cmd/backend/server.go b/cmd/backend/server.go index 0273912..7694b3d 100644 --- a/cmd/backend/server.go +++ b/cmd/backend/server.go @@ -19,6 +19,7 @@ func main() { UserAccountContext, EventService, SubmissionService, + ApqCache, AnnotateService(AuthService), GqlgenHandler, fx.Annotate( diff --git a/internal/graphql/apq/apq.go b/internal/graphql/apq/apq.go new file mode 100644 index 0000000..a3d1c21 --- /dev/null +++ b/internal/graphql/apq/apq.go @@ -0,0 +1,37 @@ +// Package apq implements Apollo Client's Automatic Persisted Queries. +// https://gqlgen.com/reference/apq/ +package apq + +import ( + "context" + "time" + + "github.com/99designs/gqlgen/graphql" + "github.com/redis/rueidis" +) + +type Cache struct { + client rueidis.Client + ttl time.Duration +} + +const redisApqPrefix = "apq:" + +func NewCache(client rueidis.Client, ttl time.Duration) *Cache { + return &Cache{client: client, ttl: ttl} +} + +func (c *Cache) Get(ctx context.Context, query string) (string, bool) { + reply, err := c.client.Do(ctx, c.client.B().Get().Key(redisApqPrefix+query).Build()).ToString() + if err != nil { + return "", false + } + + return reply, true +} + +func (c *Cache) Add(ctx context.Context, query string, value string) { + c.client.Do(ctx, c.client.B().Set().Key(redisApqPrefix+query).Value(value).Ex(c.ttl).Build()) +} + +var _ graphql.Cache[string] = &Cache{} From 391a72b49791afba02a2ab65cb54dd8657b9cfef Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Thu, 25 Sep 2025 00:25:38 +0800 Subject: [PATCH 2/2] feat(graphql): log the APQ errors --- internal/graphql/apq/apq.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/internal/graphql/apq/apq.go b/internal/graphql/apq/apq.go index a3d1c21..8b453c5 100644 --- a/internal/graphql/apq/apq.go +++ b/internal/graphql/apq/apq.go @@ -4,6 +4,7 @@ package apq import ( "context" + "log/slog" "time" "github.com/99designs/gqlgen/graphql" @@ -24,6 +25,11 @@ func NewCache(client rueidis.Client, ttl time.Duration) *Cache { func (c *Cache) Get(ctx context.Context, query string) (string, bool) { reply, err := c.client.Do(ctx, c.client.B().Get().Key(redisApqPrefix+query).Build()).ToString() if err != nil { + if rueidis.IsRedisNil(err) { + return "", false + } + + slog.Warn("error getting apq from cache", "error", err, "query", query) return "", false } @@ -31,7 +37,10 @@ func (c *Cache) Get(ctx context.Context, query string) (string, bool) { } func (c *Cache) Add(ctx context.Context, query string, value string) { - c.client.Do(ctx, c.client.B().Set().Key(redisApqPrefix+query).Value(value).Ex(c.ttl).Build()) + err := c.client.Do(ctx, c.client.B().Set().Key(redisApqPrefix+query).Value(value).Ex(c.ttl).Build()).Error() + if err != nil { + slog.Warn("error adding apq to cache", "error", err, "query", query) + } } var _ graphql.Cache[string] = &Cache{}