refactor: remove page cache feature (#6108)

#### What type of PR is this?

/area core
/kind api-change
/milestone 2.17.x

#### What this PR does / why we need it:

移除内置的页面静态缓存功能,后续将由 https://github.com/halo-sigs/plugin-page-cache 插件提供。

#### Which issue(s) this PR fixes:

Fixes #5639 

#### Special notes for your reviewer:

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

```release-note
移除内置的页面静态缓存功能,后续由 https://github.com/halo-sigs/plugin-page-cache 插件提供。
```
pull/6216/head
Ryan Wang 2024-06-28 18:08:59 +08:00 committed by GitHub
parent 4cafdb5a72
commit f0445f4e51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 20 additions and 373 deletions

View File

@ -2366,31 +2366,6 @@
]
}
},
"/apis/api.console.halo.run/v1alpha1/caches/{name}": {
"delete": {
"description": "Evict a cache.",
"operationId": "EvictCache",
"parameters": [
{
"description": "Cache name",
"in": "path",
"name": "name",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"204": {
"description": "No Content"
}
},
"tags": [
"CacheV1alpha1Console"
]
}
},
"/apis/api.console.halo.run/v1alpha1/comments": {
"get": {
"description": "List comments.",
@ -19692,12 +19667,12 @@
},
"visible": {
"type": "string",
"default": "PUBLIC",
"enum": [
"PUBLIC",
"INTERNAL",
"PRIVATE"
]
],
"default": "PUBLIC"
}
}
},
@ -21592,12 +21567,12 @@
},
"visible": {
"type": "string",
"default": "PUBLIC",
"enum": [
"PUBLIC",
"INTERNAL",
"PRIVATE"
]
],
"default": "PUBLIC"
}
}
},

View File

@ -233,31 +233,6 @@
]
}
},
"/apis/api.console.halo.run/v1alpha1/caches/{name}": {
"delete": {
"description": "Evict a cache.",
"operationId": "EvictCache",
"parameters": [
{
"description": "Cache name",
"in": "path",
"name": "name",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"204": {
"description": "No Content"
}
},
"tags": [
"CacheV1alpha1Console"
]
}
},
"/apis/api.console.halo.run/v1alpha1/comments": {
"get": {
"description": "List comments.",
@ -5166,12 +5141,12 @@
},
"visible": {
"type": "string",
"default": "PUBLIC",
"enum": [
"PUBLIC",
"INTERNAL",
"PRIVATE"
]
],
"default": "PUBLIC"
}
}
},
@ -5677,12 +5652,12 @@
},
"visible": {
"type": "string",
"default": "PUBLIC",
"enum": [
"PUBLIC",
"INTERNAL",
"PRIVATE"
]
],
"default": "PUBLIC"
}
}
},

View File

@ -10609,12 +10609,12 @@
},
"visible": {
"type": "string",
"default": "PUBLIC",
"enum": [
"PUBLIC",
"INTERNAL",
"PRIVATE"
]
],
"default": "PUBLIC"
}
}
},
@ -11799,12 +11799,12 @@
},
"visible": {
"type": "string",
"default": "PUBLIC",
"enum": [
"PUBLIC",
"INTERNAL",
"PRIVATE"
]
],
"default": "PUBLIC"
}
}
},

View File

@ -1582,12 +1582,12 @@
},
"visible": {
"type": "string",
"default": "PUBLIC",
"enum": [
"PUBLIC",
"INTERNAL",
"PRIVATE"
]
],
"default": "PUBLIC"
}
}
},

View File

@ -9,5 +9,6 @@ package run.halo.app.theme.router;
public enum ModelConst {
;
public static final String TEMPLATE_ID = "_templateId";
public static final String POWERED_BY_HALO_TEMPLATE_ENGINE = "poweredByHaloTemplateEngine";
public static final Integer DEFAULT_PAGE_SIZE = 10;
}

View File

