fix(credential): 修复并行测试偶发失败——Credential Manager 调用串行化,模拟 Gitea 改阻塞读
All checks were successful
Push & PR Check / check (push) Successful in 1m29s
All checks were successful
Push & PR Check / check (push) Successful in 1m29s
全量并行 cargo test 10 次失败 3 次(单跑稳定通过),两个独立根因: - 同进程并发 CredWriteW/CredDeleteW 会丢更新:写入并校验成功的凭据随后读回 NOT_FOUND, 删除成功的条目又被并发写回。R10 readiness 报 needs_repair、开发变量试点报「凭证引用缺失」 均源于此(诊断日志证实无跨测试删除;8 线程压力实验直接复现)。windows_store 的 put/get/delete 经进程级锁串行,生产中 MCP 与 UI 并发写凭据同样受益;跨进程不在保护范围 - Windows 上从 nonblocking listener accept 的 socket 继承 nonblocking,set_read_timeout 不生效,请求晚到即 WouldBlock。mcp_server 两处模拟 Gitea 补 set_nonblocking(false), 与 commands/gitea.rs 既有写法一致 验证:修复后全量并行 cargo test 连续 30 次 0 失败(247 passed、5 ignored)。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
79853b71f2
commit
4a37438a58
@ -494,7 +494,7 @@ modules:
|
||||
area: infra
|
||||
status: done
|
||||
progress: 100
|
||||
updated: 2026-09-26
|
||||
updated: 2026-10-02
|
||||
note: "R1–R11 的合成路径和真实 Codex CLI 客户端试点已验证;真实用户凭证迁移、正式版 UI 与生产链路未验证,未启动第二个日常 Tauri 实例。"
|
||||
position: [3000, 950]
|
||||
|
||||
|
||||
@ -68,6 +68,16 @@ progress: 100
|
||||
|
||||
## 任务拆解
|
||||
|
||||
### ✅ R12 — 修复并行测试偶发失败:Credential Manager 串行化 + 模拟 Gitea 阻塞读
|
||||
- status: done
|
||||
- complexity: S
|
||||
- files: src-tauri/src/credential_store.rs, src-tauri/src/mcp_server.rs
|
||||
- depends: R3, R10
|
||||
- acceptance: "agent_verify: [cargo test] 全量并行连续 30 次 → 0 失败(修复前基线 10 次失败 3 次:R10 scoped_mcp_create_pull_request_uses_synthetic_credential 的 readiness=needs_repair、scoped_dev_variable_http_pilot 的「凭证引用缺失」;仅修凭据后 30 次仍有 1 次 R10 模拟 Gitea read_line WouldBlock)。"
|
||||
- verification: "2026-10-02:修复后全量并行 cargo test 连续 30 次 0 失败(247 passed、5 ignored);仅凭据串行化的中间版本 30 次中凭据类失败为 0。"
|
||||
- unverified: "跨进程并发(如 tauri dev 实例与 cargo test 同时写凭据)不受进程内锁保护;历史失败可能在本机 Credential Manager 残留无权限合成条目,非 diag 前缀的未逐条清理。"
|
||||
- root_cause: "①同进程并发 CredWriteW/CredDeleteW 丢更新——写入成功后立即读回 NOT_FOUND、删除成功的条目被并发写回(8 线程压力实验复现;诊断日志证实无跨测试删除);②Windows 上 nonblocking listener accept 出的 socket 继承 nonblocking,set_read_timeout 不生效,客户端请求晚到即 WouldBlock(commands/gitea.rs 已有同款修复,mcp_server 两处 mock 遗漏)。"
|
||||
|
||||
### ✅ R11 — 隔离真实客户端试点宿主
|
||||
- status: done
|
||||
- complexity: M
|
||||
|
||||
@ -328,6 +328,17 @@ mod windows_store {
|
||||
const CRED_MAX_CREDENTIAL_BLOB_SIZE: usize = 5 * 512;
|
||||
const ERROR_NOT_FOUND: i32 = 1168;
|
||||
|
||||
/// 同进程内并发 CredWrite/CredDelete 实测会丢更新:写入成功后立即读回 NOT_FOUND,
|
||||
/// 删除成功的条目又被并发写回(2026-10-02 并行 cargo test 偶发 needs_repair 的根因)。
|
||||
/// 所有 Credential Manager 调用经此锁串行;跨进程并发不在此保护范围内。
|
||||
static CREDENTIAL_MANAGER_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
fn serialized() -> std::sync::MutexGuard<'static, ()> {
|
||||
CREDENTIAL_MANAGER_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct FileTime {
|
||||
low: u32,
|
||||
@ -402,7 +413,10 @@ mod windows_store {
|
||||
target_alias: null_mut(),
|
||||
user_name: null_mut(),
|
||||
};
|
||||
let ok = unsafe { CredWriteW(&credential, 0) };
|
||||
let ok = {
|
||||
let _guard = serialized();
|
||||
unsafe { CredWriteW(&credential, 0) }
|
||||
};
|
||||
blob.fill(0);
|
||||
if ok == 0 {
|
||||
Err("Credential Manager 写入失败".into())
|
||||
@ -414,6 +428,7 @@ mod windows_store {
|
||||
pub fn get(reference: &str) -> Result<Option<Vec<u8>>, String> {
|
||||
let target = target(reference)?;
|
||||
let mut raw = null_mut();
|
||||
let _guard = serialized();
|
||||
let ok = unsafe { CredReadW(target.as_ptr(), CRED_TYPE_GENERIC, 0, &mut raw) };
|
||||
if ok == 0 {
|
||||
let error = std::io::Error::last_os_error();
|
||||
@ -450,6 +465,7 @@ mod windows_store {
|
||||
|
||||
pub fn delete(reference: &str) -> Result<(), String> {
|
||||
let target = target(reference)?;
|
||||
let _guard = serialized();
|
||||
let ok = unsafe { CredDeleteW(target.as_ptr(), CRED_TYPE_GENERIC, 0) };
|
||||
if ok != 0 {
|
||||
return Ok(());
|
||||
|
||||
@ -1045,6 +1045,8 @@ mod tests {
|
||||
Err(error) => panic!("模拟 Gitea 未收到请求: {error}"),
|
||||
}
|
||||
};
|
||||
// Windows 上从 nonblocking listener 接受的 socket 也可能保持 nonblocking。
|
||||
stream.set_nonblocking(false).unwrap();
|
||||
stream
|
||||
.set_read_timeout(Some(std::time::Duration::from_secs(3)))
|
||||
.unwrap();
|
||||
@ -1217,6 +1219,8 @@ mod tests {
|
||||
Err(error) => panic!("5 分钟内未收到客户端的模拟 PR 请求: {error}"),
|
||||
}
|
||||
};
|
||||
// Windows 上从 nonblocking listener 接受的 socket 也可能保持 nonblocking。
|
||||
stream.set_nonblocking(false).unwrap();
|
||||
stream
|
||||
.set_read_timeout(Some(std::time::Duration::from_secs(5)))
|
||||
.unwrap();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user