优化: 边界加固(AI loop竞态根治+数据/审批/反馈/并发/错误分类)
AI loop 竞态(P0):per-conv epoch/owner token + 存活心跳治 force_send 双loop + stop 3s兜底误判;旧loop stale 全跳过(guard/emit/save)
agentic 收尾(A2-B8):Fatal 退出落库user消息(镜像Exhausted)+ 入口早退补save + usage is_estimated 打标 + emit_ai_completed_once 单点收敛清审批残留
聊天清理(A2-B9):clearChat 先停loop→DB单事务→内存清(clear_conversation_atomic)+ 前端错误气泡
循环并发(A2-B11):三态 ProviderAcquire(NotConfigured/Acquired/Exhausted)+ 候选循环非阻塞+防抖3次饱和降级+单测
错误分类(A2-B12):stream error帧接入 classify_status_or_class + 关键词保守降级 + 7单测
数据(G1.2/G1.4):purge_with_descendants 级联补全(11表单事务+存在性守卫)+ move_task_queue 单事务收口(两调用方共用)
git只读(G3.1):run_git_status/diff/log success判定(exit_code差异语义,失败结构化{success:false,error})
安全(G5.2/G5.6):create_project 目录Err+name校验 + module.rs 路径遍历DRY(分段匹配修a..b.rs误伤)
幂等(V2/V32):裸ALTER全守卫化 + v1..v40全链重跑幂等测试(16过)
附:remote_bridge await 临时引用修(E0716)+ agentic emit 收敛 E0716 app_state 绑定修
This commit is contained in:
@@ -34,9 +34,12 @@ use tokio::sync::{Mutex, Semaphore};
|
||||
///
|
||||
/// ## F-260614-04: per-provider 层(可选)
|
||||
/// `per_provider` 为 HashMap<provider_id, Arc<Semaphore>>。调用方(agentic loop)经
|
||||
/// `acquire_for_provider(pid)` 取额外 permit,防单 provider 被打满(限流 429)。
|
||||
/// `try_acquire_for_provider(pid)` 取额外 permit,防单 provider 被打满(限流 429)。
|
||||
/// **单 provider 场景**:若未调 `set_provider_caps`,HashMap 为空,
|
||||
/// `acquire_for_provider` 返回 None(无限流,行为同 F-01 前)。零变化保证。
|
||||
/// `try_acquire_for_provider` 返 `NotConfigured`(无限流,行为同 F-01 前)。零变化保证。
|
||||
/// **三态语义(G4.1,2026-08-05)**:`NotConfigured`(未配置无限流) / `Acquired(permit)`
|
||||
/// (拿到) / `Exhausted`(占满,非阻塞跳下一 candidate)。修复旧阻塞 `acquire_for_provider`
|
||||
/// 在主 provider 信号量占满时整条 fallback 链卡死(降级失效)。
|
||||
/// 全局容量 = min(sum(各 provider 上限), global_cap):由调用方在配置时约束
|
||||
/// (set_provider_caps 传 min(sum, global_cap)),非运行时强约束。
|
||||
///
|
||||
@@ -87,6 +90,26 @@ pub struct LlmConcurrency {
|
||||
per_sub_permits: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
/// F-260614-04(2026-08-05 G4.1 裁决): per-provider 限流 acquire 的**显式三态**结果。
|
||||
///
|
||||
/// **None 双关陷阱(本枚举的动机)**:旧 `acquire_for_provider` 用 `Option` 返回值,
|
||||
/// `None` 既表示「未配置无限流」又可能被 naive 调用方误读为「耗尽」——耗尽语义下
|
||||
/// naive 用 None 表耗尽会静默绕过限流打爆 429。三态把两种语义显式拆分:
|
||||
///
|
||||
/// - `NotConfigured`:未配置(per_provider 表无该 provider → 无限流,permits 跳过不落表)。
|
||||
/// - `Acquired(permit)`:成功拿到 permit(绑 Semaphore,Drop 自动释放)。
|
||||
/// - `Exhausted`:信号量占满(非阻塞 try_acquire 失败),调用方跳下一 candidate。
|
||||
pub enum ProviderAcquire {
|
||||
/// 未配置(per_provider 表无该 provider → 无限流,permits 跳过不落表)。
|
||||
/// 与耗尽语义完全区分:调用方无 permit 直接 proceed(单 provider 零变化)。
|
||||
NotConfigured,
|
||||
/// 成功拿到 permit(绑 Semaphore,Drop 自动释放)。
|
||||
Acquired(tokio::sync::OwnedSemaphorePermit),
|
||||
/// 该 provider 信号量占满(try_acquire_owned 失败,非阻塞)。
|
||||
/// 调用方跳下一 candidate(候选链降级),不阻塞等待。
|
||||
Exhausted,
|
||||
}
|
||||
|
||||
impl LlmConcurrency {
|
||||
pub fn new(global: usize, per_conv: usize) -> Self {
|
||||
Self {
|
||||
@@ -239,33 +262,48 @@ impl LlmConcurrency {
|
||||
self.per_sub_flow.lock().await.clear();
|
||||
}
|
||||
|
||||
/// F-260614-04: 取 per-provider 并发 permit(可选)。
|
||||
/// F-260614-04(2026-08-05 G4.1 裁决): per-provider 并发 permit 的**非阻塞三态**版本。
|
||||
///
|
||||
/// - provider 在 `per_provider` 表中有配置 → 取其 Semaphore permit,返回 Some。
|
||||
/// - provider 无配置(单 provider 场景或未 set_provider_caps)→ 返回 None,无限流。
|
||||
/// **背景**:旧 `acquire_for_provider` 用阻塞 `acquire_owned()`,主 provider 信号量占满时
|
||||
/// 整条 fallback 候选链卡死(降级失效)。compress/title 已改 try_acquire,候选链是唯一漏网。
|
||||
/// 本方法用 `try_acquire_owned`(非阻塞),三态返回见 [`ProviderAcquire`]:
|
||||
///
|
||||
/// 调用方(agentic loop)用法:
|
||||
/// - `NotConfigured`:per_provider 表无该 provider(未配置 → 无限流,单 provider 零变化)。
|
||||
/// - `Acquired(permit)`:成功拿到,permit 随 Drop 自动释放。
|
||||
/// - `Exhausted`:信号量占满,调用方跳下一 candidate(候选链降级),不阻塞。
|
||||
///
|
||||
/// **仅 per-provider 层非阻塞**:global/per_conv 仍由 `acquire_global`/`acquire_per_conv`
|
||||
/// 阻塞限流(调用方各自持有),不受本方法影响。
|
||||
///
|
||||
/// 锁持有短(不含 try_acquire),对齐 `acquire_per_sub_flow` 模式。
|
||||
///
|
||||
/// **调用方用法**(agentic 候选循环):
|
||||
/// ```ignore
|
||||
/// let _global_permit = llm_concurrency.acquire_global().await;
|
||||
/// let _per_conv_permit = llm_concurrency.acquire_per_conv(&conv_id).await;
|
||||
/// let _provider_permit = llm_concurrency.acquire_for_provider(&provider_id).await;
|
||||
/// match llm_concurrency.try_acquire_for_provider(&candidate.id).await {
|
||||
/// ProviderAcquire::NotConfigured => { /* 无 permit 直接 proceed */ }
|
||||
/// ProviderAcquire::Acquired(permit) => { /* 持 permit stream,Drop 自动释放 */ }
|
||||
/// ProviderAcquire::Exhausted => { /* 跳下一 candidate */ }
|
||||
/// }
|
||||
/// ```
|
||||
/// 三 permit 均绑 guard Drop 自动释放。None 时无 permit 需释放(行为同 F-01 前)。
|
||||
pub async fn acquire_for_provider(
|
||||
pub async fn try_acquire_for_provider(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
) -> Option<tokio::sync::OwnedSemaphorePermit> {
|
||||
) -> ProviderAcquire {
|
||||
let sema = {
|
||||
let map = self.per_provider.lock().await;
|
||||
map.get(provider_id).cloned()
|
||||
}?;
|
||||
// Semaphore 存在 → acquire。expect 同 acquire_global/per_conv:Semaphore 不会 close
|
||||
// (无 close() 调用,仅在 set_provider_caps 时替换表内 Arc,旧 Arc permit 仍有效)。
|
||||
Some(
|
||||
sema.acquire_owned()
|
||||
.await
|
||||
.expect("llm per_provider semaphore closed"),
|
||||
)
|
||||
};
|
||||
match sema {
|
||||
// 无配置:无限流(permits 跳过不落表),显式区分于耗尽。
|
||||
None => ProviderAcquire::NotConfigured,
|
||||
Some(sema) => match sema.try_acquire_owned() {
|
||||
// 成功拿到:permit 绑 Semaphore,Drop 自动释放。
|
||||
Ok(permit) => ProviderAcquire::Acquired(permit),
|
||||
// 占满:非阻塞返 Exhausted,调用方跳下一 candidate(降级)。
|
||||
// try_acquire_owned 不会 Err 于 closed(Semaphore 无 close() 调用)。
|
||||
Err(_) => ProviderAcquire::Exhausted,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// F-260614-04: 批量设置 per-provider 并发上限(替换整表)。
|
||||
@@ -280,10 +318,107 @@ impl LlmConcurrency {
|
||||
map.clear();
|
||||
for (pid, permits) in caps {
|
||||
// permits=0 等同无配置(Semaphore::new(0) 永远 acquire 不到 → 死锁),
|
||||
// 故 permits=0 跳过(不落表 → acquire_for_provider 返 None → 无限流)。
|
||||
// 故 permits=0 跳过(不落表 → try_acquire_for_provider 返 NotConfigured → 无限流)。
|
||||
if permits > 0 {
|
||||
map.insert(pid, Arc::new(Semaphore::new(permits)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// ============================================================
|
||||
// F-260614-04(2026-08-05 G4.1): try_acquire_for_provider 三态测试
|
||||
// NotConfigured / Acquired / Exhausted 显式区分(修 None 双关:None='未配置无限流',
|
||||
// naive 用 None 表耗尽会静默绕过限流)。仅验证 per-provider 层三态语义,
|
||||
// global/per_conv 仍阻塞限流(不经本方法)。
|
||||
// ============================================================
|
||||
|
||||
/// 未配置(空 per_provider 表)→ NotConfigured:无限流,单 provider 零变化。
|
||||
#[tokio::test]
|
||||
async fn try_acquire_for_provider_not_configured() {
|
||||
let lc = LlmConcurrency::new(3, 2);
|
||||
let r = lc.try_acquire_for_provider("p1").await;
|
||||
assert!(matches!(r, ProviderAcquire::NotConfigured));
|
||||
}
|
||||
|
||||
/// 配置且有空闲 permit → Acquired(permit 绑 Semaphore,Drop 自动释放)。
|
||||
#[tokio::test]
|
||||
async fn try_acquire_for_provider_acquired() {
|
||||
let lc = LlmConcurrency::new(3, 2);
|
||||
let mut caps = HashMap::new();
|
||||
caps.insert("p1".to_string(), 2usize);
|
||||
lc.set_provider_caps(caps).await;
|
||||
|
||||
let r = lc.try_acquire_for_provider("p1").await;
|
||||
let permit = match r {
|
||||
ProviderAcquire::Acquired(p) => p,
|
||||
other => panic!("配置后应返回 Acquired,实际 {:?}", discriminant(&other)),
|
||||
};
|
||||
// Drop 释放后仍可再拿(permit 绑旧 Semaphore Arc,set 后计数守恒)。
|
||||
drop(permit);
|
||||
let r2 = lc.try_acquire_for_provider("p1").await;
|
||||
assert!(matches!(r2, ProviderAcquire::Acquired(_)));
|
||||
}
|
||||
|
||||
/// 配置但信号量占满 → Exhausted(非阻塞,不等待)。
|
||||
#[tokio::test]
|
||||
async fn try_acquire_for_provider_exhausted() {
|
||||
let lc = LlmConcurrency::new(3, 2);
|
||||
let mut caps = HashMap::new();
|
||||
caps.insert("p1".to_string(), 1usize); // 仅 1 permit
|
||||
lc.set_provider_caps(caps).await;
|
||||
|
||||
// 第一次拿到(permit 仍持于作用域内,不 Drop)。
|
||||
let first = lc.try_acquire_for_provider("p1").await;
|
||||
assert!(matches!(first, ProviderAcquire::Acquired(_)));
|
||||
// 第二次耗尽:信号量满,非阻塞返 Exhausted。
|
||||
let second = lc.try_acquire_for_provider("p1").await;
|
||||
assert!(matches!(second, ProviderAcquire::Exhausted));
|
||||
|
||||
// Drop 首 permit 后恢复可再拿。
|
||||
drop(first);
|
||||
let third = lc.try_acquire_for_provider("p1").await;
|
||||
assert!(matches!(third, ProviderAcquire::Acquired(_)));
|
||||
}
|
||||
|
||||
/// 配置后清空(传空 map)→ 回退 NotConfigured(permits 跳过不落表)。
|
||||
#[tokio::test]
|
||||
async fn try_acquire_for_provider_not_configured_after_caps_cleared() {
|
||||
let lc = LlmConcurrency::new(3, 2);
|
||||
let mut caps = HashMap::new();
|
||||
caps.insert("p1".to_string(), 1usize);
|
||||
lc.set_provider_caps(caps).await;
|
||||
lc.set_provider_caps(HashMap::new()).await; // 清空整表
|
||||
|
||||
let r = lc.try_acquire_for_provider("p1").await;
|
||||
assert!(matches!(r, ProviderAcquire::NotConfigured));
|
||||
}
|
||||
|
||||
/// 配置了 p1,未配置 p2 → p1 Acquired / p2 NotConfigured(按 provider 区分,不互相污染)。
|
||||
#[tokio::test]
|
||||
async fn try_acquire_for_provider_distinct_providers() {
|
||||
let lc = LlmConcurrency::new(3, 2);
|
||||
let mut caps = HashMap::new();
|
||||
caps.insert("p1".to_string(), 1usize);
|
||||
lc.set_provider_caps(caps).await;
|
||||
|
||||
// p1 配置 → Acquired;p2 未配置 → NotConfigured。
|
||||
assert!(matches!(lc.try_acquire_for_provider("p1").await, ProviderAcquire::Acquired(_)));
|
||||
assert!(matches!(lc.try_acquire_for_provider("p2").await, ProviderAcquire::NotConfigured));
|
||||
// p1 仅 1 permit 已满 → p2 不受影响。
|
||||
assert!(matches!(lc.try_acquire_for_provider("p2").await, ProviderAcquire::NotConfigured));
|
||||
}
|
||||
|
||||
/// 辅助:仅用于 panic 信息里给枚举变体一个 Debug 标签(无需求时保持简单)。
|
||||
fn discriminant(r: &ProviderAcquire) -> &'static str {
|
||||
match r {
|
||||
ProviderAcquire::NotConfigured => "NotConfigured",
|
||||
ProviderAcquire::Acquired(_) => "Acquired",
|
||||
ProviderAcquire::Exhausted => "Exhausted",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user