Skip to content

Commit

Permalink
refactor: enhance cache management in plugin setting config (#6141)
Browse files Browse the repository at this point in the history
#### What type of PR is this?
/kind feature
/area plugin
/area core
/milestone 2.17.x

#### What this PR does / why we need it:
增强插件配置的缓存管理

1. 通过 SettingFetcher/ReactiveSettingFetcher 获取插件配置可以不在考虑获取数据的性能问题,当数据变更后会自动更新缓存
2. 现在你可以通过在插件中监听 `PluginConfigUpdatedEvent` 事件来做一些处理,它会在用户更改插件配置后被触发

#### Does this PR introduce a user-facing change?
```release-note
增强插件配置的缓存管理并支持通过监听 `PluginConfigUpdatedEvent` 事件做一些特殊处理
```
  • Loading branch information
guqing authored Jun 26, 2024
1 parent 59edade commit 68d428a
Show file tree
Hide file tree
Showing 11 changed files with 326 additions and 74 deletions.
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",
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

0 comments on commit 68d428a

Please sign in to comment.