From 1e33f527e03d89717d984ced518c8a9c0b69210e Mon Sep 17 00:00:00 2001 From: wunianxiao Date: Mon, 22 Jun 2026 10:48:16 +0800 Subject: [PATCH] =?UTF-8?q?fix(approval):=20clean=5Fexpired=20=E6=8C=89=20?= =?UTF-8?q?code=5Fhash=20=E5=AE=9A=E4=BD=8D=E5=88=A0=E9=99=A4=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=B9=B6=E5=8F=91=20index=20=E9=94=99?= =?UTF-8?q?=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P0-5: 原实现快照 (uid, index, code_hash) 后释放锁更新 DB,再重新加锁 按 index remove。若两阶段间 handle_reply 增删了条目,index 指向 错误条目,可能把未过期的审批误标 Expired 并通知等待方。 改为两阶段都按 code_hash 精确定位:收集过期 hash → 更新 DB → 重新加锁按 hash 查找删除,并发回复只会让查找安全跳过。 --- src/tools/approval.rs | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/tools/approval.rs b/src/tools/approval.rs index 11e7ce6..27dbc59 100644 --- a/src/tools/approval.rs +++ b/src/tools/approval.rs @@ -192,22 +192,32 @@ impl ApprovalManager { } /// 清理过期审批(按时间判定,通知等待方 + 更新 DB) + /// + /// 两阶段加锁:收集过期 code_hash → 更新 DB → 重新加锁按 code_hash 定位删除。 + /// 不依赖快照 index,避免并发回复导致 index 错位误删。 pub async fn clean_expired(&self) { let now = Instant::now(); - let mut expired = Vec::new(); + + // 第一阶段:持锁收集过期条目的 code_hash + let mut expired_hashes: Vec = Vec::new(); { let map = self.pending.lock().await; - for (uid, entries) in map.iter() { - for (i, e) in entries.iter().enumerate() { + for entries in map.values() { + for e in entries { if now > e.expires_at { - expired.push((uid.clone(), i, e.code_hash.clone())); + expired_hashes.push(e.code_hash.clone()); } } } } + if expired_hashes.is_empty() { + return; + } + + // DB 更新(按 code_hash 精确匹配,无错位问题) if let Some(ref pool) = self.pool { - for (_uid, _i, code_hash) in &expired { + for code_hash in &expired_hashes { let _ = sqlx::query( "UPDATE pending_approvals SET status = 'expired' WHERE code_hash = $1 AND status = 'pending'", ) @@ -217,13 +227,15 @@ impl ApprovalManager { } } + // 第二阶段:重新持锁,按 code_hash 定位并移除 + // (即使两阶段间 handle_reply 增删了条目,按 hash 查找也能安全跳过) let mut map = self.pending.lock().await; - for (uid, i, _) in expired.iter().rev() { - if let Some(entries) = map.get_mut(uid) - && *i < entries.len() - { - let entry = entries.remove(*i); - let _ = entry.sender.send(ApprovalDecision::Expired); + for code_hash in &expired_hashes { + for entries in map.values_mut() { + if let Some(idx) = entries.iter().position(|e| &e.code_hash == code_hash) { + let entry = entries.remove(idx); + let _ = entry.sender.send(ApprovalDecision::Expired); + } } } map.retain(|_, v| !v.is_empty());