904 lines
30 KiB
Rust
904 lines
30 KiB
Rust
//! 并发和 RAII 设计测试
|
||
//!
|
||
//! 验证以下功能:
|
||
//! 1. 并发独立启动 agent,agent 之间互不影响
|
||
//! 2. agent 销毁的正确性
|
||
//! 3. RAII 设计(PendingGuard)是否可以正常快速销毁 agent
|
||
//! 4. 原子计数器的并发安全性
|
||
|
||
use std::sync::Arc;
|
||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||
use std::time::Duration;
|
||
use tokio::sync::Barrier;
|
||
|
||
// 重新导出必要的类型
|
||
use agent_runner::agent_runtime::{get_concurrency_limit, init_concurrency_limit};
|
||
use agent_runner::service::AgentSessionRegistry;
|
||
use agent_runner::service::PendingGuard;
|
||
use agent_client_protocol::schema::SessionId;
|
||
use shared_types::{AgentStatus, ProjectAndAgentInfo, SessionEntry};
|
||
use tokio::sync::mpsc;
|
||
|
||
// ============================================================================
|
||
// 1. PendingGuard RAII 测试
|
||
// ============================================================================
|
||
|
||
#[test]
|
||
fn test_pending_guard_auto_cleanup_on_drop() {
|
||
let registry = AgentSessionRegistry::new();
|
||
|
||
// 设置 Pending 状态
|
||
{
|
||
let _guard = PendingGuard::new(®istry, "test-project");
|
||
// 验证 Pending 状态已设置
|
||
assert!(registry.contains_project("test-project"));
|
||
let info = registry.get_agent_info("test-project").unwrap();
|
||
assert_eq!(format!("{:?}", info.status), "Pending");
|
||
}
|
||
// guard 已 drop,Pending 状态应该被清理
|
||
assert!(!registry.contains_project("test-project"));
|
||
}
|
||
|
||
#[test]
|
||
fn test_pending_guard_commit_success_prevents_cleanup() {
|
||
let registry = AgentSessionRegistry::new();
|
||
|
||
{
|
||
let guard = PendingGuard::new(®istry, "test-project");
|
||
|
||
// 验证 Pending 状态已设置
|
||
assert!(registry.contains_project("test-project"));
|
||
|
||
// 提交成功,防止清理
|
||
guard.commit_success();
|
||
}
|
||
|
||
// Pending 状态应该保留
|
||
assert!(registry.contains_project("test-project"));
|
||
let info = registry.get_agent_info("test-project").unwrap();
|
||
assert_eq!(format!("{:?}", info.status), "Pending");
|
||
|
||
// 清理
|
||
registry.remove_by_project("test-project");
|
||
}
|
||
|
||
#[test]
|
||
fn test_pending_guard_early_return_cleanup() {
|
||
let registry = AgentSessionRegistry::new();
|
||
|
||
// 模拟早期返回场景(使用 return 代替 panic,因为 DashMap 不支持 catch_unwind)
|
||
let early_return = || {
|
||
let _guard = PendingGuard::new(®istry, "test-project");
|
||
// 早期返回(模拟错误场景)
|
||
return false;
|
||
};
|
||
|
||
// 调用后,guard 已经被 drop,应该被清理
|
||
early_return();
|
||
|
||
assert!(!registry.contains_project("test-project"));
|
||
}
|
||
|
||
// ============================================================================
|
||
// 2. 原子计数器并发安全性测试
|
||
// ============================================================================
|
||
|
||
#[tokio::test]
|
||
async fn test_atomic_slot_counter_concurrent_acquisition() {
|
||
// 重置并发限制为默认值,防止其他测试影响
|
||
init_concurrency_limit(10);
|
||
let registry = Arc::new(AgentSessionRegistry::new());
|
||
let limit = get_concurrency_limit();
|
||
let num_tasks = limit * 2;
|
||
let barrier = Arc::new(Barrier::new(num_tasks));
|
||
let successful_count = Arc::new(AtomicUsize::new(0));
|
||
let failed_count = Arc::new(AtomicUsize::new(0));
|
||
|
||
let mut handles = vec![];
|
||
|
||
// 启动并发任务尝试获取槽位
|
||
for i in 0..num_tasks {
|
||
let registry_clone = registry.clone();
|
||
let barrier_clone = barrier.clone();
|
||
let successful_count_clone = successful_count.clone();
|
||
let failed_count_clone = failed_count.clone();
|
||
|
||
let handle = tokio::spawn(async move {
|
||
// 等待所有任务就绪
|
||
barrier_clone.wait().await;
|
||
|
||
// 尝试获取槽位
|
||
if registry_clone.try_acquire_session_slot() {
|
||
successful_count_clone.fetch_add(1, Ordering::Relaxed);
|
||
// 模拟工作
|
||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||
registry_clone.release_session_slot();
|
||
} else {
|
||
failed_count_clone.fetch_add(1, Ordering::Relaxed);
|
||
}
|
||
});
|
||
|
||
handles.push(handle);
|
||
}
|
||
|
||
// 等待所有任务完成
|
||
for handle in handles {
|
||
handle.await.unwrap();
|
||
}
|
||
|
||
// 验证: 只有 WORKER_THREAD_POOL_SIZE 个任务成功
|
||
let successful = successful_count.load(Ordering::Relaxed);
|
||
let failed = failed_count.load(Ordering::Relaxed);
|
||
let limit = get_concurrency_limit();
|
||
|
||
assert_eq!(successful, limit, "应该有 {} 个任务成功获取槽位", limit);
|
||
assert_eq!(failed, limit, "应该有 {} 个任务失败(槽位已满)", limit);
|
||
|
||
// 验证: 计数器最终应该回到 0
|
||
assert_eq!(registry.active_sessions_count(), 0, "所有槽位应该被释放");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_atomic_slot_counter_stress_test() {
|
||
let registry = Arc::new(AgentSessionRegistry::new());
|
||
let num_iterations = 1000;
|
||
let successful_count = Arc::new(AtomicUsize::new(0));
|
||
|
||
let mut handles = vec![];
|
||
|
||
// 启动大量并发任务
|
||
for _ in 0..50 {
|
||
let registry_clone = registry.clone();
|
||
let successful_count_clone = successful_count.clone();
|
||
|
||
let handle = tokio::spawn(async move {
|
||
for j in 0..num_iterations {
|
||
if registry_clone.try_acquire_session_slot() {
|
||
successful_count_clone.fetch_add(1, Ordering::Relaxed);
|
||
// 使用简单的随机性(不依赖 rand)
|
||
let delay = (j % 10) as u64;
|
||
tokio::time::sleep(Duration::from_micros(delay * 10)).await;
|
||
registry_clone.release_session_slot();
|
||
}
|
||
}
|
||
});
|
||
|
||
handles.push(handle);
|
||
}
|
||
|
||
// 等待所有任务完成
|
||
for handle in handles {
|
||
handle.await.unwrap();
|
||
}
|
||
|
||
// 验证: 计数器最终应该回到 0
|
||
assert_eq!(registry.active_sessions_count(), 0, "所有槽位应该被释放");
|
||
|
||
println!(
|
||
"压力测试完成: {} 次成功获取槽位",
|
||
successful_count.load(Ordering::Relaxed)
|
||
);
|
||
}
|
||
|
||
// ============================================================================
|
||
// 3. Agent 并发独立性测试
|
||
// ============================================================================
|
||
|
||
#[tokio::test]
|
||
async fn test_concurrent_agents_independence() {
|
||
let registry = Arc::new(AgentSessionRegistry::new());
|
||
let num_agents = 10;
|
||
let barrier = Arc::new(Barrier::new(num_agents));
|
||
let mut handles = vec![];
|
||
|
||
// 并发创建多个 agent
|
||
for i in 0..num_agents {
|
||
let registry_clone = registry.clone();
|
||
let barrier_clone = barrier.clone();
|
||
|
||
let handle = tokio::spawn(async move {
|
||
let project_id = format!("project-{}", i);
|
||
let session_id = format!("session-{}", i);
|
||
|
||
// 创建 AgentInfo
|
||
let (prompt_tx, _) = mpsc::channel(100);
|
||
let (cancel_tx, _) = mpsc::channel(100);
|
||
|
||
let agent_info = ProjectAndAgentInfo {
|
||
project_id: project_id.clone(),
|
||
session_id: SessionId::new(Arc::from(session_id.as_str())),
|
||
prompt_tx,
|
||
cancel_tx,
|
||
model_provider: None,
|
||
request_id: None,
|
||
status: AgentStatus::Active,
|
||
last_activity: chrono::Utc::now(),
|
||
created_at: chrono::Utc::now(),
|
||
stop_handle: None,
|
||
};
|
||
|
||
// 注册 agent
|
||
registry_clone.register(&project_id, &session_id, agent_info);
|
||
|
||
// 等待所有 agent 就绪
|
||
barrier_clone.wait().await;
|
||
|
||
// 验证: 当前 agent 存在
|
||
assert!(registry_clone.contains_project(&project_id));
|
||
|
||
// 验证: 其他 agent 也存在(不会相互覆盖)
|
||
let stats = registry_clone.stats();
|
||
assert_eq!(stats.agent_count, num_agents);
|
||
|
||
// 模拟工作
|
||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||
|
||
// 清理
|
||
registry_clone.remove_by_project(&project_id);
|
||
});
|
||
|
||
handles.push(handle);
|
||
}
|
||
|
||
// 等待所有任务完成
|
||
for handle in handles {
|
||
handle.await.unwrap();
|
||
}
|
||
|
||
// 验证: 所有 agent 已被清理
|
||
assert_eq!(registry.stats().agent_count, 0);
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_concurrent_agent_state_updates() {
|
||
let registry = Arc::new(AgentSessionRegistry::new());
|
||
let project_id = "test-project";
|
||
let session_id = "test-session";
|
||
|
||
// 创建初始 agent
|
||
let (prompt_tx, _) = mpsc::channel(100);
|
||
let (cancel_tx, _) = mpsc::channel(100);
|
||
|
||
let agent_info = ProjectAndAgentInfo {
|
||
project_id: project_id.to_string(),
|
||
session_id: SessionId::new(Arc::from(session_id)),
|
||
prompt_tx,
|
||
cancel_tx,
|
||
model_provider: None,
|
||
request_id: None,
|
||
status: AgentStatus::Idle,
|
||
last_activity: chrono::Utc::now(),
|
||
created_at: chrono::Utc::now(),
|
||
stop_handle: None,
|
||
};
|
||
|
||
registry.register(project_id, session_id, agent_info);
|
||
|
||
// 并发更新状态
|
||
let num_updates = 100;
|
||
let mut handles = vec![];
|
||
|
||
for i in 0..10 {
|
||
let registry_clone = registry.clone();
|
||
|
||
let handle = tokio::spawn(async move {
|
||
for j in 0..num_updates {
|
||
// 使用原子性更新
|
||
registry_clone.try_update_agent_info(project_id, |info| {
|
||
// 模拟状态切换
|
||
if j % 2 == 0 {
|
||
info.status = AgentStatus::Active;
|
||
} else {
|
||
info.status = AgentStatus::Idle;
|
||
}
|
||
info.last_activity = chrono::Utc::now();
|
||
true
|
||
});
|
||
tokio::time::sleep(Duration::from_micros(10)).await;
|
||
}
|
||
});
|
||
|
||
handles.push(handle);
|
||
}
|
||
|
||
// 等待所有更新完成
|
||
for handle in handles {
|
||
handle.await.unwrap();
|
||
}
|
||
|
||
// 验证: agent 仍然存在,没有数据损坏
|
||
assert!(registry.contains_project(project_id));
|
||
let info = registry.get_agent_info(project_id).unwrap();
|
||
assert!(matches!(
|
||
info.status,
|
||
AgentStatus::Active | AgentStatus::Idle
|
||
));
|
||
|
||
// 清理
|
||
registry.remove_by_project(project_id);
|
||
}
|
||
|
||
// ============================================================================
|
||
// 4. Agent 销毁测试
|
||
// ============================================================================
|
||
|
||
#[tokio::test]
|
||
async fn test_agent_lifecycle_cleanup() {
|
||
let registry = Arc::new(AgentSessionRegistry::new());
|
||
|
||
// 创建多个 agent
|
||
let num_agents = 5;
|
||
for i in 0..num_agents {
|
||
let project_id = format!("project-{}", i);
|
||
let session_id = format!("session-{}", i);
|
||
|
||
let (prompt_tx, _) = mpsc::channel(100);
|
||
let (cancel_tx, _) = mpsc::channel(100);
|
||
|
||
let agent_info = ProjectAndAgentInfo {
|
||
project_id: project_id.clone(),
|
||
session_id: SessionId::new(Arc::from(session_id.as_str())),
|
||
prompt_tx,
|
||
cancel_tx,
|
||
model_provider: None,
|
||
request_id: None,
|
||
status: AgentStatus::Active,
|
||
last_activity: chrono::Utc::now(),
|
||
created_at: chrono::Utc::now(),
|
||
stop_handle: None,
|
||
};
|
||
|
||
registry.register(&project_id, &session_id, agent_info);
|
||
}
|
||
|
||
// 验证: 所有 agent 已注册
|
||
assert_eq!(registry.stats().agent_count, num_agents);
|
||
|
||
// 销毁所有 agent
|
||
for i in 0..num_agents {
|
||
let project_id = format!("project-{}", i);
|
||
let removed = registry.remove_by_project(&project_id);
|
||
assert!(removed.is_some(), "应该能移除 agent");
|
||
}
|
||
|
||
// 验证: 所有 agent 已被清理
|
||
assert_eq!(registry.stats().agent_count, 0);
|
||
|
||
// 验证: 映射关系也被清理
|
||
for i in 0..num_agents {
|
||
let session_id = format!("session-{}", i);
|
||
assert!(!registry.contains_session(&session_id));
|
||
}
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_agent_concurrent_removal() {
|
||
let registry = Arc::new(AgentSessionRegistry::new());
|
||
|
||
// 创建大量 agent
|
||
let num_agents = 100;
|
||
for i in 0..num_agents {
|
||
let project_id = format!("project-{}", i);
|
||
let session_id = format!("session-{}", i);
|
||
|
||
let (prompt_tx, _) = mpsc::channel(100);
|
||
let (cancel_tx, _) = mpsc::channel(100);
|
||
|
||
let agent_info = ProjectAndAgentInfo {
|
||
project_id: project_id.clone(),
|
||
session_id: SessionId::new(Arc::from(session_id.as_str())),
|
||
prompt_tx,
|
||
cancel_tx,
|
||
model_provider: None,
|
||
request_id: None,
|
||
status: AgentStatus::Active,
|
||
last_activity: chrono::Utc::now(),
|
||
created_at: chrono::Utc::now(),
|
||
stop_handle: None,
|
||
};
|
||
|
||
registry.register(&project_id, &session_id, agent_info);
|
||
}
|
||
|
||
// 并发移除所有 agent
|
||
let mut handles = vec![];
|
||
for i in 0..num_agents {
|
||
let registry_clone = registry.clone();
|
||
let handle = tokio::spawn(async move {
|
||
let project_id = format!("project-{}", i);
|
||
registry_clone.remove_by_project(&project_id);
|
||
});
|
||
handles.push(handle);
|
||
}
|
||
|
||
// 等待所有移除完成
|
||
for handle in handles {
|
||
handle.await.unwrap();
|
||
}
|
||
|
||
// 验证: 所有 agent 已被清理
|
||
assert_eq!(registry.stats().agent_count, 0);
|
||
}
|
||
|
||
// ============================================================================
|
||
// 5. RAII 快速销毁测试
|
||
// ============================================================================
|
||
|
||
#[test]
|
||
fn test_raii_fast_destruction() {
|
||
let registry = AgentSessionRegistry::new();
|
||
let num_guards = 1000;
|
||
|
||
let start = std::time::Instant::now();
|
||
|
||
// 创建大量 guard
|
||
for i in 0..num_guards {
|
||
let project_id = format!("project-{}", i);
|
||
let _guard = PendingGuard::new(®istry, &project_id);
|
||
// guard 立即被 drop
|
||
}
|
||
|
||
let elapsed = start.elapsed();
|
||
|
||
// 验证: 销毁应该很快(< 10ms)
|
||
assert!(
|
||
elapsed.as_millis() < 10,
|
||
"RAII 销毁应该快速完成,实际耗时: {:?}",
|
||
elapsed
|
||
);
|
||
|
||
// 验证: 所有项目都被清理
|
||
assert_eq!(registry.stats().agent_count, 0);
|
||
|
||
println!(
|
||
"RAII 快速销毁测试: {} 个 guard 在 {:?} 内销毁",
|
||
num_guards, elapsed
|
||
);
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_pending_guard_with_tokio_spawn() {
|
||
let registry = Arc::new(AgentSessionRegistry::new());
|
||
let num_tasks = 50;
|
||
|
||
let mut handles = vec![];
|
||
|
||
// 并发创建 guard
|
||
for i in 0..num_tasks {
|
||
let registry_clone = registry.clone();
|
||
let handle = tokio::spawn(async move {
|
||
let project_id = format!("project-{}", i);
|
||
let _guard = PendingGuard::new(®istry_clone, &project_id);
|
||
// 模拟异步工作
|
||
tokio::time::sleep(Duration::from_millis(1)).await;
|
||
// guard 在这里被 drop
|
||
});
|
||
handles.push(handle);
|
||
}
|
||
|
||
// 等待所有任务完成
|
||
for handle in handles {
|
||
handle.await.unwrap();
|
||
}
|
||
|
||
// 验证: 所有项目都被清理
|
||
assert_eq!(registry.stats().agent_count, 0);
|
||
}
|
||
|
||
// ============================================================================
|
||
// 6. 边界条件测试
|
||
// ============================================================================
|
||
|
||
#[tokio::test]
|
||
async fn test_slot_counter_underflow_protection() {
|
||
let registry = AgentSessionRegistry::new();
|
||
|
||
// 尝试释放从未获取的槽位
|
||
for _ in 0..10 {
|
||
registry.release_session_slot();
|
||
}
|
||
|
||
// 验证: 计数器不会下溢(使用 saturating_sub)
|
||
let count = registry.active_sessions_count();
|
||
assert_eq!(count, 0, "计数器应该保持为 0,不会下溢");
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_multiple_pending_guards_same_project() {
|
||
let registry = AgentSessionRegistry::new();
|
||
let project_id = "test-project";
|
||
|
||
// 创建多个 guard(模拟并发请求)
|
||
{
|
||
let _guard1 = PendingGuard::new(®istry, project_id);
|
||
// 第二个 guard 会更新现有项目为 Pending(已经是 Pending,无操作)
|
||
let _guard2 = PendingGuard::new(®istry, project_id);
|
||
|
||
let info = registry.get_agent_info(project_id).unwrap();
|
||
assert_eq!(format!("{:?}", info.status), "Pending");
|
||
}
|
||
|
||
// 所有 guard 都 drop,应该被清理
|
||
assert!(!registry.contains_project(project_id));
|
||
}
|
||
|
||
// ============================================================================
|
||
// 7. 压力测试:高并发场景
|
||
// ============================================================================
|
||
|
||
#[tokio::test]
|
||
async fn test_high_concurrency_stress() {
|
||
let registry = Arc::new(AgentSessionRegistry::new());
|
||
let num_requests = 1000;
|
||
let barrier = Arc::new(Barrier::new(num_requests));
|
||
let success_count = Arc::new(AtomicUsize::new(0));
|
||
let fail_count = Arc::new(AtomicUsize::new(0));
|
||
|
||
let mut handles = vec![];
|
||
|
||
// 模拟高并发请求
|
||
for i in 0..num_requests {
|
||
let registry_clone = registry.clone();
|
||
let barrier_clone = barrier.clone();
|
||
let success_count_clone = success_count.clone();
|
||
let fail_count_clone = fail_count.clone();
|
||
|
||
let handle = tokio::spawn(async move {
|
||
let project_id = format!("project-{}", i);
|
||
|
||
// 等待所有任务就绪
|
||
barrier_clone.wait().await;
|
||
|
||
// 尝试获取槽位
|
||
if registry_clone.try_acquire_session_slot() {
|
||
success_count_clone.fetch_add(1, Ordering::Relaxed);
|
||
|
||
// 使用 PendingGuard
|
||
let _guard = PendingGuard::new(®istry_clone, &project_id);
|
||
|
||
// 模拟工作
|
||
let delay = (i % 10) as u64;
|
||
tokio::time::sleep(Duration::from_millis(delay)).await;
|
||
|
||
// 正常流程:禁用 guard,手动释放
|
||
drop(_guard);
|
||
registry_clone.release_session_slot();
|
||
registry_clone.clear_pending_if_exists(&project_id);
|
||
} else {
|
||
fail_count_clone.fetch_add(1, Ordering::Relaxed);
|
||
}
|
||
});
|
||
|
||
handles.push(handle);
|
||
}
|
||
|
||
// 等待所有任务完成
|
||
for handle in handles {
|
||
handle.await.unwrap();
|
||
}
|
||
|
||
// 验证: 只有 WORKER_THREAD_POOL_SIZE 个请求成功
|
||
let success = success_count.load(Ordering::Relaxed);
|
||
let fail = fail_count.load(Ordering::Relaxed);
|
||
|
||
assert_eq!(success + fail, num_requests, "所有请求都应该被处理");
|
||
|
||
assert_eq!(registry.active_sessions_count(), 0, "所有槽位应该被释放");
|
||
|
||
println!(
|
||
"High-concurrency stress test: {} success, {} failed",
|
||
success, fail
|
||
);
|
||
}
|
||
|
||
// ============================================================================
|
||
// 8. PendingGuard 与 SessionManager 竞态条件修复测试
|
||
// ============================================================================
|
||
|
||
/// 测试场景:PendingGuard 创建的占位符应该被真实会话替换
|
||
///
|
||
/// 这是修复的核心场景:
|
||
/// 1. PendingGuard 创建 pending 占位符
|
||
/// 2. 模拟 SessionManager 检测到 pending 并创建真实会话
|
||
/// 3. 验证 pending 占位符被正确替换
|
||
#[tokio::test]
|
||
async fn test_pending_placeholder_replaced_by_real_session() {
|
||
let registry = Arc::new(AgentSessionRegistry::new());
|
||
let project_id = "test-pending-replace";
|
||
|
||
// 第一阶段:创建 PendingGuard(模拟 gRPC 层的行为)
|
||
let guard = PendingGuard::new(®istry, project_id);
|
||
|
||
// 验证:pending 占位符已创建
|
||
assert!(registry.contains_project(project_id));
|
||
{
|
||
let pending_info = registry.get_agent_info(project_id).unwrap();
|
||
assert_eq!(format!("{:?}", pending_info.status), "Pending");
|
||
assert_eq!(pending_info.session_id.to_string(), "pending");
|
||
} // 释放 Ref 锁
|
||
|
||
// 第二阶段:模拟 SessionManager 检测到 Pending 状态并创建真实会话
|
||
// 检查状态(模拟 session_manager.rs 中的逻辑)
|
||
let should_replace = {
|
||
let info = registry.get_agent_info(project_id).unwrap();
|
||
*info.status() == AgentStatus::Pending
|
||
}; // 释放 Ref 锁
|
||
assert!(should_replace, "应该检测到 Pending 占位符");
|
||
|
||
// 创建真实会话
|
||
let real_session_id = "real-session-123";
|
||
let (prompt_tx, _prompt_rx) = mpsc::channel(100);
|
||
let (cancel_tx, _cancel_rx) = mpsc::channel(100);
|
||
|
||
let real_session = ProjectAndAgentInfo {
|
||
project_id: project_id.to_string(),
|
||
session_id: SessionId::new(Arc::from(real_session_id)),
|
||
prompt_tx,
|
||
cancel_tx,
|
||
model_provider: None,
|
||
request_id: None,
|
||
status: AgentStatus::Idle,
|
||
last_activity: chrono::Utc::now(),
|
||
created_at: chrono::Utc::now(),
|
||
stop_handle: None,
|
||
};
|
||
|
||
// 第三阶段:原子性替换(模拟 session_manager.rs 中的 Entry API 逻辑)
|
||
// 使用 DashMap 的 entry API 进行原子性替换
|
||
use dashmap::mapref::entry::Entry;
|
||
match registry.as_ref().inner_mut().entry(project_id.to_string()) {
|
||
Entry::Vacant(entry) => {
|
||
entry.insert(real_session.clone());
|
||
}
|
||
Entry::Occupied(mut entry) => {
|
||
// 检查仍然是 Pending(防止其他线程已经插入了真实会话)
|
||
// 提取状态值,避免借用冲突
|
||
let is_pending = {
|
||
let existing = entry.get();
|
||
*existing.status() == AgentStatus::Pending
|
||
};
|
||
if is_pending {
|
||
entry.insert(real_session.clone());
|
||
}
|
||
}
|
||
}
|
||
|
||
// 验证:pending 占位符已被替换为真实会话
|
||
assert!(registry.contains_project(project_id));
|
||
let final_info = registry.get_agent_info(project_id).unwrap();
|
||
assert_eq!(format!("{:?}", final_info.status), "Idle");
|
||
assert_eq!(final_info.session_id.to_string(), real_session_id);
|
||
assert!(!final_info.prompt_tx.is_closed());
|
||
|
||
// PendingGuard 不需要 commit(因为 pending 已被替换)
|
||
drop(guard);
|
||
|
||
// 验证:真实会话仍然存在(没有被 PendingGuard 清理)
|
||
assert!(registry.contains_project(project_id));
|
||
|
||
// 清理
|
||
registry.remove_by_project(project_id);
|
||
}
|
||
|
||
/// 测试场景:并发创建时,只有一个真实会话被保留
|
||
///
|
||
/// 验证修复的并发安全性:
|
||
/// 1. PendingGuard 创建 pending 占位符
|
||
/// 2. 多个线程尝试替换 pending
|
||
/// 3. 只有一个真实会话被保留
|
||
#[tokio::test]
|
||
async fn test_concurrent_pending_replacement() {
|
||
let registry = Arc::new(AgentSessionRegistry::new());
|
||
let project_id = "test-concurrent-replace";
|
||
let num_threads = 5;
|
||
let barrier = Arc::new(Barrier::new(num_threads));
|
||
let success_count = Arc::new(AtomicUsize::new(0));
|
||
|
||
// 第一阶段:创建 PendingGuard
|
||
let _guard = PendingGuard::new(®istry, project_id);
|
||
|
||
// 验证 pending 占位符
|
||
assert!(registry.contains_project(project_id));
|
||
{
|
||
let pending_info = registry.get_agent_info(project_id).unwrap();
|
||
assert_eq!(format!("{:?}", pending_info.status), "Pending");
|
||
} // 释放 Ref 锁
|
||
|
||
// 第二阶段:多个线程并发尝试替换 pending
|
||
let mut handles = vec![];
|
||
for i in 0..num_threads {
|
||
let registry_clone = registry.clone();
|
||
let barrier_clone = barrier.clone();
|
||
let success_count_clone = success_count.clone();
|
||
|
||
let handle = tokio::spawn(async move {
|
||
// 等待所有线程就绪
|
||
barrier_clone.wait().await;
|
||
|
||
// 每个线程创建一个"真实会话"
|
||
let session_id = format!("session-{}", i);
|
||
let (prompt_tx, _prompt_rx) = mpsc::channel(100);
|
||
let (cancel_tx, _cancel_rx) = mpsc::channel(100);
|
||
|
||
let real_session = ProjectAndAgentInfo {
|
||
project_id: project_id.to_string(),
|
||
session_id: SessionId::new(Arc::from(session_id.clone())),
|
||
prompt_tx,
|
||
cancel_tx,
|
||
model_provider: None,
|
||
request_id: None,
|
||
status: AgentStatus::Idle,
|
||
last_activity: chrono::Utc::now(),
|
||
created_at: chrono::Utc::now(),
|
||
stop_handle: None,
|
||
};
|
||
|
||
// 尝试原子性替换
|
||
use dashmap::mapref::entry::Entry;
|
||
let replaced = match registry_clone
|
||
.as_ref()
|
||
.inner_mut()
|
||
.entry(project_id.to_string())
|
||
{
|
||
Entry::Occupied(mut entry) => {
|
||
// 只有 pending 才替换
|
||
// 提取状态值,避免借用冲突
|
||
let is_pending = {
|
||
let existing = entry.get();
|
||
*existing.status() == AgentStatus::Pending
|
||
};
|
||
if is_pending {
|
||
entry.insert(real_session.clone());
|
||
success_count_clone.fetch_add(1, Ordering::Relaxed);
|
||
true
|
||
} else {
|
||
false
|
||
}
|
||
}
|
||
Entry::Vacant(_) => false,
|
||
};
|
||
|
||
replaced
|
||
});
|
||
|
||
handles.push(handle);
|
||
}
|
||
|
||
// 等待所有线程完成
|
||
for handle in handles {
|
||
handle.await.unwrap();
|
||
}
|
||
|
||
// 验证:只有一个线程成功替换
|
||
let success = success_count.load(Ordering::Relaxed);
|
||
assert_eq!(success, 1, "应该只有一个线程成功替换 pending");
|
||
|
||
// 验证:最终只有一个会话存在
|
||
assert!(registry.contains_project(project_id));
|
||
let final_info = registry.get_agent_info(project_id).unwrap();
|
||
assert_eq!(format!("{:?}", final_info.status), "Idle");
|
||
|
||
// 清理
|
||
registry.remove_by_project(project_id);
|
||
}
|
||
|
||
/// 测试场景:模拟真实的 session_manager.rs 逻辑流程
|
||
///
|
||
/// 这是一个端到端测试,模拟完整的修复流程:
|
||
/// 1. PendingGuard 创建占位符
|
||
/// 2. 检测到 Pending 状态
|
||
/// 3. 释放锁,创建真实会话
|
||
/// 4. 原子性插入/替换
|
||
#[tokio::test]
|
||
async fn test_session_manager_pending_replacement_flow() {
|
||
let registry = Arc::new(AgentSessionRegistry::new());
|
||
let project_id = "test-e2e-flow";
|
||
|
||
// ========== 第一阶段:PendingGuard 创建占位符 ==========
|
||
{
|
||
let _guard = PendingGuard::new(®istry, project_id);
|
||
|
||
// 验证占位符
|
||
{
|
||
let info = registry.get_agent_info(project_id).unwrap();
|
||
assert_eq!(format!("{:?}", info.status), "Pending");
|
||
} // 释放 Ref 锁
|
||
|
||
// ========== 第二阶段:模拟 SessionManager 的 get_or_create_session ==========
|
||
|
||
// 2.1 快速检查:发现 entry 存在
|
||
let entry_exists = registry.contains_project(project_id);
|
||
assert!(entry_exists);
|
||
|
||
// 2.2 显式检查 Pending 状态
|
||
let should_replace = {
|
||
let info = registry.get_agent_info(project_id).unwrap();
|
||
*info.status() == AgentStatus::Pending
|
||
}; // 释放 Ref 锁
|
||
assert!(should_replace, "应该检测到 Pending 状态");
|
||
|
||
// 2.3 创建真实会话(不持有锁)
|
||
let real_session_id = "ses_real_12345";
|
||
let (prompt_tx, _prompt_rx) = mpsc::channel(100);
|
||
let (cancel_tx, _cancel_rx) = mpsc::channel(100);
|
||
|
||
let real_session = ProjectAndAgentInfo {
|
||
project_id: project_id.to_string(),
|
||
session_id: SessionId::new(Arc::from(real_session_id)),
|
||
prompt_tx,
|
||
cancel_tx,
|
||
model_provider: None,
|
||
request_id: None,
|
||
status: AgentStatus::Idle,
|
||
last_activity: chrono::Utc::now(),
|
||
created_at: chrono::Utc::now(),
|
||
stop_handle: None,
|
||
};
|
||
|
||
// ========== 第三阶段:原子性替换 ==========
|
||
|
||
// 使用 DashMap entry API 进行原子性操作
|
||
use dashmap::mapref::entry::Entry;
|
||
let was_pending = match registry.as_ref().inner_mut().entry(project_id.to_string()) {
|
||
Entry::Occupied(mut entry) => {
|
||
// 提取状态值,避免借用冲突
|
||
let is_pending = {
|
||
let existing = entry.get();
|
||
*existing.status() == AgentStatus::Pending
|
||
};
|
||
if is_pending {
|
||
entry.insert(real_session.clone());
|
||
true
|
||
} else {
|
||
false
|
||
}
|
||
}
|
||
Entry::Vacant(_) => false,
|
||
};
|
||
|
||
assert!(was_pending, "应该成功替换 pending 占位符");
|
||
|
||
// 验证:真实会话已插入
|
||
let final_info = registry.get_agent_info(project_id).unwrap();
|
||
assert_eq!(final_info.session_id.to_string(), real_session_id);
|
||
assert_eq!(format!("{:?}", final_info.status), "Idle");
|
||
assert!(!final_info.prompt_tx.is_closed());
|
||
}
|
||
|
||
// PendingGuard 已 drop,但真实会话应该保留
|
||
assert!(registry.contains_project(project_id));
|
||
let final_info = registry.get_agent_info(project_id).unwrap();
|
||
assert_eq!(format!("{:?}", final_info.status), "Idle");
|
||
|
||
// 清理
|
||
registry.remove_by_project(project_id);
|
||
}
|
||
|
||
// ============================================================================
|
||
// 调试测试 - 定位挂起问题
|
||
// ============================================================================
|
||
|
||
#[test]
|
||
fn debug_simple_registry_operations() {
|
||
use agent_runner::service::AgentSessionRegistry;
|
||
use shared_types::SessionEntry;
|
||
|
||
let registry = AgentSessionRegistry::new();
|
||
registry.set_pending("test-1");
|
||
|
||
// 测试 contains_project
|
||
assert!(registry.contains_project("test-1"));
|
||
|
||
// 测试 get_agent_info
|
||
if let Some(info) = registry.get_agent_info("test-1") {
|
||
// 直接访问字段而不是通过 trait 方法
|
||
let status = &info.status;
|
||
assert_eq!(format!("{:?}", status), "Pending");
|
||
} else {
|
||
panic!("get_agent_info returned None");
|
||
}
|
||
|
||
registry.remove_by_project("test-1");
|
||
assert!(!registry.contains_project("test-1"));
|
||
}
|