@ -1,55 +0,0 @@
package run.halo.app.cache;
import static io.swagger.v3.oas.annotations.enums.ParameterIn.PATH;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
import static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route;
import static org.springframework.http.HttpStatus.NO_CONTENT;
import org.springdoc.core.fn.builders.apiresponse.Builder;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.endpoint.CustomEndpoint;
@Component
public class CacheEndpoint implements CustomEndpoint {
private final CacheManager cacheManager;
public CacheEndpoint(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Override
public RouterFunction<ServerResponse> endpoint() {
var tag = "CacheV1alpha1Console";
return route()
.DELETE("/caches/{name}", this::evictCache, builder -> builder
.tag(tag)
.operationId("EvictCache")
.description("Evict a cache.")
.parameter(parameterBuilder()
.name("name")
.in(PATH)
.required(true)
.description("Cache name"))
.response(Builder.responseBuilder()
.responseCode(String.valueOf(NO_CONTENT.value())))
.build())
.build();
}
private Mono<ServerResponse> evictCache(ServerRequest request) {
var cacheName = request.pathVariable("name");
if (cacheManager.getCacheNames().contains(cacheName)) {
var cache = cacheManager.getCache(cacheName);
if (cache != null) {
cache.invalidate();
}
}
return ServerResponse.accepted().build();
}
}

View File

@ -1,18 +0,0 @@
package run.halo.app.cache;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.List;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
/**
* Cached response. Refer to
* <a href="https://github.com/spring-cloud/spring-cloud-gateway/blob/f98aa6d47bf802019f07063f4fd7af6047f15116/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/factory/cache/CachedResponse.java">here</a> }
*/
public record CachedResponse(HttpStatusCode statusCode,
HttpHeaders headers,
List<ByteBuffer> body,
Instant timestamp) {
}

View File

@ -1,158 +0,0 @@
package run.halo.app.cache;
import static java.nio.ByteBuffer.allocateDirect;
import static org.springframework.http.HttpHeaders.CACHE_CONTROL;
import static run.halo.app.infra.AnonymousUserConst.isAnonymousUser;
import java.time.Instant;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.lang.NonNull;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Slf4j
public class PageCacheWebFilter implements WebFilter, Ordered {
public static final String REQUEST_TO_CACHE = "RequestCacheWebFilterToCache";
public static final String CACHE_NAME = "page";
private final Cache cache;
public PageCacheWebFilter(CacheManager cacheManager) {
this.cache = cacheManager.getCache(CACHE_NAME);
}
@Override
@NonNull
public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) {
return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.map(Authentication::getName)
.filter(name -> isAnonymousUser(name) && requestCacheable(exchange.getRequest()))
.switchIfEmpty(Mono.defer(() -> chain.filter(exchange).then(Mono.empty())))
.flatMap(name -> {
var cacheKey = generateCacheKey(exchange.getRequest());
var cachedResponse = cache.get(cacheKey, CachedResponse.class);
if (cachedResponse != null) {
// cache hit, then write the cached response
return writeCachedResponse(exchange.getResponse(), cachedResponse);
}
// decorate the ServerHttpResponse to cache the response
var decoratedExchange = exchange.mutate()
.response(new CacheResponseDecorator(exchange, cacheKey))
.build();
return chain.filter(decoratedExchange);
});
}
private boolean requestCacheable(ServerHttpRequest request) {
return HttpMethod.GET.equals(request.getMethod())
&& !hasRequestBody(request)
&& enableCacheByCacheControl(request.getHeaders());
}
private boolean enableCacheByCacheControl(HttpHeaders headers) {
return headers.getOrEmpty(CACHE_CONTROL)
.stream()
.noneMatch(cacheControl ->
"no-store".equals(cacheControl) || "private".equals(cacheControl));
}
private boolean responseCacheable(ServerWebExchange exchange) {
var response = exchange.getResponse();
if (!MediaType.TEXT_HTML.equals(response.getHeaders().getContentType())) {
return false;
}
var statusCode = response.getStatusCode();
if (statusCode == null || !statusCode.isSameCodeAs(HttpStatus.OK)) {
return false;
}
return exchange.getAttributeOrDefault(REQUEST_TO_CACHE, false);
}
private static boolean hasRequestBody(ServerHttpRequest request) {
return request.getHeaders().getContentLength() > 0;
}
private String generateCacheKey(ServerHttpRequest request) {
return request.getURI().toASCIIString();
}
@Override
public int getOrder() {
// The filter should be after org.springframework.security.web.server.WebFilterChainProxy
return Ordered.LOWEST_PRECEDENCE;
}
private Mono<Void> writeCachedResponse(ServerHttpResponse response,
CachedResponse cachedResponse) {
response.setStatusCode(cachedResponse.statusCode());
response.getHeaders().clear();
response.getHeaders().addAll(cachedResponse.headers());
var body = Flux.fromIterable(cachedResponse.body())
.map(byteBuffer -> response.bufferFactory().wrap(byteBuffer));
return response.writeWith(body);
}
class CacheResponseDecorator extends ServerHttpResponseDecorator {
private final ServerWebExchange exchange;
private final String cacheKey;
public CacheResponseDecorator(ServerWebExchange exchange, String cacheKey) {
super(exchange.getResponse());
this.exchange = exchange;
this.cacheKey = cacheKey;
}
@Override
@NonNull
public Mono<Void> writeWith(@NonNull Publisher<? extends DataBuffer> body) {
if (responseCacheable(exchange)) {
var response = getDelegate();
body = Flux.from(body)
.map(dataBuffer -> {
var byteBuffer = allocateDirect(dataBuffer.readableByteCount());
dataBuffer.toByteBuffer(byteBuffer);
DataBufferUtils.release(dataBuffer);
return byteBuffer.asReadOnlyBuffer();
})
.collectSortedList()
.doOnSuccess(byteBuffers -> {
var headers = new HttpHeaders();
headers.addAll(response.getHeaders());
var cachedResponse = new CachedResponse(response.getStatusCode(),
headers,
byteBuffers,
Instant.now());
cache.put(cacheKey, cachedResponse);
})
.flatMapMany(Flux::fromIterable)
.map(byteBuffer -> response.bufferFactory().wrap(byteBuffer));
}
// write the response
return super.writeWith(body);
}
}
}

