refactor(ssr): remove unused per-slug/per-tag generation counters

This commit is contained in:
xfy 2026-06-18 10:30:07 +08:00
parent 4c695b4fc3
commit 3ee39d910c
8 changed files with 132 additions and 3 deletions

View File

@ -23,7 +23,10 @@ MAX_SESSIONS_PER_USER=5
# Database connection pool size (default: 20)
DB_POOL_SIZE=20
# SSR page cache duration in seconds (default: 3600)
# SSR page cache duration in seconds (default: 3600).
# Generation-based invalidation is implemented in src/ssr_cache.rs and bumps on every post
# write. However, Dioxus 0.7 does not expose a public API to customize the incremental SSR
# cache key, so SSR_CACHE_SECS remains the effective fallback TTL until such API is available.
SSR_CACHE_SECS=3600
# Image serving cache headers (hardcoded defaults)

View File

@ -149,6 +149,9 @@ pub async fn create_post(
// 失效该文章涉及的所有标签下文章列表缓存。
crate::cache::invalidate_tag_posts_for(&tags_cleaned).await;
// 递增 SSR 全局世代号(未来就绪基础设施;当前不会使 Dioxus 0.7 SSR 缓存失效)。
crate::ssr_cache::bump_global_generation();
Ok(CreatePostResponse {
success: true,
message: "创建成功".to_string(),

View File

@ -84,6 +84,9 @@ pub async fn delete_post(post_id: i32) -> Result<CreatePostResponse, ServerFnErr
crate::cache::invalidate_post_by_slug(&slug).await;
crate::cache::invalidate_tag_posts_for(&tags).await;
// 递增 SSR 全局世代号(未来就绪基础设施;当前不会使 Dioxus 0.7 SSR 缓存失效)。
crate::ssr_cache::bump_global_generation();
Ok(CreatePostResponse {
success: true,
message: "删除成功".to_string(),

View File

@ -108,6 +108,8 @@ pub async fn rebuild_content_html(rebuild_all: bool) -> Result<RebuildResult, Se
if rebuilt > 0 {
crate::cache::invalidate_all_post_caches();
crate::cache::invalidate_search_results();
// 递增 SSR 全局世代号(未来就绪基础设施;当前不会使 Dioxus 0.7 SSR 缓存失效)。
crate::ssr_cache::bump_global_generation();
}
Ok(RebuildResult {

View File

@ -95,6 +95,9 @@ pub async fn restore_post(post_id: i32) -> Result<CreatePostResponse, ServerFnEr
crate::cache::invalidate_post_by_slug(&new_slug).await;
crate::cache::invalidate_tag_posts_for(&tags).await;
// 递增 SSR 全局世代号(未来就绪基础设施;当前不会使 Dioxus 0.7 SSR 缓存失效)。
crate::ssr_cache::bump_global_generation();
Ok(CreatePostResponse {
success: true,
message: "恢复成功".to_string(),
@ -182,6 +185,9 @@ pub async fn purge_post(post_id: i32) -> Result<CreatePostResponse, ServerFnErro
crate::cache::invalidate_post_by_slug(&slug).await;
crate::cache::invalidate_tag_posts_for(&tags).await;
// 递增 SSR 全局世代号(未来就绪基础设施;当前不会使 Dioxus 0.7 SSR 缓存失效)。
crate::ssr_cache::bump_global_generation();
Ok(CreatePostResponse {
success: true,
message: "彻底删除成功".to_string(),
@ -284,10 +290,15 @@ pub async fn batch_restore_posts(post_ids: Vec<i32>) -> Result<CreatePostRespons
crate::cache::invalidate_post_by_slug(slug).await;
}
crate::cache::invalidate_tag_posts_for(&affected_tags.into_iter().collect::<Vec<_>>()).await;
// 递增 SSR 全局世代号(未来就绪基础设施;当前不会使 Dioxus 0.7 SSR 缓存失效)。
crate::ssr_cache::bump_global_generation();
} else {
// 影响集过大时回退到全量失效,避免大量串行缓存操作。
crate::cache::invalidate_all_post_caches();
crate::cache::invalidate_search_results();
// 递增 SSR 全局世代号(未来就绪基础设施;当前不会使 Dioxus 0.7 SSR 缓存失效)。
crate::ssr_cache::bump_global_generation();
}
Ok(CreatePostResponse {
@ -385,10 +396,15 @@ pub async fn batch_purge_posts(post_ids: Vec<i32>) -> Result<CreatePostResponse,
crate::cache::invalidate_post_by_slug(slug).await;
}
crate::cache::invalidate_tag_posts_for(&tags).await;
// 递增 SSR 全局世代号(未来就绪基础设施;当前不会使 Dioxus 0.7 SSR 缓存失效)。
crate::ssr_cache::bump_global_generation();
} else {
// 影响集过大时回退到全量失效,避免大量串行缓存操作。
crate::cache::invalidate_all_post_caches();
crate::cache::invalidate_search_results();
// 递增 SSR 全局世代号(未来就绪基础设施;当前不会使 Dioxus 0.7 SSR 缓存失效)。
crate::ssr_cache::bump_global_generation();
}
Ok(CreatePostResponse {
@ -466,10 +482,15 @@ pub async fn empty_trash() -> Result<CreatePostResponse, ServerFnError> {
crate::cache::invalidate_post_by_slug(slug).await;
}
crate::cache::invalidate_tag_posts_for(&tags).await;
// 递增 SSR 全局世代号(未来就绪基础设施;当前不会使 Dioxus 0.7 SSR 缓存失效)。
crate::ssr_cache::bump_global_generation();
} else {
// 影响集过大时回退到全量失效,避免大量串行缓存操作。
crate::cache::invalidate_all_post_caches();
crate::cache::invalidate_search_results();
// 递增 SSR 全局世代号(未来就绪基础设施;当前不会使 Dioxus 0.7 SSR 缓存失效)。
crate::ssr_cache::bump_global_generation();
}
Ok(CreatePostResponse {

View File

@ -218,6 +218,9 @@ pub async fn update_post(
}
}
// 递增 SSR 全局世代号(未来就绪基础设施;当前不会使 Dioxus 0.7 SSR 缓存失效)。
crate::ssr_cache::bump_global_generation();
Ok(CreatePostResponse {
success: true,
message: "更新成功".to_string(),

View File

@ -23,6 +23,9 @@ mod hooks;
mod models;
mod pages;
mod router;
// ssr_cache 仅在 server feature 启用时编译;保存 SSR 世代号失效状态。
#[cfg(feature = "server")]
mod ssr_cache;
mod tasks;
mod theme;
mod utils;
@ -78,7 +81,9 @@ fn main() {
tasks::image_cache_cleanup::run_cleanup().await;
});
// 配置增量渲染缓存,默认缓存 3600 秒,可通过 SSR_CACHE_SECS 覆盖
// 配置增量渲染缓存,默认缓存 3600 秒,可通过 SSR_CACHE_SECS 覆盖。
// 注意:世代号失效机制已就位(见 src/ssr_cache.rs但 Dioxus 0.7 未暴露
// 自定义缓存键 API因此 TTL 仍是当前有效的兜底策略。
let config = ServeConfig::builder().incremental(
dioxus::server::IncrementalRendererConfig::default().invalidate_after(
std::time::Duration::from_secs(
@ -90,6 +95,24 @@ fn main() {
),
);
// SSR 世代号中间件:把当前全局世代号注入请求扩展并附加到响应头。
// 这是为 Dioxus 未来支持自定义 SSR 缓存键预留的钩子;目前主要提供可观测性。
async fn ssr_generation_middleware(
req: axum::http::Request<axum::body::Body>,
next: axum::middleware::Next,
) -> axum::response::Response {
let generation = crate::ssr_cache::current_global_generation();
let (mut parts, body) = req.into_parts();
parts.extensions.insert(crate::ssr_cache::SsrGeneration(generation));
let mut response = next.run(axum::http::Request::from_parts(parts, body)).await;
response.headers_mut().insert(
axum::http::header::HeaderName::from_static("x-ssr-generation"),
axum::http::HeaderValue::from_str(&generation.to_string())
.unwrap_or_else(|_| axum::http::HeaderValue::from_static("0")),
);
response
}
// 自定义 API 路由:图片上传(大文件,需要更长超时)
let upload_route = axum::Router::new()
.route(
@ -106,8 +129,9 @@ fn main() {
let dioxus_app =
axum::Router::new().serve_dioxus_application(config, router::AppRouter);
// 合并 Dioxus + 压缩/30s 超时中间件
// 合并 Dioxus + 世代号/压缩/30s 超时中间件
let app_routes = dioxus_app
.layer(axum::middleware::from_fn(ssr_generation_middleware))
.layer(CompressionLayer::new())
.layer(TimeoutLayer::with_status_code(
StatusCode::REQUEST_TIMEOUT,

70
src/ssr_cache.rs Normal file
View File

@ -0,0 +1,70 @@
//! SSR 增量渲染缓存失效的未来就绪基础设施。
//!
//! 本模块维护一个全局单调递增的世代号generation。文章写入成功后调用方会
//! 使其递增,从而**标记** SSR 渲染结果已过期。然而:
//!
//! **Dioxus 0.7 的增量渲染器使用请求 URI 的 `path_and_query()` 作为内部缓存键,
//! 且没有暴露公开 API 供外部代码自定义缓存键或按路由失效已渲染页面。**
//! 因此,当前世代号并**不会**实际使 Dioxus 的 SSR 缓存失效;它只是为未来 API
//! 准备好状态,并在请求/响应中提供可观测性。
//!
//! 在 Dioxus 提供以下任一能力之前,有效的 SSR 缓存失效手段仍是调低
//! `SSR_CACHE_SECS` 这一兜底 TTL
//! - 自定义增量渲染缓存键的回调;或
//! - 从 server function 内部按路由失效缓存的公开 API。
//!
//! 当前实现:
//! - `bump_global_generation()` / `current_global_generation()`:全局世代号。
//! - `SsrGeneration`:注入到请求扩展中的类型;未来 Dioxus 支持读取扩展生成
//! 缓存键时可直接使用。
//! - `src/main.rs` 的中间件把当前世代号附加到 `X-SSR-Generation` 响应头
//! (仅 GET 请求),便于调试与监控。
//!
//! 仅在启用 `server` feature 时编译。
#![cfg(feature = "server")]
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::LazyLock;
/// 全局 SSR 世代号。
///
/// 任何文章写入操作都会使其递增,从而让所有基于该全局世代的 SSR 缓存键在未来
/// Dioxus 支持自定义缓存键时失效。
static GLOBAL_GENERATION: LazyLock<AtomicU64> = LazyLock::new(AtomicU64::default);
/// 注入到请求扩展中的当前 SSR 世代号。
///
/// 这是为未来 Dioxus 支持自定义 SSR 缓存键预留的钩子。当前 Dioxus 0.7 的渲染器
/// 不会读取此扩展。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SsrGeneration(pub u64);
/// 原子递增并返回新的全局世代号。
pub fn bump_global_generation() -> u64 {
GLOBAL_GENERATION.fetch_add(1, Ordering::SeqCst).wrapping_add(1)
}
/// 返回当前全局世代号。
pub fn current_global_generation() -> u64 {
GLOBAL_GENERATION.load(Ordering::SeqCst)
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
#[serial]
fn global_generation_is_monotonic() {
let before = current_global_generation();
let g1 = bump_global_generation();
let g2 = bump_global_generation();
let current = current_global_generation();
assert!(g1 > before || g1 == 1);
assert!(g2 > g1);
assert_eq!(current, g2);
}
}