fix(posts): wrap rebuild_content_html in single transaction
整批 UPDATE 纳入事务,任一写入失败整批回滚,避免产生「部分文章已重建」的 中间态(M5)。渲染失败(spawn_blocking panic)仍跳过该条不进入事务操作。 失败时返回 rebuilt:0 明确告知整批回滚。
This commit is contained in:
parent
82ab190e0d
commit
71d4126e94
@ -32,7 +32,7 @@ pub async fn rebuild_content_html(rebuild_all: bool) -> Result<RebuildResult, Se
|
|||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
{
|
{
|
||||||
let client = get_conn().await.map_err(AppError::db_conn)?;
|
let mut client = get_conn().await.map_err(AppError::db_conn)?;
|
||||||
|
|
||||||
// 根据参数构造 WHERE 条件,限制单次处理数量。
|
// 根据参数构造 WHERE 条件,限制单次处理数量。
|
||||||
let query = if rebuild_all {
|
let query = if rebuild_all {
|
||||||
@ -51,6 +51,10 @@ pub async fn rebuild_content_html(rebuild_all: bool) -> Result<RebuildResult, Se
|
|||||||
let mut failed: u64 = 0;
|
let mut failed: u64 = 0;
|
||||||
let mut errors: Vec<String> = Vec::new();
|
let mut errors: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
// 整批 UPDATE 纳入单事务:中途断连或写入失败整批回滚,避免产生
|
||||||
|
// 「部分文章已重建」的中间态(M5)。
|
||||||
|
let tx = client.transaction().await.map_err(AppError::query)?;
|
||||||
|
|
||||||
for row in &rows {
|
for row in &rows {
|
||||||
let id: i32 = row.get(0);
|
let id: i32 = row.get(0);
|
||||||
let content_md: String = row.get(1);
|
let content_md: String = row.get(1);
|
||||||
@ -82,7 +86,7 @@ pub async fn rebuild_content_html(rebuild_all: bool) -> Result<RebuildResult, Se
|
|||||||
let word_count = crate::utils::text::count_words(&content_md);
|
let word_count = crate::utils::text::count_words(&content_md);
|
||||||
let reading_time = crate::utils::text::reading_time(word_count);
|
let reading_time = crate::utils::text::reading_time(word_count);
|
||||||
|
|
||||||
match client
|
match tx
|
||||||
.execute(
|
.execute(
|
||||||
"UPDATE posts SET content_html = $1, toc_html = $2, word_count = $3, reading_time = $4 WHERE id = $5",
|
"UPDATE posts SET content_html = $1, toc_html = $2, word_count = $3, reading_time = $4 WHERE id = $5",
|
||||||
&[
|
&[
|
||||||
@ -98,15 +102,26 @@ pub async fn rebuild_content_html(rebuild_all: bool) -> Result<RebuildResult, Se
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
rebuilt += 1;
|
rebuilt += 1;
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(e) => {
|
||||||
|
// 事务内任一写入失败会使事务进入 abort 状态,后续写入都会失败;
|
||||||
|
// 此时整批回滚,保证不产生中间态。
|
||||||
failed += 1;
|
failed += 1;
|
||||||
if errors.len() < MAX_DISPLAY_ERRORS {
|
if errors.len() < MAX_DISPLAY_ERRORS {
|
||||||
errors.push(format!("文章 #{id}: DB 写入失败"));
|
errors.push(format!("文章 #{id}: DB 写入失败(整批将回滚)"));
|
||||||
}
|
}
|
||||||
|
tracing::error!("rebuild UPDATE 失败,整批回滚: {:?}", e);
|
||||||
|
tx.rollback().await.ok();
|
||||||
|
return Ok(RebuildResult {
|
||||||
|
rebuilt: 0,
|
||||||
|
failed,
|
||||||
|
errors,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tx.commit().await.map_err(AppError::query)?;
|
||||||
|
|
||||||
// 重建会修改 word_count / reading_time 等列表项字段,批量影响列表、标签云、
|
// 重建会修改 word_count / reading_time 等列表项字段,批量影响列表、标签云、
|
||||||
// 标签文章及单篇缓存;这里使用全量失效作为务实的回退策略。
|
// 标签文章及单篇缓存;这里使用全量失效作为务实的回退策略。
|
||||||
if rebuilt > 0 {
|
if rebuilt > 0 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user