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)
pull/6009/head
Sergei Tsvetkov 2024-05-28 10:30:01 +03:00
parent afabffc546
commit 3c97d06f88
7 changed files with 253 additions and 0 deletions

View File

@ -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.
*
* <p>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;
}
}

View File

@ -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() {
}
}

View File

@ -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);
}
}

View File

@ -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<Role> listDependenciesFlux(Set<String> names) {
return listDependencies(names, shouldFilterHidden(false));
}

View File

@ -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 <E extends Extension> Mono<E> 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 <E extends Extension> Mono<E> 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 <E extends Extension> Mono<E> delete(E extension) {
checkClientWritable(extension);
// set deletionTimestamp

View File

@ -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 <T extends ExtensionPoint> Flux<T> getEnabledExtensionByDefinition(
Class<T> extensionPoint) {
return fetchExtensionPointDefinition(extensionPoint)
@ -88,6 +94,10 @@ public class DefaultExtensionGetter implements ExtensionGetter {
}
@Override
@Cacheable(
value = CacheNames.PLUGIN_EXTENSIONS,
condition = "@cacheConditionProvider.isPluginExtensionCacheEnabled()"
)
public <T extends ExtensionPoint> Flux<T> getExtensions(Class<T> extensionPointClass) {
var extensions = new ArrayList<>(pluginManager.getExtensions(extensionPointClass));
applicationContext.getBeanProvider(extensionPointClass)

View File

@ -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