From 7ec9a3586d8d332a5631e28a838e5095f4f1493d Mon Sep 17 00:00:00 2001 From: Andrea Di Cesare Date: Wed, 21 Aug 2024 12:06:21 +0200 Subject: [PATCH] :zap: Improve Performance for GraphQL App Definition Caching https://github.com/SoftInstigate/restheart/issues/523 --- .../restheart-default-config-no-mongodb.yml | 2 - .../resources/restheart-default-config.yml | 2 - .../src/test/resources/etc/conf-overrides.yml | 1 - .../org/restheart/graphql/GraphQLService.java | 5 +- .../graphql/cache/AppDefinitionLoader.java | 8 +- .../cache/AppDefinitionLoadingCache.java | 46 ++++----- .../initializers/GraphAppsInitializer.java | 97 +++++++++++++++++++ .../GraphAppDefinitionCacheInvalidator.java | 89 +++++++++++++++++ .../GraphAppDefinitionPatchChecker.java | 11 ++- .../GraphAppDefinitionPutPostChecker.java | 11 ++- .../graphql/models/AppDescriptor.java | 2 +- 11 files changed, 234 insertions(+), 40 deletions(-) create mode 100644 graphql/src/main/java/org/restheart/graphql/initializers/GraphAppsInitializer.java create mode 100644 graphql/src/main/java/org/restheart/graphql/interceptors/GraphAppDefinitionCacheInvalidator.java diff --git a/core/src/main/resources/restheart-default-config-no-mongodb.yml b/core/src/main/resources/restheart-default-config-no-mongodb.yml index 13d210be17..86750e6436 100644 --- a/core/src/main/resources/restheart-default-config-no-mongodb.yml +++ b/core/src/main/resources/restheart-default-config-no-mongodb.yml @@ -331,8 +331,6 @@ graphql: uri: /graphql db: restheart collection: gql-apps - # app definitions are cached. this sets the time to live in msecs - app-def-cache-ttl: 10_000 # default-limit is used for queries that don't not specify a limit default-limit: 100 # max-limit is the maximum value for a Query limit diff --git a/core/src/main/resources/restheart-default-config.yml b/core/src/main/resources/restheart-default-config.yml index 541ac082e8..c478d91d01 100644 --- a/core/src/main/resources/restheart-default-config.yml +++ b/core/src/main/resources/restheart-default-config.yml @@ -318,8 +318,6 @@ graphql: uri: /graphql db: restheart collection: gql-apps - # app definitions are cached. this sets the time to live in msecs - app-def-cache-ttl: 10_000 # in msecs # default-limit is used for queries that don't not specify a limit default-limit: 100 # max-limit is the maximum value for a Query limit diff --git a/core/src/test/resources/etc/conf-overrides.yml b/core/src/test/resources/etc/conf-overrides.yml index 08cb5fa5e4..da2c6f3c73 100644 --- a/core/src/test/resources/etc/conf-overrides.yml +++ b/core/src/test/resources/etc/conf-overrides.yml @@ -273,7 +273,6 @@ /graphql/db: test-graphql /graphql/verbose: true -/graphql/app-def-cache-ttl: 100 /proxies: - location: /pecho diff --git a/graphql/src/main/java/org/restheart/graphql/GraphQLService.java b/graphql/src/main/java/org/restheart/graphql/GraphQLService.java index 69c8d4d919..6568897794 100644 --- a/graphql/src/main/java/org/restheart/graphql/GraphQLService.java +++ b/graphql/src/main/java/org/restheart/graphql/GraphQLService.java @@ -87,7 +87,6 @@ import io.undertow.server.HttpServerExchange; @RegisterPlugin(name = "graphql", description = "Service that handles GraphQL requests", secure = true, enabledByDefault = true, defaultURI = "/graphql") - public class GraphQLService implements Service { public static final String DEFAULT_APP_DEF_DB = "restheart"; public static final String DEFAULT_APP_DEF_COLLECTION = "gqlapps"; @@ -126,8 +125,6 @@ public void init()throws ConfigurationException, NoSuchFieldException, IllegalAc this.queryTimeLimit = ((Number)argOrDefault(config, "query-time-limit", DEFAULT_QUERY_TIME_LIMIT)).longValue(); - AppDefinitionLoadingCache.setTTL(argOrDefault(config, "app-def-cache-ttl", 1_000)); - QueryBatchLoader.setMongoClient(mclient); AggregationBatchLoader.setMongoClient(mclient); GraphQLDataFetcher.setMongoClient(mclient); @@ -463,7 +460,7 @@ private String appURI(HttpServerExchange exchange) { } private GraphQLApp gqlApp(String appURI) throws GraphQLAppDefNotFoundException, GraphQLIllegalAppDefinitionException { - return AppDefinitionLoadingCache.getInstance().get(appURI); + return AppDefinitionLoadingCache.get(appURI); } @Override diff --git a/graphql/src/main/java/org/restheart/graphql/cache/AppDefinitionLoader.java b/graphql/src/main/java/org/restheart/graphql/cache/AppDefinitionLoader.java index 9e690fcf4e..7721cc64f1 100644 --- a/graphql/src/main/java/org/restheart/graphql/cache/AppDefinitionLoader.java +++ b/graphql/src/main/java/org/restheart/graphql/cache/AppDefinitionLoader.java @@ -29,7 +29,11 @@ import static org.restheart.utils.BsonUtils.array; import static org.restheart.utils.BsonUtils.document; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class AppDefinitionLoader { + private static final Logger LOGGER = LoggerFactory.getLogger(AppDefinitionLoader.class); private static final String APP_URI_FIELD = "descriptor.uri"; private static final String APP_NAME_FIELD = "descriptor.name"; @@ -45,7 +49,9 @@ public static void setup(String _db, String _collection, MongoClient mclient){ mongoClient = mclient; } - public static GraphQLApp loadAppDefinition(String appURI) throws GraphQLIllegalAppDefinitionException { + static GraphQLApp loadAppDefinition(String appURI) throws GraphQLIllegalAppDefinitionException { + LOGGER.debug("Loading GQL App Definition {} from db", appURI); + var uriOrNameCond = array() .add(document().put(APP_URI_FIELD, appURI)) .add(document().put(APP_NAME_FIELD, appURI)); diff --git a/graphql/src/main/java/org/restheart/graphql/cache/AppDefinitionLoadingCache.java b/graphql/src/main/java/org/restheart/graphql/cache/AppDefinitionLoadingCache.java index 0115e42b6a..85dc84a6f3 100644 --- a/graphql/src/main/java/org/restheart/graphql/cache/AppDefinitionLoadingCache.java +++ b/graphql/src/main/java/org/restheart/graphql/cache/AppDefinitionLoadingCache.java @@ -26,48 +26,37 @@ import org.restheart.graphql.GraphQLAppDefNotFoundException; import org.restheart.graphql.GraphQLIllegalAppDefinitionException; import org.restheart.graphql.models.GraphQLApp; +import org.restheart.plugins.PluginRecord; +import org.restheart.plugins.Provider; +import org.restheart.plugins.RegisterPlugin; import org.restheart.utils.LambdaUtils; -public class AppDefinitionLoadingCache { +@RegisterPlugin(name="gql-app-definition-cache", description="provides access to the GQL App Definition cache") +public class AppDefinitionLoadingCache implements Provider { + private static final long MAX_CACHE_SIZE = 100_000; - private static AppDefinitionLoadingCache instance = null; - private static long TTL = 100_000; - private static final long MAX_CACHE_SIZE = 1_000; - - private final LoadingCache appLoadingCache; - - private AppDefinitionLoadingCache(){ - this.appLoadingCache = CacheFactory.createLocalLoadingCache(MAX_CACHE_SIZE, - Cache.EXPIRE_POLICY.AFTER_WRITE, TTL, key -> { + private static final LoadingCache CACHE = CacheFactory.createLocalLoadingCache(MAX_CACHE_SIZE, + Cache.EXPIRE_POLICY.NEVER, 0, appURI -> { try { - return AppDefinitionLoader.loadAppDefinition(key); + return AppDefinitionLoader.loadAppDefinition(appURI); } catch (GraphQLIllegalAppDefinitionException e) { LambdaUtils.throwsSneakyException(e); return null; } }); - } - public static void setTTL(long ttl) { - TTL = ttl; + public static LoadingCache getCache() { + return CACHE; } - public static AppDefinitionLoadingCache getInstance(){ - if (instance == null){ - instance = new AppDefinitionLoadingCache(); - } - - return instance; - } - - public GraphQLApp get(String appName) throws GraphQLAppDefNotFoundException, GraphQLIllegalAppDefinitionException { - var _app = this.appLoadingCache.get(appName); + public static GraphQLApp get(String appURI) throws GraphQLAppDefNotFoundException, GraphQLIllegalAppDefinitionException { + var _app = CACHE.get(appURI); if (_app != null && _app.isPresent()){ return _app.get(); } else { try { - _app = this.appLoadingCache.getLoading(appName); + _app = CACHE.getLoading(appURI); } catch (Exception e) { throw new GraphQLIllegalAppDefinitionException(e.getMessage(), e); } @@ -75,8 +64,13 @@ public GraphQLApp get(String appName) throws GraphQLAppDefNotFoundException, Gra if (_app != null && _app.isPresent()) { return _app.get(); } else { - throw new GraphQLAppDefNotFoundException("Valid configuration for " + appName + " not found. "); + throw new GraphQLAppDefNotFoundException("Valid configuration for " + appURI + " not found. "); } } } + + @Override + public LoadingCache get(PluginRecord caller) { + return CACHE; + } } \ No newline at end of file diff --git a/graphql/src/main/java/org/restheart/graphql/initializers/GraphAppsInitializer.java b/graphql/src/main/java/org/restheart/graphql/initializers/GraphAppsInitializer.java new file mode 100644 index 0000000000..e8fcdccbb1 --- /dev/null +++ b/graphql/src/main/java/org/restheart/graphql/initializers/GraphAppsInitializer.java @@ -0,0 +1,97 @@ +/*- + * ========================LICENSE_START================================= + * restheart-security + * %% + * Copyright (C) 2018 - 2024 SoftInstigate + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * =========================LICENSE_END================================== + */ +package org.restheart.graphql.initializers; + +import org.restheart.configuration.Configuration; +import org.restheart.configuration.ConfigurationException; +import org.restheart.graphql.GraphQLService; +import org.restheart.plugins.Inject; +import org.restheart.plugins.OnInit; +import org.restheart.plugins.RegisterPlugin; + +import java.util.Map; + +import org.bson.BsonDocument; +import org.restheart.graphql.GraphQLIllegalAppDefinitionException; +import org.restheart.graphql.cache.AppDefinitionLoadingCache; +import org.restheart.graphql.models.builder.AppBuilder; +import org.restheart.plugins.Initializer; + +import com.mongodb.client.MongoClient; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@RegisterPlugin(name="graphAppsInitializer", + description = "initializes and caches all GQL Apps at boot timeGraphQL", + enabledByDefault = true +) +public class GraphAppsInitializer implements Initializer { + private static final Logger LOGGER = LoggerFactory.getLogger(GraphAppsInitializer.class); + + private String db = GraphQLService.DEFAULT_APP_DEF_DB; + private String coll = GraphQLService.DEFAULT_APP_DEF_COLLECTION; + + private boolean enabled = false; + + @Inject("rh-config") + private Configuration config; + + @Inject("mclient") + private MongoClient mclient; + + @OnInit + public void onInit() { + try { + Map graphqlArgs = config.getOrDefault("graphql", null); + if (graphqlArgs != null) { + this.db = arg(graphqlArgs, "db"); + this.coll = arg(graphqlArgs, "collection"); + this.enabled = true; + } else { + this.enabled = false; + } + } catch(ConfigurationException ce) { + // nothing to do, using default values + } + } + + @Override + public void init() { + if (this.enabled) { + this.mclient + .getDatabase(this.db) + .getCollection(this.coll) + .withDocumentClass(BsonDocument.class) + .find() + .forEach(appDef -> { + try { + var app = AppBuilder.build(appDef); + var appUri = app.getDescriptor().getUri() != null ? app.getDescriptor().getUri() : app.getDescriptor().getAppName(); + AppDefinitionLoadingCache.getCache().put(appUri, app); + LOGGER.debug("GQL App Definition {} initialized", appUri); + } catch (GraphQLIllegalAppDefinitionException e) { + LOGGER.warn("GQL App Definition {} is invalid", appDef.get("_id"), e); + } + }); + } + } +} \ No newline at end of file diff --git a/graphql/src/main/java/org/restheart/graphql/interceptors/GraphAppDefinitionCacheInvalidator.java b/graphql/src/main/java/org/restheart/graphql/interceptors/GraphAppDefinitionCacheInvalidator.java new file mode 100644 index 0000000000..b4a211fa13 --- /dev/null +++ b/graphql/src/main/java/org/restheart/graphql/interceptors/GraphAppDefinitionCacheInvalidator.java @@ -0,0 +1,89 @@ +/*- + * ========================LICENSE_START================================= + * restheart-security + * %% + * Copyright (C) 2018 - 2024 SoftInstigate + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * =========================LICENSE_END================================== + */ +package org.restheart.graphql.interceptors; + +import org.restheart.configuration.Configuration; +import org.restheart.configuration.ConfigurationException; +import org.restheart.exchange.MongoRequest; +import org.restheart.exchange.MongoResponse; +import org.restheart.graphql.GraphQLService; +import org.restheart.plugins.Inject; +import org.restheart.plugins.MongoInterceptor; +import org.restheart.plugins.OnInit; +import org.restheart.plugins.RegisterPlugin; + +import java.util.Map; + +import org.restheart.graphql.cache.AppDefinitionLoadingCache; + +import static org.restheart.plugins.InterceptPoint.RESPONSE; +import org.restheart.utils.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@RegisterPlugin(name="graphAppDefinitionCacheInvalidator", + description = "invalidate GraphQL cached application definitions on gqlapps delete requests", + interceptPoint = RESPONSE, + enabledByDefault = true +) +public class GraphAppDefinitionCacheInvalidator implements MongoInterceptor { + private static final Logger LOGGER = LoggerFactory.getLogger(GraphAppDefinitionCacheInvalidator.class); + + private String db = GraphQLService.DEFAULT_APP_DEF_DB; + private String coll = GraphQLService.DEFAULT_APP_DEF_COLLECTION; + + private boolean enabled = false; + + @Inject("rh-config") + private Configuration config; + + @OnInit + public void init() { + try { + Map graphqlArgs = config.getOrDefault("graphql", null); + if (graphqlArgs != null) { + this.db = arg(graphqlArgs, "db"); + this.coll = arg(graphqlArgs, "collection"); + this.enabled = true; + } else { + this.enabled = false; + } + } catch(ConfigurationException ce) { + // nothing to do, using default values + } + } + + @Override + public void handle(MongoRequest request, MongoResponse response) throws Exception { + LOGGER.debug("invalidating all gql app definitions cache entries"); + AppDefinitionLoadingCache.getCache().invalidateAll(); + } + + @Override + public boolean resolve(MongoRequest request, MongoResponse response) { + return enabled + && this.db.equals(request.getDBName()) + && this.coll.equals(request.getCollectionName()) + && request.isDelete() + && !response.isInError() + && response.getStatusCode() == HttpStatus.SC_NO_CONTENT; + } +} \ No newline at end of file diff --git a/graphql/src/main/java/org/restheart/graphql/interceptors/GraphAppDefinitionPatchChecker.java b/graphql/src/main/java/org/restheart/graphql/interceptors/GraphAppDefinitionPatchChecker.java index 8d39e4e5d2..98561c4585 100644 --- a/graphql/src/main/java/org/restheart/graphql/interceptors/GraphAppDefinitionPatchChecker.java +++ b/graphql/src/main/java/org/restheart/graphql/interceptors/GraphAppDefinitionPatchChecker.java @@ -32,8 +32,15 @@ import org.restheart.plugins.OnInit; import org.restheart.plugins.RegisterPlugin; import org.restheart.utils.HttpStatus; + import java.util.Map; + +import org.bson.BsonDocument; +import org.restheart.graphql.cache.AppDefinitionLoader; +import org.restheart.graphql.cache.AppDefinitionLoadingCache; + import com.mongodb.client.MongoClient; + import static org.restheart.plugins.InterceptPoint.RESPONSE; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -83,7 +90,9 @@ public void handle(MongoRequest request, MongoResponse response) throws Exceptio var appDef = response.getDbOperationResult().getNewData(); try { - AppBuilder.build(appDef); + var app = AppBuilder.build(appDef); + var appUri = app.getDescriptor().getUri() != null ? app.getDescriptor().getUri() : app.getDescriptor().getAppName(); + AppDefinitionLoadingCache.getCache().put(appUri, app); } catch(GraphQLIllegalAppDefinitionException e) { LOGGER.debug("Wrong GraphQL App definition", e); response.rollback(this.mclient); diff --git a/graphql/src/main/java/org/restheart/graphql/interceptors/GraphAppDefinitionPutPostChecker.java b/graphql/src/main/java/org/restheart/graphql/interceptors/GraphAppDefinitionPutPostChecker.java index de17221163..80817a544c 100644 --- a/graphql/src/main/java/org/restheart/graphql/interceptors/GraphAppDefinitionPutPostChecker.java +++ b/graphql/src/main/java/org/restheart/graphql/interceptors/GraphAppDefinitionPutPostChecker.java @@ -33,7 +33,10 @@ import org.restheart.plugins.RegisterPlugin; import org.restheart.utils.BsonUtils; import org.restheart.utils.HttpStatus; + import java.util.Map; + +import org.restheart.graphql.cache.AppDefinitionLoadingCache; import static org.restheart.plugins.InterceptPoint.REQUEST_AFTER_AUTH; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -79,7 +82,9 @@ public void handle(MongoRequest request, MongoResponse response) throws Exceptio var appDef = content.asDocument(); try { - AppBuilder.build(BsonUtils.unflatten(appDef).asDocument()); + var app = AppBuilder.build(BsonUtils.unflatten(appDef).asDocument()); + var appUri = app.getDescriptor().getUri() != null ? app.getDescriptor().getUri() : app.getDescriptor().getAppName(); + AppDefinitionLoadingCache.getCache().put(appUri, app); } catch(GraphQLIllegalAppDefinitionException e) { LOGGER.debug("Wrong GraphQL App definition", e); response.setInError(HttpStatus.SC_BAD_REQUEST, "Wrong GraphQL App definition: " + e.getMessage(), e); @@ -89,7 +94,9 @@ public void handle(MongoRequest request, MongoResponse response) throws Exceptio for (var appDef: content.asArray()) { try { - AppBuilder.build(BsonUtils.unflatten(appDef).asDocument()); + var app = AppBuilder.build(BsonUtils.unflatten(appDef).asDocument()); + var appUri = app.getDescriptor().getUri() != null ? app.getDescriptor().getUri() : app.getDescriptor().getAppName(); + AppDefinitionLoadingCache.getCache().put(appUri, app); } catch(GraphQLIllegalAppDefinitionException e) { LOGGER.debug("Wrong GraphQL App definition", e); response.setInError(HttpStatus.SC_BAD_REQUEST, "Wrong GraphQL App definition in document at index positon " + index + ": " + e.getMessage(), e); diff --git a/graphql/src/main/java/org/restheart/graphql/models/AppDescriptor.java b/graphql/src/main/java/org/restheart/graphql/models/AppDescriptor.java index 163569a52c..101c94d62a 100644 --- a/graphql/src/main/java/org/restheart/graphql/models/AppDescriptor.java +++ b/graphql/src/main/java/org/restheart/graphql/models/AppDescriptor.java @@ -61,7 +61,7 @@ public void setDescription(String description) { this.description = description; } - public String getUrl() { + public String getUri() { return uri; }