Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: enhance cache management in plugin setting config #6141

Merged
merged 3 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package run.halo.app.plugin;

import com.fasterxml.jackson.databind.JsonNode;
import java.util.Map;
import lombok.Builder;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;
import run.halo.app.core.extension.Plugin;
import run.halo.app.extension.ConfigMap;

/**
* <p>Event that is triggered when the {@link ConfigMap } represented by
* {@link Plugin.PluginSpec#getConfigMapName()} in the {@link Plugin} is updated.</p>
* <p>has two properties, oldConfig and newConfig, which represent the {@link ConfigMap#getData()}
* property value of the {@link ConfigMap}.</p>
*
* @author guqing
* @since 2.17.0
*/
@Getter
public class PluginConfigUpdatedEvent extends ApplicationEvent {
private final Map<String, JsonNode> oldConfig;
private final Map<String, JsonNode> newConfig;

@Builder
public PluginConfigUpdatedEvent(Object source, Map<String, JsonNode> oldConfig,
Map<String, JsonNode> newConfig) {
super(source);
this.oldConfig = oldConfig;
this.newConfig = newConfig;
}
}
4 changes: 4 additions & 0 deletions api/src/main/java/run/halo/app/plugin/PluginContext.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package run.halo.app.plugin;

import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.pf4j.RuntimeMode;
Expand All @@ -17,10 +18,13 @@
* @since 2.10.0
*/
@Getter
@Builder
@RequiredArgsConstructor
public class PluginContext {
private final String name;

private final String configMapName;

private final String version;

private final RuntimeMode runtimeMode;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package run.halo.app.plugin;

import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_SINGLETON;
import static org.springframework.util.ResourceUtils.CLASSPATH_URL_PREFIX;

import java.io.IOException;
Expand Down Expand Up @@ -101,10 +102,9 @@ public ApplicationContext create(String pluginId) {

rootContext.getBeanProvider(ReactiveExtensionClient.class)
.ifUnique(client -> {
var reactiveSettingFetcher = new DefaultReactiveSettingFetcher(client, pluginId);
var settingFetcher = new DefaultSettingFetcher(reactiveSettingFetcher);
beanFactory.registerSingleton("reactiveSettingFetcher", reactiveSettingFetcher);
beanFactory.registerSingleton("settingFetcher", settingFetcher);
context.registerBean("reactiveSettingFetcher",
guqing marked this conversation as resolved.
Show resolved Hide resolved
DefaultReactiveSettingFetcher.class, bhd -> bhd.setScope(SCOPE_SINGLETON));
beanFactory.registerSingleton("settingFetcher", DefaultSettingFetcher.class);
});

rootContext.getBeanProvider(PluginRequestMappingHandlerMapping.class)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package run.halo.app.plugin;

import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import run.halo.app.core.extension.Plugin;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.infra.exception.NotFoundException;

/**
* Default implementation of {@link PluginGetter}.
*
* @author guqing
* @since 2.17.0
*/
@Component
@RequiredArgsConstructor
public class DefaultPluginGetter implements PluginGetter {
private final ExtensionClient client;

@Override
public Plugin getPlugin(String name) {
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("Plugin name must not be blank");
}
return client.fetch(Plugin.class, name)
.orElseThrow(() -> new NotFoundException("Plugin not found"));
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
package run.halo.app.plugin;

import static run.halo.app.extension.index.query.QueryFactory.equal;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.Plugin;
import run.halo.app.extension.ConfigMap;
import run.halo.app.extension.DefaultExtensionMatcher;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.controller.Controller;
import run.halo.app.extension.controller.ControllerBuilder;
import run.halo.app.extension.controller.Reconciler;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.infra.utils.JsonParseException;
import run.halo.app.infra.utils.JsonUtils;

Expand All @@ -20,15 +34,36 @@
* @author guqing
* @since 2.0.0
*/
public class DefaultReactiveSettingFetcher implements ReactiveSettingFetcher {
public class DefaultReactiveSettingFetcher
implements ReactiveSettingFetcher, Reconciler<Reconciler.Request>, DisposableBean,
ApplicationContextAware {

private final ReactiveExtensionClient client;

private final ExtensionClient blockingClient;

private final CacheManager cacheManager;

/**
* The application context of the plugin.
*/
private ApplicationContext applicationContext;

private final String pluginName;

public DefaultReactiveSettingFetcher(ReactiveExtensionClient client, String pluginName) {
private final String configMapName;

private final String cacheName;

public DefaultReactiveSettingFetcher(PluginContext pluginContext,
ReactiveExtensionClient client, ExtensionClient blockingClient,
CacheManager cacheManager) {
this.client = client;
this.pluginName = pluginName;
this.pluginName = pluginContext.getName();
this.configMapName = pluginContext.getConfigMapName();
this.blockingClient = blockingClient;
this.cacheManager = cacheManager;
this.cacheName = buildCacheKey(pluginName);
}

@Override
Expand Down Expand Up @@ -60,26 +95,31 @@ private Mono<JsonNode> getInternal(String group) {
.defaultIfEmpty(JsonNodeFactory.instance.missingNode());
}

private Mono<Map<String, JsonNode>> getValuesInternal() {
return configMap(pluginName)
.mapNotNull(ConfigMap::getData)
.map(data -> {
Map<String, JsonNode> result = new LinkedHashMap<>();
data.forEach((key, value) -> result.put(key, readTree(value)));
return result;
})
.defaultIfEmpty(Map.of());
}

private Mono<ConfigMap> configMap(String pluginName) {
return client.fetch(Plugin.class, pluginName)
.flatMap(plugin -> {
String configMapName = plugin.getSpec().getConfigMapName();
if (StringUtils.isBlank(configMapName)) {
return Mono.empty();
}
return client.fetch(ConfigMap.class, plugin.getSpec().getConfigMapName());
});
Mono<Map<String, JsonNode>> getValuesInternal() {
var cache = getCache();
var cachedValue = getCachedConfigData(cache);
if (cachedValue != null) {
return Mono.justOrEmpty(cachedValue);
}
return Mono.defer(() -> {
// double check
var newCachedValue = getCachedConfigData(cache);
if (newCachedValue != null) {
return Mono.justOrEmpty(newCachedValue);
}
if (StringUtils.isBlank(configMapName)) {
return Mono.empty();
}
return client.fetch(ConfigMap.class, configMapName)
.mapNotNull(ConfigMap::getData)
.map(data -> {
Map<String, JsonNode> result = new LinkedHashMap<>();
data.forEach((key, value) -> result.put(key, readTree(value)));
return result;
})
.defaultIfEmpty(Map.of())
.doOnNext(values -> cache.put(pluginName, values));
});
}

private JsonNode readTree(String json) {
Expand All @@ -96,4 +136,76 @@ private JsonNode readTree(String json) {
private <T> T convertValue(JsonNode jsonNode, Class<T> clazz) {
return JsonUtils.DEFAULT_JSON_MAPPER.convertValue(jsonNode, clazz);
}

@NonNull
private Cache getCache() {
var cache = cacheManager.getCache(cacheName);
if (cache == null) {
// should never happen
throw new IllegalStateException("Cache [" + cacheName + "] not found.");
}
return cache;
}

static String buildCacheKey(String pluginName) {
return "plugin-" + pluginName + "-configmap";
}

@Override
public Result reconcile(Request request) {
blockingClient.fetch(ConfigMap.class, configMapName)
.ifPresent(configMap -> {
var cache = getCache();
var existData = getCachedConfigData(cache);
var configMapData = configMap.getData();
Map<String, JsonNode> result = new LinkedHashMap<>();
if (configMapData != null) {
configMapData.forEach((key, value) -> result.put(key, readTree(value)));
}
applicationContext.publishEvent(PluginConfigUpdatedEvent.builder()
.source(this)
.oldConfig(existData)
.newConfig(result)
.build());
// update cache
cache.put(pluginName, result);
});
return Result.doNotRetry();
}

@Nullable
@SuppressWarnings("unchecked")
private Map<String, JsonNode> getCachedConfigData(@NonNull Cache cache) {
var existData = cache.get(pluginName);
if (existData == null) {
return null;
}
return (Map<String, JsonNode>) existData.get();
}

@Override
public Controller setupWith(ControllerBuilder builder) {
var configMap = new ConfigMap();
var extensionMatcher =
DefaultExtensionMatcher.builder(blockingClient, configMap.groupVersionKind())
.fieldSelector(FieldSelector.of(equal("metadata.name", configMapName)))
.build();
return builder
.extension(configMap)
.syncAllOnStart(false)
.onAddMatcher(extensionMatcher)
.onUpdateMatcher(extensionMatcher)
.build();
}

@Override
public void destroy() {
getCache().invalidate();
}

@Override
public void setApplicationContext(@NonNull ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ protected ExtensionFinder createExtensionFinder() {
@Override
protected PluginFactory createPluginFactory() {
var contextFactory = new DefaultPluginApplicationContextFactory(this);
return new SpringPluginFactory(contextFactory);
var pluginGetter = rootContext.getBean(PluginGetter.class);
return new SpringPluginFactory(contextFactory, pluginGetter);
}

@Override
Expand Down
24 changes: 24 additions & 0 deletions application/src/main/java/run/halo/app/plugin/PluginGetter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package run.halo.app.plugin;

import run.halo.app.core.extension.Plugin;
import run.halo.app.infra.exception.NotFoundException;

/**
* An interface to get {@link Plugin} by name.
*
* @author guqing
* @since 2.17.0
*/
@FunctionalInterface
public interface PluginGetter {

/**
* Get plugin by name.
*
* @param name plugin name must not be null
* @return plugin
* @throws IllegalArgumentException if plugin name is null
* @throws NotFoundException if plugin not found
*/
Plugin getPlugin(String name);
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package run.halo.app.plugin;

import org.springframework.cache.CacheManager;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
Expand Down Expand Up @@ -56,6 +57,8 @@ public static ApplicationContext create(ApplicationContext rootContext) {
rootContext.getBean(ExternalLinkProcessor.class));
beanFactory.registerSingleton("postContentService",
rootContext.getBean(PostContentService.class));
beanFactory.registerSingleton("cacheManager",
rootContext.getBean(CacheManager.class));
// TODO add more shared instance here

sharedContext.refresh();
Expand Down
Loading