ProcessManager容量与回收
Unified Exec 的进程 store 不是无限增长的 hashmap。manager 把 64 条目作为软上限,优先回收保护集之外的已退出进程;如果候选正在发布终态,则宁可暂时超限,也不误杀活跃进程。回收还必须结束网络审批、终止底层进程并清理 ID,同时不能抢走 exit watcher 持有的 plugin metrics 收尾资源。
本文承接Unified Exec数据结构和Unified Exec会话状态机,专门分析 ProcessManager 的资源治理,不重复字段定义。读完后,你应能根据 last_used、退出状态和 interaction_lock 预测候选条目,并解释单进程终止为何采用两阶段校验。
1. 软上限
1.1 64个条目
插入新 ProcessEntry 前,manager 调用 prune_processes_if_needed。数量小于 MAX_UNIFIED_EXEC_PROCESSES 时直接返回,不做扫描。
源码位置:codex-rs/core/src/unified_exec/process_manager.rs :: prune_processes_if_needed
fn prune_processes_if_needed(store: &mut ProcessStore) -> Option<ProcessEntry> {
if store.processes.len() < MAX_UNIFIED_EXEC_PROCESSES {
return None;
}
let mut meta: Vec<(i32, Instant, bool)> = store
.processes
.iter()
.map(|(id, entry)| (*id, entry.last_used, entry.process.has_exited()))
.collect();
// choose a removable entry...
}这是软上限而非硬断言:锁住的已退出条目可能使 store 暂时保持 64 个以上,算法不会因此立即驱逐 live process。
1.2 最近八个保护
算法按 last_used 从新到旧排序,保护最近 8 个 ID;即使保护集中的条目已经退出,也不会优先回收它。
源码位置:codex-rs/core/src/unified_exec/process_manager.rs :: process_id_to_prune_from_meta
let mut by_recency = meta.to_vec();
by_recency.sort_by_key(|(_, last_used, _)| Reverse(*last_used));
let protected: HashSet<i32> = by_recency
.iter()
.take(8)
.map(|(process_id, _, _)| *process_id)
.collect();保护集减少刚被用户交互过的会话被马上回收的概率,但不表示这些会话永久保留。
2. 候选顺序
2.1 已退出优先
保护集之外,算法先找 exited == true 的最久未使用条目;没有已退出条目时才回退到保护集外的 LRU。
源码位置:codex-rs/core/src/unified_exec/process_manager.rs :: process_id_to_prune_from_meta
let mut lru = meta.to_vec();
lru.sort_by_key(|(_, last_used, _)| *last_used);
if let Some((process_id, _, _)) = lru
.iter()
.find(|(process_id, _, exited)| !protected.contains(process_id) && *exited)
{
return Some(*process_id);
}
lru.into_iter()
.find(|(process_id, _, _)| !protected.contains(process_id))
.map(|(process_id, _, _)| process_id)退出优先减少终止活进程的代价;但当所有条目都活跃时,容量治理仍会回收最久未使用者。
2.2 交互锁竞争
候选选出后还要尝试取得 interaction_lock。锁被 write_stdin 或终态事件占用时,候选会被跳过。
源码位置:codex-rs/core/src/unified_exec/process_manager.rs :: prune_processes_if_needed
if let Some(interaction_lock) = candidate_process
.as_ref()
.map(|process| process.interaction_lock())
&& let Ok(_interaction_guard) = interaction_lock.try_lock_owned()
{
return store.remove(process_id);
}
found_locked_exited_process |= candidate_has_exited
|| candidate_process.is_some_and(|process| process.has_exited());
meta.retain(|(id, _, _)| *id != process_id);如果已退出候选被锁住,后续发现活跃候选时函数会返回 None,避免用活进程填补暂时不可回收的退出条目。
3. 锁外清理
3.1 插入与清理
store_process 在持有 store 锁时完成候选移出和新条目插入;释放锁后才注销网络审批并终止被裁剪进程。
源码位置:codex-rs/core/src/unified_exec/process_manager.rs :: store_process
let pruned_entry = {
let mut store = self.process_store.lock().await;
let pruned_entry = Self::prune_processes_if_needed(&mut store);
store.processes.insert(process_id, entry);
pruned_entry
};
if let Some(pruned_entry) = pruned_entry {
unregister_network_approval_for_entry(&pruned_entry).await;
pruned_entry.process.terminate();
}锁外 await 是必要的:网络审批注销可能调用 session service,进程终止也可能等待 backend;把它们放在 store 锁内会阻塞所有 ID 操作。
ProcessEntry 还保存 Option<SharedPluginMetricsSidecar>。store 移除条目只释放这一份 Arc;spawn_exit_watcher 在创建时已经取得另一份共享所有权。进程 terminate 后,watcher 仍能等待输出 drain,并通过 Option::take 唯一消费 sidecar。prune 路径因此不在这里直接完成插件测量。
源码位置:
codex-rs/core/src/unified_exec/mod.rs :: ProcessEntry、SharedPluginMetricsSidecarcodex-rs/core/src/unified_exec/process_manager.rs :: UnifiedExecProcessManager::store_processcodex-rs/core/src/unified_exec/async_watcher.rs :: spawn_exit_watcher
3.2 全部终止
session 关闭时,manager drain 整个 map、清空 reserved IDs,再逐条注销审批和终止进程。先清空索引,后执行慢清理,保证关闭期间不会再找到旧条目。
源码位置:codex-rs/core/src/unified_exec/process_manager.rs :: terminate_all_processes
let entries: Vec<ProcessEntry> = {
let mut processes = self.process_store.lock().await;
let entries = processes.processes.drain().map(|(_, entry)| entry).collect();
processes.reserved_process_ids.clear();
entries
};
for entry in entries {
unregister_network_approval_for_entry(&entry).await;
entry.process.terminate();
}4. 单进程终止
4.1 两阶段校验
terminate_process 先复制进程 Arc 并释放 store 锁,锁外等待 terminate_confirmed;随后重新加锁,确认 map 中仍是同一个 Arc,再决定是否移除。
源码位置:codex-rs/core/src/unified_exec/process_manager.rs :: terminate_process
let (process, already_exited) = {
let store = self.process_store.lock().await;
let Some(entry) = store.processes.get(&process_id) else {
return false;
};
(Arc::clone(&entry.process), entry.process.has_exited())
};
if !already_exited && process.terminate_confirmed().await.is_err() {
return false;
}4.2 防止误删
重新加锁后的 Arc::ptr_eq 防止相同数字 ID 已被替换后误删新进程;首轮响应保护位为真时保留条目。
源码位置:codex-rs/core/src/unified_exec/process_manager.rs :: terminate_process
let entry = {
let mut store = self.process_store.lock().await;
let Some(entry) = store.processes.get(&process_id) else {
return true;
};
if !Arc::ptr_eq(&entry.process, &process) {
return true;
}
if entry.initial_exec_command_active.load(Ordering::Acquire) {
return true;
}
let Some(entry) = store.remove(process_id) else {
return false;
};
entry
};
unregister_network_approval_for_entry(&entry).await;
true5. 两个验证
pruning_prefers_exited_processes_outside_recently_used、pruning_falls_back_to_lru_when_no_exited 和 pruning_protects_recent_processes_even_if_exited 分别证明已退出优先、无退出时 LRU、最近 8 个保护。pruning_does_not_evict_live_process_while_exited_process_is_finalizing 持有退出进程锁,断言活进程不被误杀。
源码位置:codex-rs/core/src/unified_exec/process_manager_tests.rs :: pruning_prefers_exited_processes_outside_recently_used、pruning_does_not_evict_live_process_while_exited_process_is_finalizing
cd codex-rs
cargo test -p codex-core --lib 'unified_exec::process_manager::tests::pruning_' -- --test-threads=1这些测试证明候选选择和锁保护,不证明真实 OS 进程的终止延迟或网络审批服务的外部行为。
6. 源码排查
rg -n "MAX_UNIFIED_EXEC_PROCESSES|prune_processes_if_needed|process_id_to_prune" codex-rs/core/src/unified_exec
rg -n "store_process|terminate_all_processes|terminate_process" codex-rs/core/src/unified_exec/process_manager.rs
rg -n "pruning_prefers|pruning_falls_back|pruning_protects|does_not_evict" codex-rs/core/src/unified_exec/process_manager_tests.rs核心不变量是:容量治理优先回收已退出且不在最近保护集的条目;回收不能与同会话交互重叠;所有慢清理在释放 store 锁后进行;两阶段终止必须防止旧 Arc 误删新 ID。
