整包工作流恢复只在需要恢复时才取整轮任务锁
- `recover_direct_taonier_regeneration_workflow_at` 先做只读判据(复用 `direct_taonier_regeneration_workflow_requires_recovery_at`),不需要恢复就直接返回 - 真要动手才取 `direct-codex-art`,并在锁下重读一次判据(以锁下那一份为准) - 原来无条件取锁:正在跑的整包重生成整轮持有它,入队会先同步阻塞约 1 秒再被 `HostStateUnavailable` 拒掉 - 实施计划补一行说明,与 ADR §6"入队不取整轮持有的锁"对齐 - 测试:新增用例"整包重生成在跑(锁被占)时不需要恢复的入队路径不取锁",撤掉修复会红
This commit is contained in:
@@ -1072,8 +1072,19 @@ pub(in crate::agent) fn direct_taonier_regeneration_workflow_requires_recovery_a
|
||||
pub(in crate::agent) fn recover_direct_taonier_regeneration_workflow_at(
|
||||
root: &Path,
|
||||
) -> Result<bool, String> {
|
||||
// 锁只在**真的需要恢复**时才取:正在跑的那一轮整包重生成会整轮持有 `direct-codex-art`,入队
|
||||
// 命令若无条件去取它,整包重生成期间每条消息都要先在 worker 上同步阻塞约 1 秒(`..._with_wait`
|
||||
// 是 `std::thread::sleep`),然后被 `HostStateUnavailable` 拒掉——而队列本该收下它。
|
||||
// 判据本身是只读的:没有工作流 / 状态不需要恢复时,这条路一个字都不写。
|
||||
if !direct_taonier_regeneration_workflow_requires_recovery_at(root)? {
|
||||
return Ok(false);
|
||||
}
|
||||
let _executor_lock =
|
||||
acquire_game_creator_agent_runtime_task_lock_with_wait(root, "direct-codex-art")?;
|
||||
// 判据以**锁下那一份**为准:两次读之间真的重生成可能已经推进,或者别的进程已经恢复完了。
|
||||
if !direct_taonier_regeneration_workflow_requires_recovery_at(root)? {
|
||||
return Ok(false);
|
||||
}
|
||||
let Some(workflow) = read_direct_taonier_regeneration_workflow_at(root)? else {
|
||||
return Ok(false);
|
||||
};
|
||||
@@ -5632,6 +5643,85 @@ mod tests {
|
||||
}
|
||||
|
||||
/// 没有任何活动回合时,兜底释放给出可读原因,而不是静默成功。
|
||||
/// 不需要恢复时**不取**整轮任务锁。
|
||||
///
|
||||
/// 正在跑的那一轮整包重生成会整轮持有 `direct-codex-art`;入队命令的第一步就是这里,若无条件
|
||||
/// 取锁,整包重生成期间每条消息都要先在 worker 上同步阻塞约 1 秒,再被 `HostStateUnavailable`
|
||||
/// 拒掉——队列本该收下它。判据本身只读,不需要恢复就一个字节都不写、一把锁都不取。
|
||||
#[test]
|
||||
fn recovery_skips_the_executor_task_lock_when_nothing_needs_recovery() {
|
||||
let root = tempfile::tempdir().expect("recovery lock probe");
|
||||
init_local_game_project_at(root.path(), "direct-recovery-lock-probe", "恢复不取锁")
|
||||
.expect("init project");
|
||||
register_direct_taonier_art_package_fixture(root.path());
|
||||
// 有人正持有这一轮的任务锁:等价于"整包重生成正在跑"。
|
||||
let held =
|
||||
try_acquire_game_creator_agent_runtime_task_lock(root.path(), "direct-codex-art")
|
||||
.expect("probe the art task lock")
|
||||
.expect("hold the art task lock");
|
||||
|
||||
// ① 完全没有工作流:不需要恢复,不去取锁。
|
||||
assert!(
|
||||
!recover_direct_taonier_regeneration_workflow_at(root.path())
|
||||
.expect("an absent workflow needs no recovery")
|
||||
);
|
||||
|
||||
// ② 正在正常进行中的整包重生成(InProgress、没有待恢复的替换锚点):这正是"整包重生成在跑"
|
||||
// 的那一刻——不需要恢复,更**不该去取**那把整轮持有的锁。
|
||||
let in_progress = {
|
||||
let _project_lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||||
root.path(),
|
||||
"direct-codex.test-in-progress-lock-probe",
|
||||
)
|
||||
.expect("project write lock");
|
||||
new_direct_taonier_regeneration_workflow_locked(
|
||||
root.path(),
|
||||
&direct_taonier_regeneration_intent_sha256("正在跑的一套美术"),
|
||||
&direct_taonier_regeneration_invocation_sha256("client-turn-in-progress-probe"),
|
||||
DirectTaonierRegenerationWorkflowState::InProgress,
|
||||
)
|
||||
.expect("build in-progress workflow")
|
||||
};
|
||||
write_direct_taonier_regeneration_workflow_at(root.path(), &in_progress)
|
||||
.expect("persist the in-progress workflow");
|
||||
assert!(
|
||||
!direct_taonier_regeneration_workflow_requires_recovery_at(root.path())
|
||||
.expect("an in-progress workflow is discoverable")
|
||||
);
|
||||
assert!(
|
||||
!recover_direct_taonier_regeneration_workflow_at(root.path())
|
||||
.expect("a running regeneration needs no recovery"),
|
||||
"整包重生成在跑时入队不该去取整轮持有的锁"
|
||||
);
|
||||
|
||||
// ③ 真需要恢复(Resetting)时仍然要取锁:拿不到就如实报错,绝不当成"恢复过了"。
|
||||
let resetting = DirectTaonierRegenerationWorkflow {
|
||||
state: DirectTaonierRegenerationWorkflowState::Resetting,
|
||||
..in_progress
|
||||
};
|
||||
write_direct_taonier_regeneration_workflow_at(root.path(), &resetting)
|
||||
.expect("persist the resetting workflow");
|
||||
assert!(
|
||||
direct_taonier_regeneration_workflow_requires_recovery_at(root.path())
|
||||
.expect("a resetting workflow is discoverable")
|
||||
);
|
||||
let blocked = recover_direct_taonier_regeneration_workflow_at(root.path())
|
||||
.expect_err("a needed recovery must still take the task lock");
|
||||
assert!(blocked.contains("direct-codex-art"), "{blocked}");
|
||||
|
||||
// ④ 锁一放开照旧恢复。
|
||||
drop(held);
|
||||
assert!(recover_direct_taonier_regeneration_workflow_at(root.path())
|
||||
.expect("resetting recovery resumes once the lock is free"));
|
||||
assert_eq!(
|
||||
read_direct_taonier_regeneration_workflow_at(root.path())
|
||||
.expect("read recovered workflow")
|
||||
.expect("recovered workflow remains")
|
||||
.state,
|
||||
DirectTaonierRegenerationWorkflowState::InProgress
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_turn_release_without_an_active_turn_says_so() {
|
||||
let root = tempfile::tempdir().expect("empty turn root");
|
||||
|
||||
@@ -67,6 +67,8 @@
|
||||
**入队侧**:`clientTurnId` 校验 → 工作流恢复 → 用户条目校验 → prompt 投影 → 前置条件 → 容量预判 → 工程准备 → 入队;
|
||||
任何一步失败返回 typed 入队失败。容量预判只是一次提前的快速失败,权威判据仍在入队临界区里(工程准备是分钟级、会
|
||||
在磁盘留产物的活,满了就不该先做它)。**放行侧**(第 2 步)从占用登记起。
|
||||
其中"工作流恢复"只在**真的需要恢复**时才去取整轮任务锁(`direct-codex-art`):正在跑的那一轮整包重生成整轮持有
|
||||
它,入队无条件取锁会先同步阻塞约 1 秒再被拒(见 ADR §6"入队不取整轮持有的锁")。
|
||||
- 队列条目(宿主侧产物,只在内存):`PendingDirectTurn { client_turn_id, user_item: Value, prompt: String, creation_type: Option<String>, at: u64 }`。
|
||||
`prompt` 与 `creation_type` 是入队检查的产物,放行不再重算;事件里**不带** `prompt`。
|
||||
- `agent/direct_thread_manager.rs`:
|
||||
|
||||
Reference in New Issue
Block a user