View File

@ -1,20 +0,0 @@
package run.halo.app.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.server.WebFilter;
import run.halo.app.cache.PageCacheWebFilter;
@EnableCaching
@Configuration
public class CacheConfiguration {
@Bean
@ConditionalOnProperty(name = "halo.cache.page.disabled", havingValue = "false")
WebFilter pageCacheWebFilter(CacheManager cacheManager) {
return new PageCacheWebFilter(cacheManager);
}
}

View File

@ -5,12 +5,14 @@ import com.fasterxml.jackson.databind.MapperFeature;
import java.io.IOException;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import run.halo.app.infra.properties.HaloProperties;
import run.halo.app.search.lucene.LuceneSearchEngine;
@EnableCaching
@Configuration(proxyBeanMethods = false)
@EnableAsync
public class HaloConfiguration {

View File

@ -5,8 +5,6 @@ import jakarta.validation.constraints.NotNull;
import java.net.URL;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ -63,9 +61,6 @@ public class HaloProperties implements Validator {
@Valid
private final AttachmentProperties attachment = new AttachmentProperties();
@Valid
private final Map<String, CacheProperties> caches = new LinkedHashMap<>();
@Override
public boolean supports(Class<?> clazz) {
return HaloProperties.class.isAssignableFrom(clazz);

View File

@ -15,8 +15,8 @@ import org.thymeleaf.spring6.view.reactive.ThymeleafReactiveView;
import org.thymeleaf.spring6.view.reactive.ThymeleafReactiveViewResolver;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.cache.PageCacheWebFilter;
import run.halo.app.theme.finders.FinderRegistry;
import run.halo.app.theme.router.ModelConst;
@Component("thymeleafReactiveViewResolver")
public class HaloViewResolver extends ThymeleafReactiveViewResolver {
@ -53,7 +53,7 @@ public class HaloViewResolver extends ThymeleafReactiveViewResolver {
return themeResolver.getTheme(exchange).flatMap(theme -> {
// calculate the engine before rendering
setTemplateEngine(engineManager.getTemplateEngine(theme));
exchange.getAttributes().put(PageCacheWebFilter.REQUEST_TO_CACHE, true);
exchange.getAttributes().put(ModelConst.POWERED_BY_HALO_TEMPLATE_ENGINE, true);
return super.render(model, contentType, exchange);
});
}

View File

@ -33,10 +33,6 @@ spring:
spec: expireAfterAccess=1h, maximumSize=10000
halo:
caches:
page:
# Disable page cache by default due to experimental feature
disabled: true
work-dir: ${user.home}/.halo2
plugin:
plugins-root: ${halo.work-dir}/plugins

View File

@ -2,6 +2,7 @@ apiVersion: v1alpha1
kind: "Role"
metadata:
name: role-template-manage-cache
deletionTimestamp: 2024-06-01T00:00:00Z
labels:
halo.run/role-template: "true"
annotations:

View File

@ -6,7 +6,6 @@ import {
IconAccountCircleLine,
IconArrowRight,
IconBookRead,
IconDatabase2Line,
IconFolder,
IconPages,
IconPalette,
@ -154,33 +153,6 @@ const actions: Action[] = [
},
permissions: ["system:posts:manage"],
},
{
icon: markRaw(IconDatabase2Line),
title: t(
"core.dashboard.widgets.presets.quicklink.actions.evict_page_cache.title"
),
action: () => {
Dialog.warning({
title: t(
"core.dashboard.widgets.presets.quicklink.actions.evict_page_cache.dialog_title"
),
description: t(
"core.dashboard.widgets.presets.quicklink.actions.evict_page_cache.dialog_content"
),
confirmText: t("core.common.buttons.confirm"),
cancelText: t("core.common.buttons.cancel"),
onConfirm: async () => {
await consoleApiClient.cache.evictCache({ name: "page" });
Toast.success(
t(
"core.dashboard.widgets.presets.quicklink.actions.evict_page_cache.success_message"
)
);
},
});
},
permissions: ["system:caches:manage"],
},
];
</script>
<template>

View File

@ -7,7 +7,6 @@ import {
AuthProviderV1alpha1Api,
AuthProviderV1alpha1ConsoleApi,
BackupV1alpha1Api,
CacheV1alpha1ConsoleApi,
CategoryV1alpha1Api,
CommentV1alpha1Api,
CommentV1alpha1ConsoleApi,
@ -277,7 +276,6 @@ function createConsoleApiClient(axiosInstance: AxiosInstance) {
baseURL,
axiosInstance
),
cache: new CacheV1alpha1ConsoleApi(undefined, baseURL, axiosInstance),
login: new LoginApi(undefined, baseURL, axiosInstance),
storage: {
attachment: new AttachmentV1alpha1ConsoleApi(

View File

@ -9,7 +9,6 @@ api/attachment-v1alpha1-uc-api.ts
api/auth-provider-v1alpha1-api.ts
api/auth-provider-v1alpha1-console-api.ts
api/backup-v1alpha1-api.ts
api/cache-v1alpha1-console-api.ts
api/category-v1alpha1-api.ts
api/category-v1alpha1-public-api.ts
api/comment-v1alpha1-api.ts

View File

@ -22,7 +22,6 @@ export * from './api/attachment-v1alpha1-uc-api';
export * from './api/auth-provider-v1alpha1-api';
export * from './api/auth-provider-v1alpha1-console-api';
export * from './api/backup-v1alpha1-api';
export * from './api/cache-v1alpha1-console-api';
export * from './api/category-v1alpha1-api';
export * from './api/category-v1alpha1-public-api';
export * from './api/comment-v1alpha1-api';

View File

@ -162,11 +162,6 @@ core:
This operation will recreate search engine indexes for all
published posts.
success_message: Refresh search engine index successfully.
evict_page_cache:
title: Refresh Page Cache
dialog_title: Refresh the page cache
dialog_content: This operation will clear the cache for all pages.
success_message: Refresh page cache successfully.
user_stats:
title: Users
comment_stats:

View File

@ -158,11 +158,6 @@ core:
dialog_title: 刷新搜索引擎索引
dialog_content: 此操作会对所有已发布的文章重新创建本地搜索引擎的索引。
success_message: 刷新成功
evict_page_cache:
title: 刷新页面缓存
dialog_title: 刷新页面缓存
dialog_content: 此操作会清空所有页面的缓存。
success_message: 刷新成功
user_stats:
title: 用户
comment_stats:

View File

@ -158,11 +158,6 @@ core:
dialog_title: 刷新搜尋引擎索引
dialog_content: 此操作會對所有已發佈的文章重新創建本地搜尋引擎的索引。
success_message: 刷新成功
evict_page_cache:
title: 重新整理頁面快取
dialog_title: 整理頁面快取
dialog_content: 這個操作會清空所有頁面的快取。
success_message: 刷新成功
user_stats:
title: 用戶
comment_stats: