perf: add caching for system configuration fetcher to enhance performance (#7100)

#### What type of PR is this?
/kind improvement
/area core
/milestone 2.20.x

#### What this PR does / why we need it:
为系统配置获取增加缓存以提高路由和主题模板渲染的速度

#### Special notes for your reviewer:
1. 系统能正确初始化
2. 测试修改系统配置后 http://localhost:8090/actuator/globalinfo 和主题端 `${site}` 是否都是新的
3. 更改了文章路由规则后能正确调整到新的规则

#### Does this PR introduce a user-facing change?

```release-note
为系统配置的获取增加缓存以提高路由和主题模板渲染的速度
```
pull/7094/head^2
guqing 2024-12-04 10:31:08 +08:00 committed by GitHub
parent 7bd9408519
commit 2b4d1ab8d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 89 additions and 35 deletions

View File

@ -76,7 +76,7 @@ public class SystemConfigEndpoint implements CustomEndpoint {
private Mono<ServerResponse> updateConfigByGroup(ServerRequest request) {
final var group = request.pathVariable("group");
return request.bodyToMono(ObjectNode.class)
.flatMap(objectNode -> configurableEnvironmentFetcher.getConfigMap()
.flatMap(objectNode -> configurableEnvironmentFetcher.loadConfigMap()
.flatMap(configMap -> {
var data = configMap.getData();
data.put(group, JsonUtils.objectToJson(objectNode));

View File

@ -295,6 +295,6 @@ public class SystemSettingReconciler implements Reconciler<Reconciler.Request> {
}
private Optional<ConfigMap> getConfigMap(String name) {
return environmentFetcher.getConfigMapBlocking();
return environmentFetcher.loadConfigMapBlocking();
}
}

View File

@ -1,19 +1,28 @@
package run.halo.app.infra;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.github.fge.jsonpatch.JsonPatchException;
import com.github.fge.jsonpatch.mergepatch.JsonMergePatch;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import run.halo.app.extension.ConfigMap;
import run.halo.app.extension.ExtensionMatcher;
import run.halo.app.extension.ListOptions;
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.infra.utils.JsonParseException;
import run.halo.app.infra.utils.JsonUtils;
@ -27,9 +36,10 @@ import run.halo.app.infra.utils.JsonUtils;
* @since 2.0.0
*/
@Component
public class SystemConfigurableEnvironmentFetcher {
public class SystemConfigurableEnvironmentFetcher implements Reconciler<Reconciler.Request> {
private final ReactiveExtensionClient extensionClient;
private final ConversionService conversionService;
private final AtomicReference<ConfigMap> configMapCache = new AtomicReference<>();
public SystemConfigurableEnvironmentFetcher(ReactiveExtensionClient extensionClient,
ConversionService conversionService) {
@ -71,31 +81,27 @@ public class SystemConfigurableEnvironmentFetcher {
.defaultIfEmpty(Map.of());
}
/**
* Gets config map.
*
* @return a new {@link ConfigMap} named <code>system</code> by json merge patch.
*/
public Mono<ConfigMap> getConfigMap() {
Mono<ConfigMap> mapMono =
extensionClient.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG_DEFAULT);
if (mapMono == null) {
return Mono.empty();
}
return mapMono.flatMap(systemDefault ->
extensionClient.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG)
.map(system -> {
Map<String, String> defaultData = systemDefault.getData();
Map<String, String> data = system.getData();
Map<String, String> mergedData = mergeData(defaultData, data);
system.setData(mergedData);
return system;
})
.switchIfEmpty(Mono.just(systemDefault)));
return Mono.fromSupplier(configMapCache::get)
.switchIfEmpty(Mono.defer(this::loadConfigMapInternal));
}
public Optional<ConfigMap> getConfigMapBlocking() {
return getConfigMap().blockOptional();
/**
* Load the system config map from the extension client.
*
* @return latest configMap from {@link ReactiveExtensionClient} without any cache.
*/
public Mono<ConfigMap> loadConfigMap() {
return loadConfigMapInternal();
}
/**
* Gets the system config map without any cache.
*
* @return load configMap from {@link ReactiveExtensionClient}
*/
public Optional<ConfigMap> loadConfigMapBlocking() {
return loadConfigMapInternal().blockOptional();
}
private Map<String, String> mergeData(Map<String, String> defaultData,
@ -132,7 +138,7 @@ public class SystemConfigurableEnvironmentFetcher {
return copiedDefault;
}
String mergeRemappingFunction(String dataV, String defaultV) {
private String mergeRemappingFunction(String dataV, String defaultV) {
JsonNode dataJsonValue = nullSafeToJsonNode(dataV);
// original
JsonNode defaultJsonValue = nullSafeToJsonNode(defaultV);
@ -147,8 +153,57 @@ public class SystemConfigurableEnvironmentFetcher {
}
}
JsonNode nullSafeToJsonNode(String json) {
private JsonNode nullSafeToJsonNode(String json) {
return StringUtils.isBlank(json) ? JsonNodeFactory.instance.nullNode()
: JsonUtils.jsonToObject(json, JsonNode.class);
}
@Override
public Result reconcile(Request request) {
loadConfigMapInternal()
// should never happen
.switchIfEmpty(Mono.error(new IllegalStateException("System configMap not found.")))
.doOnNext(configMapCache::set)
.block();
return Result.doNotRetry();
}
@Override
public Controller setupWith(ControllerBuilder builder) {
ExtensionMatcher matcher = extension -> Objects.equals(extension.getMetadata().getName(),
SystemSetting.SYSTEM_CONFIG);
return builder
.extension(new ConfigMap())
.syncAllOnStart(true)
.syncAllListOptions(ListOptions.builder()
.fieldQuery(equal("metadata.name", SystemSetting.SYSTEM_CONFIG))
.build())
.onAddMatcher(matcher)
.onUpdateMatcher(matcher)
.onDeleteMatcher(matcher)
.build();
}
/**
* Gets config map.
*
* @return a new {@link ConfigMap} named <code>system</code> by json merge patch.
*/
private Mono<ConfigMap> loadConfigMapInternal() {
Mono<ConfigMap> mapMono =
extensionClient.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG_DEFAULT);
if (mapMono == null) {
return Mono.empty();
}
return mapMono.flatMap(systemDefault ->
extensionClient.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG)
.map(system -> {
Map<String, String> defaultData = systemDefault.getData();
Map<String, String> data = system.getData();
Map<String, String> mergedData = mergeData(defaultData, data);
system.setData(mergedData);
return system;
})
.switchIfEmpty(Mono.just(systemDefault)));
}
}

View File

@ -177,7 +177,7 @@ public class SystemSetupEndpoint {
)
.subscribeOn(Schedulers.boundedElastic());
var basicConfigMono = Mono.defer(() -> systemConfigFetcher.getConfigMap()
var basicConfigMono = Mono.defer(() -> systemConfigFetcher.loadConfigMap()
.flatMap(configMap -> {
mergeToBasicConfig(body, configMap);
return client.update(configMap);

View File

@ -18,7 +18,6 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.ApplicationContext;
import run.halo.app.core.reconciler.SystemSettingReconciler;
import run.halo.app.extension.ConfigMap;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.Metadata;
@ -59,7 +58,7 @@ class SystemSettingReconcilerTest {
rules.setArchives("archives-new");
return rules;
});
when(environmentFetcher.getConfigMapBlocking()).thenReturn(Optional.of(configMap));
when(environmentFetcher.loadConfigMapBlocking()).thenReturn(Optional.of(configMap));
when(client.fetch(eq(ConfigMap.class), eq(SystemSetting.SYSTEM_CONFIG)))
.thenReturn(Optional.of(configMap));
systemSettingReconciler.reconcile(new Reconciler.Request(SystemSetting.SYSTEM_CONFIG));
@ -83,7 +82,7 @@ class SystemSettingReconcilerTest {
rules.setTags("tags-new");
return rules;
});
when(environmentFetcher.getConfigMapBlocking()).thenReturn(Optional.of(configMap));
when(environmentFetcher.loadConfigMapBlocking()).thenReturn(Optional.of(configMap));
when(client.fetch(eq(ConfigMap.class), eq(SystemSetting.SYSTEM_CONFIG)))
.thenReturn(Optional.of(configMap));
systemSettingReconciler.reconcile(new Reconciler.Request(SystemSetting.SYSTEM_CONFIG));
@ -104,7 +103,7 @@ class SystemSettingReconcilerTest {
rules.setCategories("categories-new");
return rules;
});
when(environmentFetcher.getConfigMapBlocking()).thenReturn(Optional.of(configMap));
when(environmentFetcher.loadConfigMapBlocking()).thenReturn(Optional.of(configMap));
when(client.fetch(eq(ConfigMap.class), eq(SystemSetting.SYSTEM_CONFIG)))
.thenReturn(Optional.of(configMap));
systemSettingReconciler.reconcile(new Reconciler.Request(SystemSetting.SYSTEM_CONFIG));
@ -125,7 +124,7 @@ class SystemSettingReconcilerTest {
rules.setPost("/post-new/{slug}");
return rules;
});
when(environmentFetcher.getConfigMapBlocking()).thenReturn(Optional.of(configMap));
when(environmentFetcher.loadConfigMapBlocking()).thenReturn(Optional.of(configMap));
when(client.fetch(eq(ConfigMap.class), eq(SystemSetting.SYSTEM_CONFIG)))
.thenReturn(Optional.of(configMap));
systemSettingReconciler.reconcile(new Reconciler.Request(SystemSetting.SYSTEM_CONFIG));

View File

@ -44,8 +44,8 @@ class SystemConfigurableEnvironmentFetcherTest {
}
@Test
void getConfigMap() {
environmentFetcher.getConfigMap()
void loadConfigMap() {
environmentFetcher.loadConfigMap()
.as(StepVerifier::create)
.consumeNextWith(configMap -> {
assertThat(configMap.getMetadata().getName())