Skip to content

Commit f99b90e

Browse files
committed
fix(tasks): proofread 任务 id 认领并入各族统一单调分配(去 +1 偏移)
1 parent 68bf43a commit f99b90e

3 files changed

Lines changed: 86 additions & 3 deletions

File tree

‎app/src-tauri/src/commands_ai_refine.rs‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,10 @@ pub async fn ai_refine_start(
205205
}
206206
drop(guards);
207207
}
208-
// ③ 注册任务 + 后台执行(spawn_blocking——网络/分析不阻塞异步运行时)
209-
let task_id = st.ai_task_seq.fetch_add(1, Ordering::Relaxed);
208+
// ③ 注册任务 + 后台执行(spawn_blocking——网络/分析不阻塞异步运行时);
209+
// 任务 id 走全任务族统一认领封装(claim_task_id——见其 Why:proofread
210+
// 原 +1 偏移与各族口径冲突,相邻认领撞 id 会被 INSERT OR REPLACE 顶替)
211+
let task_id = claim_task_id(&st.ai_task_seq);
210212
{
211213
let mut tasks = st.ai_tasks.lock().map_err(|e| format!("任务注册表锁中毒: {}", e))?;
212214
tasks.insert(task_id, AiTaskEntry { state: AiTaskState::Pending, result: None, target_id: session_id });
@@ -616,6 +618,24 @@ pub fn task_seq_lower_bound(current: u64, db_max_task_id: u64) -> u64 {
616618
current.max(db_max_task_id.saturating_add(1))
617619
}
618620

621+
/// 认领下一个 AI 任务 id(全任务族唯一分配点——proofread/refine/enrich/
622+
/// note_refine/goal_plan 共用的单调序列,返回值即 task_id,推进量=认领量)。
623+
///
624+
/// @ai-context Why(2026-09-09 批 2 审查修复):proofread 原写作
625+
/// fetch_add(1, SeqCst) + 1(+1 存量来源 1904c2c7——按「0 起
626+
/// 序列」直觉,但 task_seq() 初值 1 且其余族均以 fetch_add
627+
/// 返回值直接作 id)。fetch_add(1)+1 只把计数器推进 1 却领走
628+
/// 后值:紧邻的下一次认领(任意其他族)恰好拿到同一 id →
629+
/// insert_ai_task 的 INSERT OR REPLACE 运行期顶替先落库行
630+
/// (running 记账/结果/成本丢行)。统一本函数分配:认领严格
631+
/// 单调、相邻认领永不相交。Relaxed 即足——唯一性由 fetch_add
632+
/// 原子读-改-写保证,无需跨线程同步排序(与启动序列下限
633+
/// fetch_update 及 enrich/note_refine/goal_plan 现场同式内联
634+
/// 的 Relaxed 同档,口径一致)。
635+
pub fn claim_task_id(seq: &AtomicU64) -> u64 {
636+
seq.fetch_add(1, Ordering::Relaxed)
637+
}
638+
619639
/// 任务注册表(AppState 装配)。
620640
pub fn task_registry() -> Arc<Mutex<HashMap<u64, AiTaskEntry>>> {
621641
Arc::new(Mutex::new(HashMap::new()))

‎app/src-tauri/src/commands_proofread.rs‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,12 @@ pub async fn proofread_run(
158158
let expected: Vec<&str> = flat.iter().take(take).map(|(_, s)| *s).collect();
159159
let chars: usize = expected.iter().map(|s| s.chars().count()).sum();
160160

161-
let task_id = st.ai_task_seq.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1;
161+
// 任务 id 认领走全任务族统一封装(claim_task_id——见其 Why:proofread
162+
// 原 fetch_add(1, SeqCst) + 1(+1 存量来源 1904c2c7)与 refine/enrich/
163+
// note_refine/goal_plan 的裸 fetch_add 口径冲突——fetch_add 只推进 1 却
164+
// 领走后值,跨任务族紧邻认领会撞同 id,被 insert_ai_task 的 INSERT OR
165+
// REPLACE 运行期顶替;修复:去掉 +1,与其他族同一单调分配)
166+
let task_id = crate::commands_ai_refine::claim_task_id(&st.ai_task_seq);
162167
let created_at = crate::db::unix_seconds();
163168
let _ = db.insert_ai_task(&crate::db_ai_tasks::AiTaskRecord {
164169
task_id,
@@ -372,3 +377,7 @@ pub fn proofread_list(
372377
items,
373378
})
374379
}
380+
381+
#[cfg(test)]
382+
#[path = "commands_proofread_tests.rs"]
383+
mod tests;
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//! commands_proofread.rs 单测(AAA 模式;内存态原子——不触库/网络)。
2+
//!
3+
//! @ai-context: 覆盖任务 id 认领语义——proofread 族与其余任务族(refine/
4+
//! enrich/note_refine/goal_plan)共用 AppState.ai_task_seq 同一
5+
//! 单调序列(P2-1 审查修复回归,见 claim_task_id Why)。
6+
7+
use crate::commands_ai_refine::{claim_task_id, task_seq};
8+
9+
/// AAA(P2-1 回归):proofread 认领 N 后任意其他族紧邻认领 ≠ N——
10+
/// 相邻认领不相交。修复前 proofread 为 fetch_add(1)+1(1904c2c7 引入):
11+
/// 计数器 1→2 却领走 2,其他族紧邻 fetch_add 恰好也返回 2——跨任务族
12+
/// 同 id,insert_ai_task 的 INSERT OR REPLACE 会顶替先落库的 proofread 行
13+
/// (running 记账/结果/成本丢行)。
14+
#[test]
15+
fn proofread_claim_then_adjacent_family_claim_do_not_collide() {
16+
// Arrange:与装配等价的序列起点(task_seq() 初值 1——app_setup 即以此
17+
// 装配 AppState.ai_task_seq;proofread_run 现与 refine 同一封装认领)
18+
let seq = task_seq();
19+
// Act:proofread 族先认领 N,任意其他族紧邻认领(生产同一代码路径)
20+
let proofread_id = claim_task_id(&seq);
21+
let other_family_id = claim_task_id(&seq);
22+
// Assert:相邻认领不相交(修复前两者都 = 2)
23+
assert_ne!(
24+
proofread_id, other_family_id,
25+
"proofread 与相邻认领不得撞 id(撞 id 会被 INSERT OR REPLACE 顶替)"
26+
);
27+
// 序列语义回归:初值 1 起逐号分配;认领后计数器越过所领 id(修复前
28+
// proofread 认领后计数器仍停在所领 id 上——同一破绽的另一面)
29+
assert_eq!(proofread_id, 1, "首个 id 应为 1(task_seq 初值语义)");
30+
assert_eq!(other_family_id, 2, "紧邻认领应为下一号");
31+
assert_eq!(seq.load(std::sync::atomic::Ordering::Relaxed), 3);
32+
}
33+
34+
/// AAA:认领与启动序列下限的衔接——DB 最大 id 为 41 时(含历史行),重启后
35+
/// proofread 首个认领必须 ≥42(防复用历史 task_id 顶替已采纳行——与批 1
36+
/// task_seq_lower_bound 语义闭合,proofread 无特殊偏移)。
37+
#[test]
38+
fn proofread_claim_after_startup_lower_bound_never_reuses_db_rows() {
39+
// Arrange:启动序列按 DB 全表最大 id 41 推进(app_setup fetch_update)
40+
let seq = task_seq();
41+
let _ = seq.fetch_update(
42+
std::sync::atomic::Ordering::Relaxed,
43+
std::sync::atomic::Ordering::Relaxed,
44+
|cur| Some(crate::commands_ai_refine::task_seq_lower_bound(cur, 41)),
45+
);
46+
// Act:proofread 族认领 + 紧邻其他族认领
47+
let proofread_id = claim_task_id(&seq);
48+
let other_family_id = claim_task_id(&seq);
49+
// Assert:从 42 起且互不相交(修复前 proofread fetch_add(1)+1 在计数器
50+
// 42 处领走 43,紧邻族 fetch_add 返回 43——同 id 相撞,同上顶替风险)
51+
assert_eq!(proofread_id, 42, "proofread 首个认领须越过 DB 最大 id");
52+
assert_eq!(other_family_id, 43);
53+
assert_ne!(proofread_id, other_family_id);
54+
}

0 commit comments

Comments
 (0)