From 3c97d06f882f2346f86a3d325a5651f2cc3fc93b Mon Sep 17 00:00:00 2001 From: Sergei Tsvetkov Date: Tue, 28 May 2024 10:30:01 +0300 Subject: [PATCH] feat: caching optimization Add a post-processor to enable asynchronous mode for Caffeine Add caching for roles Add caching for plugins Add caching for extension point definitions Add CacheConditionProvider for caching conditions Add CacheNames to store cache names Add a feature-toggle function for manually switching caching feature (disabled by default) --- .../app/cache/CacheConditionProvider.java | 100 ++++++++++++++++++ .../java/run/halo/app/cache/CacheNames.java | 41 +++++++ .../CaffeineCacheManagerPostProcessor.java | 32 ++++++ .../extension/service/DefaultRoleService.java | 6 ++ .../ReactiveExtensionClientImpl.java | 57 ++++++++++ .../DefaultExtensionGetter.java | 10 ++ .../src/main/resources/application.yaml | 7 ++ 7 files changed, 253 insertions(+) create mode 100644 application/src/main/java/run/halo/app/cache/CacheConditionProvider.java create mode 100644 application/src/main/java/run/halo/app/cache/CacheNames.java create mode 100644 application/src/main/java/run/halo/app/config/postproccessor/CaffeineCacheManagerPostProcessor.java diff --git a/application/src/main/java/run/halo/app/cache/CacheConditionProvider.java b/application/src/main/java/run/halo/app/cache/CacheConditionProvider.java new file mode 100644 index 000000000..fd51a2f27 --- /dev/null +++ b/application/src/main/java/run/halo/app/cache/CacheConditionProvider.java @@ -0,0 +1,100 @@ +package run.halo.app.cache; + +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; +import run.halo.app.extension.ReactiveExtensionClientImpl; +import run.halo.app.infra.properties.HaloProperties; + +/** + * Provides methods to determine if specific types of caches are evictable or enabled. + * + *

Uses {@link HaloProperties} to check the configuration for cache settings. + * + * @author sergei + * @see HaloProperties#getCaches() + * @see Cacheable#condition() + * @see CacheEvict#condition() + * @see ReactiveExtensionClientImpl + */ +@Component +@RequiredArgsConstructor +public class CacheConditionProvider { + private static final String ROLE_KIND = "Role"; + private static final String PLUGIN_KIND = "Plugin"; + private static final String EXTENSION_POINT_DEFINITION_KIND = "ExtensionPointDefinition"; + + private final HaloProperties properties; + + /** + * Determines if the role dependencies cache is evictable based on the provided kind. + * + * @param kind the kind to check against. + * @return {@code true} if the role dependencies cache is evictable, {@code false} otherwise. + */ + public boolean isRoleDependenciesCacheEvictableByKind(String kind) { + return isRoleCacheEnabled() && Objects.equals(kind, ROLE_KIND); + } + + /** + * Determines if the plugin extension cache is evictable based on the provided kind. + * + * @param kind the kind to check against. + * @return {@code true} if the plugin extension cache is evictable, {@code false} otherwise. + */ + public boolean isPluginExtensionCacheEvictableByKind(String kind) { + return isPluginExtensionCacheEnabled() && Objects.equals(kind, PLUGIN_KIND); + } + + /** + * Determines if the extension point definition cache is evictable based on the provided kind. + * + * @param kind the kind to check against. + * @return {@code true} if the extension point definition cache is evictable, {@code false} + * otherwise. + */ + public boolean isExtensionPointDefinitionCacheEvictableByKind(String kind) { + return isExtensionPointDefinitionCacheEnabled() && Objects.equals(kind, + EXTENSION_POINT_DEFINITION_KIND); + } + + /** + * Checks if the role dependencies cache is enabled. + * + * @return {@code true} if the role dependencies cache is enabled, {@code false} otherwise + */ + public boolean isRoleCacheEnabled() { + return isCacheEnabled("role-dependencies"); + } + + /** + * Checks if the plugin extension cache is enabled. + * + * @return {@code true} if the plugin extension cache is enabled, {@code false} otherwise. + */ + public boolean isPluginExtensionCacheEnabled() { + return isCacheEnabled("plugin-extension"); + } + + /** + * Checks if the extension point definition cache is enabled. + * + * @return {@code true} if the extension point definition cache is enabled, {@code false} + * otherwise. + */ + public boolean isExtensionPointDefinitionCacheEnabled() { + return isCacheEnabled("extension-point-definition"); + } + + private boolean isCacheEnabled(String cache) { + var cacheProperties = properties.getCaches().get(cache); + + if (cacheProperties != null) { + return !cacheProperties.isDisabled(); + } + + return false; + } +} diff --git a/application/src/main/java/run/halo/app/cache/CacheNames.java b/application/src/main/java/run/halo/app/cache/CacheNames.java new file mode 100644 index 000000000..f19d1c624 --- /dev/null +++ b/application/src/main/java/run/halo/app/cache/CacheNames.java @@ -0,0 +1,41 @@ +package run.halo.app.cache; + +import java.util.Set; +import run.halo.app.core.extension.service.DefaultRoleService; +import run.halo.app.extension.ReactiveExtensionClientImpl; +import run.halo.app.plugin.extensionpoint.DefaultExtensionGetter; + +/** + * Defines constant cache names used in the application. + * + * @author sergei + */ +public final class CacheNames { + + /** + * Cache name for dependencies roles. + * + * @see DefaultRoleService#listDependenciesFlux(Set) + * @see ReactiveExtensionClientImpl + */ + public static final String ROLE_DEPENDENCIES = "ROLE_DEPENDENCIES"; + + /** + * Cache name for plugin extensions. + * + * @see DefaultExtensionGetter#getExtensions(Class) + * @see ReactiveExtensionClientImpl + */ + public static final String PLUGIN_EXTENSIONS = "PLUGIN_EXTENSIONS"; + + /** + * Cache name for plugin extensions. + * + * @see DefaultExtensionGetter#getEnabledExtensionByDefinition(Class) + * @see ReactiveExtensionClientImpl + */ + public static final String EXTENSION_POINT_DEFINITIONS = "EXTENSION_POINT_DEFINITIONS"; + + private CacheNames() { + } +} diff --git a/application/src/main/java/run/halo/app/config/postproccessor/CaffeineCacheManagerPostProcessor.java b/application/src/main/java/run/halo/app/config/postproccessor/CaffeineCacheManagerPostProcessor.java new file mode 100644 index 000000000..43658d056 --- /dev/null +++ b/application/src/main/java/run/halo/app/config/postproccessor/CaffeineCacheManagerPostProcessor.java @@ -0,0 +1,32 @@ +package run.halo.app.config.postproccessor; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.cache.caffeine.CaffeineCacheManager; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +/** + * Post-processes of {@link CaffeineCacheManager} beans to enabled async mode for them after + * initialization. + * + * @author sergei + * @see CaffeineCacheManager#setAsyncCacheMode(boolean) + */ +@Component +public class CaffeineCacheManagerPostProcessor implements BeanPostProcessor { + + @Override + public Object postProcessAfterInitialization(@NonNull Object bean, @NonNull String beanName) + throws BeansException { + if (bean instanceof CaffeineCacheManager cacheManager) { + configure(cacheManager); + } + + return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName); + } + + private void configure(CaffeineCacheManager caffeineCacheManager) { + caffeineCacheManager.setAsyncCacheMode(true); + } +} diff --git a/application/src/main/java/run/halo/app/core/extension/service/DefaultRoleService.java b/application/src/main/java/run/halo/app/core/extension/service/DefaultRoleService.java index 2feb50f87..949c89dc8 100644 --- a/application/src/main/java/run/halo/app/core/extension/service/DefaultRoleService.java +++ b/application/src/main/java/run/halo/app/core/extension/service/DefaultRoleService.java @@ -13,11 +13,13 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.springframework.cache.annotation.Cacheable; import org.springframework.lang.NonNull; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import run.halo.app.cache.CacheNames; import run.halo.app.core.extension.Role; import run.halo.app.core.extension.RoleBinding; import run.halo.app.core.extension.RoleBinding.RoleRef; @@ -72,6 +74,10 @@ public class DefaultRoleService implements RoleService { } @Override + @Cacheable( + value = CacheNames.ROLE_DEPENDENCIES, + condition = "@cacheConditionProvider.isRoleCacheEnabled()" + ) public Flux listDependenciesFlux(Set names) { return listDependencies(names, shouldFilterHidden(false)); } diff --git a/application/src/main/java/run/halo/app/extension/ReactiveExtensionClientImpl.java b/application/src/main/java/run/halo/app/extension/ReactiveExtensionClientImpl.java index e6f11098d..9e3fca7a3 100644 --- a/application/src/main/java/run/halo/app/extension/ReactiveExtensionClientImpl.java +++ b/application/src/main/java/run/halo/app/extension/ReactiveExtensionClientImpl.java @@ -18,6 +18,8 @@ import java.util.function.Predicate; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Caching; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; import org.springframework.dao.DataIntegrityViolationException; @@ -29,6 +31,7 @@ import org.springframework.transaction.reactive.TransactionalOperator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.retry.Retry; +import run.halo.app.cache.CacheNames; import run.halo.app.extension.exception.ExtensionNotFoundException; import run.halo.app.extension.index.DefaultExtensionIterator; import run.halo.app.extension.index.ExtensionIterator; @@ -170,6 +173,24 @@ public class ReactiveExtensionClientImpl implements ReactiveExtensionClient { } @Override + @Caching(evict = { + @CacheEvict( + value = CacheNames.ROLE_DEPENDENCIES, + condition = "@cacheConditionProvider.isRoleDependenciesCacheEvictableByKind" + + "(#extension.kind)", + allEntries = true), + @CacheEvict( + value = CacheNames.PLUGIN_EXTENSIONS, + condition = + "@cacheConditionProvider.isPluginExtensionCacheEvictableByKind" + + "(#extension.kind)", + allEntries = true), + @CacheEvict( + value = CacheNames.EXTENSION_POINT_DEFINITIONS, + condition = "@cacheConditionProvider.isExtensionPointDefinitionCacheEvictableByKind" + + "(#extension.kind)", + allEntries = true) + }) public Mono create(E extension) { checkClientWritable(extension); return Mono.just(extension) @@ -203,6 +224,24 @@ public class ReactiveExtensionClientImpl implements ReactiveExtensionClient { } @Override + @Caching(evict = { + @CacheEvict( + value = CacheNames.ROLE_DEPENDENCIES, + condition = "@cacheConditionProvider.isRoleDependenciesCacheEvictableByKind" + + "(#extension.kind)", + allEntries = true), + @CacheEvict( + value = CacheNames.PLUGIN_EXTENSIONS, + condition = + "@cacheConditionProvider.isPluginExtensionCacheEvictableByKind" + + "(#extension.kind)", + allEntries = true), + @CacheEvict( + value = CacheNames.EXTENSION_POINT_DEFINITIONS, + condition = "@cacheConditionProvider.isExtensionPointDefinitionCacheEvictableByKind" + + "(#extension.kind)", + allEntries = true) + }) public Mono update(E extension) { checkClientWritable(extension); // Refactor the atomic reference if we have a better solution. @@ -245,6 +284,24 @@ public class ReactiveExtensionClientImpl implements ReactiveExtensionClient { } @Override + @Caching(evict = { + @CacheEvict( + value = CacheNames.ROLE_DEPENDENCIES, + condition = "@cacheConditionProvider.isRoleDependenciesCacheEvictableByKind" + + "(#extension.kind)", + allEntries = true), + @CacheEvict( + value = CacheNames.PLUGIN_EXTENSIONS, + condition = + "@cacheConditionProvider.isPluginExtensionCacheEvictableByKind" + + "(#extension.kind)", + allEntries = true), + @CacheEvict( + value = CacheNames.EXTENSION_POINT_DEFINITIONS, + condition = "@cacheConditionProvider.isExtensionPointDefinitionCacheEvictableByKind" + + "(#extension.kind)", + allEntries = true) + }) public Mono delete(E extension) { checkClientWritable(extension); // set deletionTimestamp diff --git a/application/src/main/java/run/halo/app/plugin/extensionpoint/DefaultExtensionGetter.java b/application/src/main/java/run/halo/app/plugin/extensionpoint/DefaultExtensionGetter.java index 1db8faea8..b7c0064d1 100644 --- a/application/src/main/java/run/halo/app/plugin/extensionpoint/DefaultExtensionGetter.java +++ b/application/src/main/java/run/halo/app/plugin/extensionpoint/DefaultExtensionGetter.java @@ -8,12 +8,14 @@ import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import org.pf4j.ExtensionPoint; import org.pf4j.PluginManager; +import org.springframework.cache.annotation.Cacheable; import org.springframework.context.ApplicationContext; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import run.halo.app.cache.CacheNames; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemSetting.ExtensionPointEnabled; @@ -72,6 +74,10 @@ public class DefaultExtensionGetter implements ExtensionGetter { } @Override + @Cacheable( + value = CacheNames.EXTENSION_POINT_DEFINITIONS, + condition = "@cacheConditionProvider.isExtensionPointDefinitionCacheEnabled()" + ) public Flux getEnabledExtensionByDefinition( Class extensionPoint) { return fetchExtensionPointDefinition(extensionPoint) @@ -88,6 +94,10 @@ public class DefaultExtensionGetter implements ExtensionGetter { } @Override + @Cacheable( + value = CacheNames.PLUGIN_EXTENSIONS, + condition = "@cacheConditionProvider.isPluginExtensionCacheEnabled()" + ) public Flux getExtensions(Class extensionPointClass) { var extensions = new ArrayList<>(pluginManager.getExtensions(extensionPointClass)); applicationContext.getBeanProvider(extensionPointClass) diff --git a/application/src/main/resources/application.yaml b/application/src/main/resources/application.yaml index d289d8cf2..7043c4cce 100644 --- a/application/src/main/resources/application.yaml +++ b/application/src/main/resources/application.yaml @@ -37,6 +37,13 @@ halo: page: # Disable page cache by default due to experimental feature disabled: true + role-dependencies: + disabled: ${ROLE_DEPENDENCIES_CACHE_DISABLED:true} + plugin-extension: + disabled: ${PLUGIN_EXTENSION_CACHE_DISABLED:true} + extension-point-definition: + disabled: ${EXTENSION_POINT_DEFINITION_CACHE_DISABLED:true} + work-dir: ${user.home}/.halo2 plugin: plugins-root: ${halo.work-dir}/plugins