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