Skip to content

Commit

Permalink
✨ Add GraphAppsUpdater to periodically revalidate entries in GQL Apps…
Browse files Browse the repository at this point in the history
… cache

This synchronizes gql app caches in multi-node deployments #523
  • Loading branch information
ujibang committed Aug 23, 2024
1 parent d5a8e8d commit 1267a82
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 18 deletions.
8 changes: 5 additions & 3 deletions core/src/main/resources/restheart-default-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -308,22 +308,24 @@ mongo:
schema-cache-ttl: 60_000 # in msecs

# The time limit in milliseconds for processing queries. Set to 0 for no time limit.
query-time-limit: 0
query-time-limit: 0 # in msecs
# The time limit in milliseconds for processing aggregations. Set to 0 for no time limit.
aggregation-time-limit: 0
aggregation-time-limit: 0 # in msecs

# MongoDB GraphQL API
# see https://restheart.org/docs/mongodb-graphql/
graphql:
uri: /graphql
db: restheart
collection: gql-apps
# app cache entries are automatically revalidated every TTR milliseconds
app-cache-ttr: 60_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
max-limit: 1_000
# The time limit in milliseconds for processing queries. Set to 0 for no time limit.
query-time-limit: 0
query-time-limit: 0 # in msecs
verbose: false

# Proxied resources - expose exrernal API with RESTHeart acting as a reverese proxy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,17 @@
*/
package org.restheart.graphql.cache;

import com.mongodb.client.MongoClient;
import org.bson.BsonDocument;
import org.restheart.graphql.GraphQLIllegalAppDefinitionException;
import org.restheart.graphql.models.*;
import org.restheart.graphql.models.GraphQLApp;
import org.restheart.graphql.models.builder.AppBuilder;

import static org.restheart.utils.BsonUtils.array;
import static org.restheart.utils.BsonUtils.document;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.mongodb.client.MongoClient;

public class AppDefinitionLoader {
private static final Logger LOGGER = LoggerFactory.getLogger(AppDefinitionLoader.class);

Expand All @@ -50,7 +49,7 @@ public static void setup(String _db, String _collection, MongoClient mclient){
}

static GraphQLApp loadAppDefinition(String appURI) throws GraphQLIllegalAppDefinitionException {
LOGGER.debug("Loading GQL App Definition {} from db", appURI);
LOGGER.trace("Loading GQL App Definition {} from db", appURI);

var uriOrNameCond = array()
.add(document().put(APP_URI_FIELD, appURI))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,15 @@
public class AppDefinitionLoadingCache implements Provider<LoadingCache> {
private static final long MAX_CACHE_SIZE = 100_000;

private static final LoadingCache<String, GraphQLApp> CACHE = CacheFactory.createLocalLoadingCache(MAX_CACHE_SIZE,
Cache.EXPIRE_POLICY.NEVER, 0, appURI -> {
try {
return AppDefinitionLoader.loadAppDefinition(appURI);
} catch (GraphQLIllegalAppDefinitionException e) {
LambdaUtils.throwsSneakyException(e);
return null;
}
});
private static final LoadingCache<String, GraphQLApp> CACHE = CacheFactory.createLocalLoadingCache(MAX_CACHE_SIZE, Cache.EXPIRE_POLICY.NEVER, 0,
appURI -> {
try {
return AppDefinitionLoader.loadAppDefinition(appURI);
} catch (GraphQLIllegalAppDefinitionException e) {
LambdaUtils.throwsSneakyException(e);
return null;
}
});

public static LoadingCache<String, GraphQLApp> getCache() {
return CACHE;
Expand All @@ -70,7 +70,7 @@ public static GraphQLApp get(String appURI) throws GraphQLAppDefNotFoundExceptio
}

@Override
public LoadingCache get(PluginRecord<?> caller) {
public LoadingCache<String, GraphQLApp> get(PluginRecord<?> caller) {
return CACHE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*-
* ========================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 java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.restheart.cache.LoadingCache;
import org.restheart.configuration.Configuration;
import org.restheart.graphql.models.GraphQLApp;
import org.restheart.plugins.Initializer;
import org.restheart.plugins.Inject;
import org.restheart.plugins.OnInit;
import org.restheart.plugins.RegisterPlugin;
import org.restheart.utils.ThreadsUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@RegisterPlugin(name="graphAppsUpdater",
description = "periodically revalidates entries in GQL Apps cache",
enabledByDefault = true
)
public class GraphAppsUpdater implements Initializer {
private static final Logger LOGGER = LoggerFactory.getLogger(GraphAppsUpdater.class);
private static final long DEFAULT_TTR = 60_000; // in milliseconds
private long TTR = DEFAULT_TTR;

@Inject("rh-config")
private Configuration config;

@Inject("gql-app-definition-cache")
LoadingCache<String, GraphQLApp> gqlAppDefCache;

@OnInit
public void onInit() {
Map<String, Object> graphqlArgs = config.getOrDefault("graphql", null);

if (graphqlArgs != null) {
this.TTR = argOrDefault(graphqlArgs, "app-cache-ttr", 60_000);
} else {
this.TTR = DEFAULT_TTR;
}
}

@Override
public void init() {
Executors.newSingleThreadScheduledExecutor()
.scheduleAtFixedRate(() -> ThreadsUtils.virtualThreadsExecutor()
.execute(() -> this.revalidateCacheEntries()), TTR, TTR, TimeUnit.MILLISECONDS);
}

private void revalidateCacheEntries() {
this.gqlAppDefCache.asMap().entrySet().stream()
.filter(entry -> entry.getValue().isPresent())
.map(entry -> entry.getKey())
.forEach(appUri -> {
try {
this.gqlAppDefCache.invalidate(appUri);
var app$ = this.gqlAppDefCache.getLoading(appUri);
if (app$.isPresent()) {
LOGGER.debug("gql cache entry {} updated", appUri);
} else {
this.gqlAppDefCache.invalidate(appUri);
LOGGER.debug("gql cache entry {} removed", appUri);
}

} catch (Exception e) {
LOGGER.warn("error updaring gql cache entry {}", appUri, e);
}
});
}
}

0 comments on commit 1267a82

Please sign in to comment.