Skip to content

Commit

Permalink
⚡ Improve Performance for GraphQL App Definition Caching
Browse files Browse the repository at this point in the history
  • Loading branch information
ujibang committed Aug 23, 2024
1 parent cc5078a commit 7ec9a35
Show file tree
Hide file tree
Showing 11 changed files with 234 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions core/src/main/resources/restheart-default-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion core/src/test/resources/etc/conf-overrides.yml
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,6 @@

/graphql/db: test-graphql
/graphql/verbose: true
/graphql/app-def-cache-ttl: 100

/proxies:
- location: /pecho
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<GraphQLRequest, GraphQLResponse> {
public static final String DEFAULT_APP_DEF_DB = "restheart";
public static final String DEFAULT_APP_DEF_COLLECTION = "gqlapps";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,57 +26,51 @@
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<LoadingCache> {
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<String, GraphQLApp> appLoadingCache;

private AppDefinitionLoadingCache(){
this.appLoadingCache = CacheFactory.createLocalLoadingCache(MAX_CACHE_SIZE,
Cache.EXPIRE_POLICY.AFTER_WRITE, TTL, key -> {
private static final LoadingCache<String, GraphQLApp> 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<String, GraphQLApp> 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);
}

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;
}
}
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
* =========================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<String, Object> 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);
}
});
}
}
}
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
* =========================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<String, Object> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Loading

0 comments on commit 7ec9a35

Please sign in to comment.