93 KiB
DuckDB 内存数据库替代 DashMap 设计规范
1. 背景与目标
1.1 背景
1.1.3 系统约束
重要约束: 数据规模较小,无需考虑大数据量优化策略
- 数据量评估: DuckDB 数据库中的数据量不会很大
- 统计查询特点: 统计类查询(如容器数量统计)需要统计所有数据,不受时间范围限制
- 设计影响: 可以简化索引策略,优先考虑查询的简洁性和可维护性
当前 crates/rcoder 模块使用 DashMap 来管理运行时状态数据,但存在两个不同的业务场景:
1.1.1 RCoder 场景 (当前主要使用)
- 容器标识:
project_id对应一个容器 - 容器命名:
rcoder-agent-{project_id} - 工作目录:
/app/project_workspace/{project_id} - Agent实例: 每个容器只运行一个 Agent 实例
- 映射关系:
project_id -> ProjectAndContainerInfo
1.1.2 重要约束
- 数据规模: 数据量较小,无需考虑大数据量优化
- 统计查询: 统计类查询(如容器数量统计)需要统计所有数据,不受时间范围限制
- 查询模式: 主要为精确查询和全表统计,无需复杂的范围查询优化
- 事务要求: 大部分操作不需要强事务保证,只有
agent_status状态变更需要原子性 - 结构体分类: 公共结构体放在 shared_types 模块,专用结构体放在 duckdb_manager 模块
- 内存模式: DuckDB 使用内存模式,每次容器重启都是全新状态
1.1.2 ComputerAgentRunner 场景 (新功能)
- 容器标识:
user_id对应一个容器 - 容器命名:
computer-agent-runner-{user_id} - 工作目录:
/home/user(通过挂载配置映射) - Agent实例: 一个容器内可以运行多个
project_id的 Agent 实例 - 映射关系:
user_id -> ProjectAndContainerInfo
1.1.3 模块架构约束
- 专用模块: 创建
crates/duckdb_manager模块专门管理数据库操作 - 接口封装: 以 lib 库形式提供统一的数据访问接口
- 业务隔离:
crates/rcoder通过接口使用,避免直接数据库操作 - 职责分离: 数据库操作与业务逻辑分离,其他模块不直接操作数据库
1.1.3 现有 DashMap 结构
pub struct AppState {
/// 活跃的项目和容器映射
/// - RCoder: project_id -> ProjectAndContainerInfo
/// - ComputerAgentRunner: user_id -> ProjectAndContainerInfo
pub project_and_agent_map: DashMap<String, Arc<ProjectAndContainerInfo>>,
/// 会话映射, session_id -> ProjectAndContainerInfo
pub sessions: DashMap<String, Arc<ProjectAndContainerInfo>>,
/// 会话到容器ID的映射, session_id -> container_id
pub session_to_container_id: DashMap<String, String>,
}
1.2 业务目标
使用 DuckDB 内存模式替代当前的 DashMap,实现:
- 统一数据模型: 通过关系型数据库设计,提供更清晰的数据结构和关系
- SQL 查询能力: 支持复杂查询,如按时间范围筛选闲置容器、按服务类型统计等
- 事务支持: 保证多表操作的原子性
- 内存模式: 数据随容器重启重置,无需持久化
- 高性能: DuckDB 的列式存储和向量化执行引擎提供高效查询
- 双模式支持: 同时支持 RCoder 和 ComputerAgentRunner 两种业务场景
1.3 DuckDB 简介
DuckDB 是一个嵌入式分析型数据库,具有以下特点:
- 嵌入式: 无需外部服务器,直接嵌入应用程序
- 内存模式: 支持纯内存数据库,重启后数据清零
- 高性能: 列式存储 + 向量化执行
- Rust 支持: 官方提供
duckdbcrate
2. 现状分析
2.1 现有数据结构
AppState 中的 DashMap 字段
// crates/rcoder/src/router.rs
pub struct AppState {
pub config: AppConfig,
/// 活跃的会话映射, session_id -> ProjectAndContainerInfo
pub sessions: DashMap<String, Arc<ProjectAndContainerInfo>>,
/// 活跃的项目和容器映射, project_id -> ProjectAndContainerInfo
pub project_and_agent_map: DashMap<String, Arc<ProjectAndContainerInfo>>,
/// 会话到容器ID的映射, session_id -> container_id
pub session_to_container_id: DashMap<String, String>,
// ... 其他字段
}
ProjectAndContainerInfo 结构
// crates/shared_types/src/model/agent_project_runner_model.rs
pub struct ProjectCoreState {
pub project_id: String,
pub user_id: Option<String>,
pub session_id: Option<String>,
pub last_activity: DateTime<Utc>,
pub created_at: DateTime<Utc>,
}
pub struct ProjectExtendedState {
pub model_provider: Option<ModelProviderConfig>,
pub container: Option<ContainerBasicInfo>,
pub request_id: Option<String>,
pub status: Option<AgentStatus>,
pub service_type: Option<ServiceType>,
}
pub struct ContainerBasicInfo {
pub container_id: String,
pub container_name: String,
pub container_ip: String,
pub internal_port: u16,
pub external_port: u16,
pub project_id: String,
pub status: String,
pub created_at: DateTime<Utc>,
pub service_url: String,
}
2.2 现有操作模式
| 操作类型 | 使用场景 | 频率 |
|---|---|---|
| 插入/更新 | 新会话创建、容器启动 | 中 |
| 查询(按Key) | 获取容器信息、SSE 连接建立 | 高 |
| 查询(遍历) | 按 session_id 查找项目、闲置检测 | 中 |
| 删除 | 容器清理、会话过期 | 低 |
| 条件查询 | 闲置超时检测 (last_activity 筛选) |
低(定时任务) |
2.3 现有代码依赖分析
使用 DashMap 的文件:
| 文件路径 | 使用方式 |
|---|---|
crates/rcoder/src/router.rs |
定义 AppState |
crates/rcoder/src/handler/chat_handler.rs |
项目信息的获取/创建/更新 |
crates/rcoder/src/handler/computer_chat_handler.rs |
ComputerAgent 映射管理 |
crates/rcoder/src/handler/agent_session_notification.rs |
会话查找 |
crates/rcoder/src/handler/pod_handler.rs |
Pod 容器管理 |
crates/rcoder/src/proxy_agent/cleanup_task.rs |
闲置容器清理 |
crates/rcoder/src/service/container_status_checker.rs |
容器状态更新 |
3. 数据库设计
3.1 表结构设计
3.1.1 容器信息表 (containers)
存储所有容器的基本信息,每个容器对应一个服务实例。
| 列名 | 类型 | 约束 | 说明 |
|---|---|---|---|
| container_id | VARCHAR | PRIMARY KEY | 容器唯一标识 |
| container_name | VARCHAR | NOT NULL | 容器名称 |
| container_ip | VARCHAR | NOT NULL | 容器 IP 地址 |
| internal_port | INTEGER | NOT NULL | 内部端口 |
| external_port | INTEGER | NOT NULL | 外部端口 |
| service_type | VARCHAR | NOT NULL | 服务类型 (RCoder/ComputerAgentRunner) |
| status | VARCHAR | NOT NULL | 容器状态 |
| service_url | VARCHAR | NOT NULL | 服务 URL |
| created_at | TIMESTAMP | NOT NULL | 创建时间 |
| last_activity | TIMESTAMP | NOT NULL | 最后活动时间 |
CREATE TABLE containers (
container_id VARCHAR PRIMARY KEY,
container_name VARCHAR NOT NULL,
container_ip VARCHAR NOT NULL,
internal_port INTEGER NOT NULL,
external_port INTEGER NOT NULL,
service_type VARCHAR NOT NULL, -- 'RCoder' 或 'ComputerAgentRunner'
status VARCHAR NOT NULL,
service_url VARCHAR NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_activity TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- 索引 (无外键约束)
CREATE INDEX idx_containers_service_type ON containers(service_type);
CREATE INDEX idx_containers_last_activity ON containers(last_activity);
3.1.2 统一项目表 (projects) - 已合并session信息
存储所有类型的项目信息和关联的session信息,通过 service_type 字段区分不同业务场景。遵循第一范式设计原则,已合并sessions表以优化查询性能。
| 列名 | 类型 | 约束 | 说明 |
|---|---|---|---|
| project_id | VARCHAR | PRIMARY KEY | 项目唯一标识 |
| session_id | VARCHAR | NULL | 当前活跃会话 ID(可为空) |
| service_type | VARCHAR | NOT NULL | 服务类型 (RCoder/ComputerAgentRunner) |
| container_id | VARCHAR | NOT NULL | 关联的容器 ID |
| user_id | VARCHAR | NULL | ComputerAgentRunner 用户 ID (RCoder 模式为 NULL) |
| agent_status_code | INTEGER | NULL | Agent 状态码 (0=Active, 1=Idle, 2=Terminating) |
| agent_status_name | VARCHAR | NULL | Agent 状态描述 (Active/Idle/Terminating) |
| request_id | VARCHAR | NULL | 当前请求 ID |
| model_provider_json | VARCHAR | NULL | 模型配置 (JSON 序列化) |
| created_at | TIMESTAMP | NOT NULL | 项目创建时间 |
| last_activity | TIMESTAMP | NOT NULL | 项目最后活动时间 |
| session_created_at | TIMESTAMP | NULL | 会话创建时间(可为空) |
| session_last_activity | TIMESTAMP | NULL | 会话最后活动时间(可为空) |
-- 合并后的projects表(包含session信息)
CREATE TABLE projects (
project_id VARCHAR PRIMARY KEY,
session_id VARCHAR, -- 从sessions表合并,允许NULL(无活跃session)
service_type VARCHAR NOT NULL, -- 'RCoder' 或 'ComputerAgentRunner'
container_id VARCHAR NOT NULL, -- 无外键约束,通过应用层保证一致性
user_id VARCHAR, -- ComputerAgentRunner 模式时使用,RCoder 模式为 NULL
agent_status_code INTEGER, -- Agent状态码 (0=Active, 1=Idle, 2=Terminating)
agent_status_name VARCHAR, -- Agent状态描述 (Active/Idle/Terminating)
request_id VARCHAR,
model_provider_json VARCHAR,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_activity TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
session_created_at TIMESTAMP, -- session创建时间,可为空
session_last_activity TIMESTAMP, -- session最后活动时间,可为空
-- 约束:RCoder 模式下 user_id 必须为 NULL,ComputerAgentRunner 模式下 user_id 必须不为 NULL
CHECK (
(service_type = 'RCoder' AND user_id IS NULL) OR
(service_type = 'ComputerAgentRunner' AND user_id IS NOT NULL)
),
-- 约束:agent_status_code 和 agent_status_name 要么都为NULL,要么都为非NULL且匹配
CHECK (
(agent_status_code IS NULL AND agent_status_name IS NULL) OR
(agent_status_code IS NOT NULL AND agent_status_name IS NOT NULL AND
((agent_status_code = 0 AND agent_status_name = 'Active') OR
(agent_status_code = 1 AND agent_status_name = 'Idle') OR
(agent_status_code = 2 AND agent_status_name = 'Terminating')))
)
);
-- 索引优化(重点支持session_id查询和状态筛选)
CREATE INDEX idx_projects_session_id ON projects(session_id); -- 核心查询:SSE消息转发
CREATE INDEX idx_projects_container_id ON projects(container_id);
CREATE INDEX idx_projects_user_id ON projects(user_id); -- ComputerAgentRunner 模式
CREATE INDEX idx_projects_agent_status_code ON projects(agent_status_code); -- 状态查询优化
CREATE INDEX idx_projects_last_activity ON projects(last_activity);
CREATE INDEX idx_projects_service_type ON projects(service_type);
CREATE INDEX idx_projects_service_type_activity ON projects(service_type, last_activity);
CREATE INDEX idx_projects_status_activity ON projects(agent_status_code, last_activity); -- 清理任务优化
3.2 轻量一致性与容错策略
3.2.1 轻量一致性保证
大部分操作为轻量级更新,采用以下策略保证基本一致性:
- 容器存在性检查: 创建项目前检查容器是否存在(轻量查询)
- 业务规则校验: 通过 CHECK 约束保证基本的业务规则
- 应用层级联: 删除操作时手动清理关联数据,无需复杂事务
3.2.2 业务规则约束
- RCoder 模式:
service_type = 'RCoder'时,user_id IS NULL - ComputerAgentRunner 模式:
service_type = 'ComputerAgentRunner'时,user_id IS NOT NULL - Agent状态一致性:
agent_status_code和agent_status_name要么都为NULL,要么都为非NULL且相互匹配
3.2.3 内存模式特性
使用 DuckDB 内存模式,每次容器重启都会获得全新的空数据库,这是设计的核心特性:
/// DuckDB 内存模式初始化
pub struct DuckDbMemoryStorage {
connection: Arc<Mutex<Connection>>,
}
impl DuckDbMemoryStorage {
/// 初始化内存数据库
pub fn new() -> Result<Self, StorageError> {
// 创建内存数据库连接
let connection = Connection::open_in_memory()?;
// 初始化表结构
Self::init_tables(&connection)?;
Ok(Self {
connection: Arc::new(Mutex::new(connection)),
})
}
/// 初始化数据库表结构
fn init_tables(conn: &Connection) -> Result<(), StorageError> {
// 创建 containers 表
conn.execute(
"CREATE TABLE containers (...)",
[],
)?;
// 创建 projects 表(已包含session信息)
conn.execute(
"CREATE TABLE projects (...)",
[],
)?;
// 创建索引
Self::create_indexes(conn)?;
Ok(())
}
}
3.2.4 原子性操作范围
需要事务保证的操作:
agent_status状态变更
轻量级操作(无需事务):
last_activity时间更新- 会话信息维护
- 基本 CRUD 操作
3.3 内存模式设计优势
3.3.1 天然的容错性
- 无持久化故障: 内存模式下不存在数据文件损坏的问题
- 重启即清理: 每次容器重启自动获得干净的状态
- 简化部署: 无需考虑数据库文件的备份和恢复
3.3.2 性能优势
- 内存访问: 数据直接在内存中,访问速度极快
- 无磁盘I/O: 避免磁盘读写瓶颈
- 轻量事务: 只有必要的状态变更使用事务,大部分操作零开销
3.3.3 架构简化
- 无降级策略: 不需要复杂的故障恢复机制
- 简化监控: 无需监控数据库健康状态
- 部署友好: 容器化环境下天然适合内存模式
3.4 Agent状态字段设计优化
3.4.1 设计理念
将单一的 agent_status 字段拆分为 agent_status_code (数字) 和 agent_status_name (字符串) 的设计有以下优势:
- 查询性能优化: 数字状态码在索引和比较操作中更高效
- 扩展性: 支持未来增加更多状态而不需要修改现有数据
- 国际化支持: 状态名称可以根据语言环境显示不同的文本
- 向后兼容: 通过枚举提供类型安全,同时支持数据库层的灵活性
3.4.2 枚举设计
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AgentStatus {
/// 活跃状态 - 正在处理请求
Active = 0,
/// 空闲状态 - 等待新请求
Idle = 1,
/// 正在终止
Terminating = 2,
}
3.4.3 数据库约束
-- 确保状态码和状态名的一致性
CHECK (
(agent_status_code IS NULL AND agent_status_name IS NULL) OR
(agent_status_code IS NOT NULL AND agent_status_name IS NOT NULL AND
((agent_status_code = 0 AND agent_status_name = 'Active') OR
(agent_status_code = 1 AND agent_status_name = 'Idle') OR
(agent_status_code = 2 AND agent_status_name = 'Terminating')))
)
3.4.4 索引优化
-- 状态码索引 - 数字比较更快
CREATE INDEX idx_projects_agent_status_code ON projects(agent_status_code);
-- 复合索引 - 优化清理任务
CREATE INDEX idx_projects_status_activity ON projects(agent_status_code, last_activity);
3.4.5 查询性能对比
-- 优化前:字符串比较
SELECT * FROM projects WHERE agent_status = 'Idle';
-- 优化后:数字比较(更快)
SELECT * FROM projects WHERE agent_status_code = 1;
-- 复合查询:状态+时间(索引更有效)
SELECT * FROM projects
WHERE agent_status_code = 1 AND last_activity < ?
3.5 DuckDB-RS 适配说明
接口设计完全基于 DuckDB-RS 的实际使用模式:
3.5.1 连接管理适配
- 内存数据库: 使用
Connection::open_in_memory()初始化 - 并发访问: 通过
try_clone()支持多连接并发操作 - 连接生命周期: 支持独立关闭,避免资源泄漏
/// 连接管理最佳实践
pub struct DuckDbStorage {
/// 主连接 - 用于 DDL 和管理操作
connection: Arc<Mutex<Connection>>,
}
impl DuckDbStorage {
/// 创建内存数据库
pub fn new() -> Result<Self, DuckDbError> {
let connection = Connection::open_in_memory()
.map_err(|e| DuckDbError::ConnectionError(e.to_string()))?;
Ok(Self {
connection: Arc::new(Mutex::new(connection)),
})
}
/// 创建工作连接(用于并发查询)
///
/// DuckDB-RS 的 try_clone() 创建共享同一数据库的新连接,
/// 适合多线程并发访问场景
pub fn create_worker_connection(&self) -> Result<WorkerConnection, DuckDbError> {
let conn = self.connection.lock().unwrap();
let cloned = conn.try_clone()
.map_err(|e| DuckDbError::ConnectionCloneError(e.to_string()))?;
Ok(WorkerConnection::new(cloned))
}
}
/// 工作连接包装器
pub struct WorkerConnection {
connection: Option<Connection>,
}
impl WorkerConnection {
pub fn new(connection: Connection) -> Self {
Self { connection: Some(connection) }
}
/// 执行查询
pub fn execute<F, T>(&self, f: F) -> Result<T, DuckDbError>
where
F: FnOnce(&Connection) -> Result<T, duckdb::Error>,
{
let conn = self.connection.as_ref()
.ok_or_else(|| DuckDbError::ConnectionError("连接已关闭".to_string()))?;
f(conn).map_err(DuckDbError::from)
}
/// 显式关闭连接
pub fn close(mut self) -> Result<(), DuckDbError> {
self.connection.take();
Ok(())
}
}
3.5.2 事务机制适配
- 事务创建: 使用
connection.transaction()创建事务对象 - 提交行为: 支持
DropBehavior::Commit和DropBehavior::Rollback - 作用域管理: 事务对象离开作用域时自动处理提交/回滚
/// 事务管理示例
impl DuckDbStorage {
/// 执行需要事务的状态更新
pub fn update_status_atomic(
&self,
project_id: &str,
status: AgentStatus,
) -> Result<(), DuckDbError> {
let conn = self.connection.lock().unwrap();
let tx = conn.transaction()
.map_err(|e| DuckDbError::TransactionError(e.to_string()))?;
// 设置自动提交行为
// tx.set_drop_behavior(DropBehavior::Rollback);
tx.execute(
"UPDATE projects SET agent_status_code = ?, agent_status_name = ?, last_activity = CURRENT_TIMESTAMP WHERE project_id = ?",
params![status.code(), status.name(), project_id],
).map_err(|e| DuckDbError::QueryError(e.to_string()))?;
// 显式提交
tx.commit().map_err(|e| DuckDbError::TransactionError(e.to_string()))?;
Ok(())
}
}
3.5.3 批量操作适配(Appender API)
- Appender API: 提供高效的批量插入接口,比逐行 INSERT 快 10-100 倍
- 参数化查询: 使用
params!宏进行安全的参数绑定 - 批量执行: 支持
execute_batch()执行多条SQL语句
use duckdb::params;
/// 批量插入示例
impl ContainerRepositoryImpl {
/// 使用 Appender 批量插入容器
pub fn bulk_insert(&self, containers: &[ContainerRecord]) -> Result<(), DuckDbError> {
let conn = self.connection.lock().unwrap();
// 创建 Appender
let mut appender = conn.appender("containers")
.map_err(|e| DuckDbError::AppenderError(e.to_string()))?;
for container in containers {
appender.append_row(params![
container.container_id,
container.container_name,
container.container_ip,
container.internal_port as i32,
container.external_port as i32,
container.service_type.to_string(),
container.status,
container.service_url,
container.created_at.to_rfc3339(),
container.last_activity.to_rfc3339(),
]).map_err(|e| DuckDbError::AppenderError(e.to_string()))?;
}
// Appender 在 drop 时自动 flush
Ok(())
}
}
/// params! 宏使用示例
impl ProjectRepositoryImpl {
pub fn upsert(&self, project: &ProjectRecord) -> Result<(), DuckDbError> {
let conn = self.connection.lock().unwrap();
// 使用 params! 宏进行参数绑定,防止 SQL 注入
conn.execute(
r#"
INSERT INTO projects (project_id, session_id, service_type, container_id, user_id,
agent_status_code, agent_status_name, request_id,
model_provider_json, created_at, last_activity)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (project_id) DO UPDATE SET
session_id = excluded.session_id,
agent_status_code = excluded.agent_status_code,
agent_status_name = excluded.agent_status_name,
request_id = excluded.request_id,
model_provider_json = excluded.model_provider_json,
last_activity = excluded.last_activity
"#,
params![
project.project_id,
project.session_id,
project.service_type.to_string(),
project.container_id,
project.user_id,
project.agent_status.as_ref().map(|s| s.code()),
project.agent_status.as_ref().map(|s| s.name()),
project.request_id,
project.model_provider.as_ref().map(|mp| serde_json::to_string(mp).ok()).flatten(),
project.created_at.to_rfc3339(),
project.last_activity.to_rfc3339(),
],
).map_err(|e| DuckDbError::QueryError(e.to_string()))?;
Ok(())
}
}
3.5.4 查询结果映射
- 预编译语句: 使用
prepare()创建可重用查询 - 结果映射: 支持将查询结果映射到 Rust 结构体
- 类型安全: 通过泛型参数确保类型安全
/// 查询结果映射示例
impl ProjectRepositoryImpl {
/// 按 session_id 查询项目
pub fn find_by_session_id(&self, session_id: &str) -> Result<Option<ProjectRecord>, DuckDbError> {
let conn = self.connection.lock().unwrap();
let mut stmt = conn.prepare(
"SELECT * FROM projects WHERE session_id = ?"
).map_err(|e| DuckDbError::QueryError(e.to_string()))?;
let mut rows = stmt.query(params![session_id])
.map_err(|e| DuckDbError::QueryError(e.to_string()))?;
if let Some(row) = rows.next()
.map_err(|e| DuckDbError::QueryError(e.to_string()))?
{
Ok(Some(Self::row_to_project_record(row)?))
} else {
Ok(None)
}
}
/// 行数据映射到 ProjectRecord
fn row_to_project_record(row: &duckdb::Row) -> Result<ProjectRecord, DuckDbError> {
let status_code: Option<i32> = row.get("agent_status_code")
.map_err(|e| DuckDbError::RowMappingError(e.to_string()))?;
let status = status_code.and_then(AgentStatus::from_code);
let model_provider_json: Option<String> = row.get("model_provider_json")
.map_err(|e| DuckDbError::RowMappingError(e.to_string()))?;
let model_provider = model_provider_json
.and_then(|json| serde_json::from_str(&json).ok());
let service_type_str: String = row.get("service_type")
.map_err(|e| DuckDbError::RowMappingError(e.to_string()))?;
let service_type = service_type_str.parse::<ServiceType>()
.map_err(|e| DuckDbError::RowMappingError(e.to_string()))?;
let created_at_str: String = row.get("created_at")
.map_err(|e| DuckDbError::RowMappingError(e.to_string()))?;
let created_at = DateTime::parse_from_rfc3339(&created_at_str)
.map_err(|e| DuckDbError::RowMappingError(e.to_string()))?
.with_timezone(&Utc);
let last_activity_str: String = row.get("last_activity")
.map_err(|e| DuckDbError::RowMappingError(e.to_string()))?;
let last_activity = DateTime::parse_from_rfc3339(&last_activity_str)
.map_err(|e| DuckDbError::RowMappingError(e.to_string()))?
.with_timezone(&Utc);
Ok(ProjectRecord {
project_id: row.get("project_id").map_err(|e| DuckDbError::RowMappingError(e.to_string()))?,
session_id: row.get("session_id").map_err(|e| DuckDbError::RowMappingError(e.to_string()))?,
service_type,
container_id: row.get("container_id").map_err(|e| DuckDbError::RowMappingError(e.to_string()))?,
user_id: row.get("user_id").map_err(|e| DuckDbError::RowMappingError(e.to_string()))?,
agent_status: status,
request_id: row.get("request_id").map_err(|e| DuckDbError::RowMappingError(e.to_string()))?,
model_provider,
created_at,
last_activity,
})
}
/// 查询所有项目(带类型过滤)
pub fn find_all(&self, service_type: Option<ServiceType>) -> Result<Vec<ProjectRecord>, DuckDbError> {
let conn = self.connection.lock().unwrap();
let sql = match service_type {
Some(_) => "SELECT * FROM projects WHERE service_type = ? ORDER BY last_activity DESC",
None => "SELECT * FROM projects ORDER BY last_activity DESC",
};
let mut stmt = conn.prepare(sql)
.map_err(|e| DuckDbError::QueryError(e.to_string()))?;
let rows = match service_type {
Some(st) => stmt.query(params![st.to_string()]),
None => stmt.query([]),
}.map_err(|e| DuckDbError::QueryError(e.to_string()))?;
let mut projects = Vec::new();
for row_result in rows.mapped(|row| Self::row_to_project_record(row)) {
projects.push(row_result.map_err(|e| DuckDbError::RowMappingError(e.to_string()))??);
}
Ok(projects)
}
}
3.5.5 异步封装
DuckDB-RS 是同步 API,需要通过 spawn_blocking 封装为异步:
/// 异步封装示例
impl AsyncProjectRepository {
pub async fn find_by_session_id(&self, session_id: String) -> Result<Option<ProjectRecord>, DuckDbError> {
let storage = self.storage.clone();
tokio::task::spawn_blocking(move || {
storage.projects().find_by_session_id(&session_id)
})
.await
.map_err(|e| DuckDbError::ConcurrencyError(e.to_string()))?
}
}
3.6 表关系图(优化后:2表设计)
┌─────────────────────────────────────────────────────────────────────────────┐
│ containers │
│ ┌─────────────────────┐ │
│ │ container_id (PK) │ 容器唯一标识 │
│ │ container_name │ 容器名称 (rcoder-agent-xxx / computer-agent-runner-xxx) │
│ │ container_ip │ 容器 IP 地址 │
│ │ internal_port │ 内部端口 │
│ │ external_port │ 外部端口 │
│ │ service_type │ 服务类型 (RCoder / ComputerAgentRunner) │
│ │ status │ 容器状态 │
│ │ service_url │ 服务 URL │
│ │ created_at │ 创建时间 │
│ │ last_activity │ 最后活动时间 │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
│ 逻辑关联 (应用层保证,无外键)
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ projects (已合并 session 信息) │
│ ┌─────────────────────┐ │
│ │ project_id (PK) │ 项目唯一标识 │
│ │ session_id │ 当前活跃会话 ID(合并自 sessions 表) │
│ │ service_type │ 服务类型 (RCoder / ComputerAgentRunner) │
│ │ container_id │ 关联的容器 ID ─────────────────────────────────────┤
│ │ user_id │ 用户 ID (ComputerAgentRunner 模式专用) │
│ │ agent_status_code │ Agent 状态码 (0=Active, 1=Idle, 2=Terminating) │
│ │ agent_status_name │ Agent 状态名称 │
│ │ request_id │ 当前请求 ID │
│ │ model_provider_json │ 模型配置 (JSON 序列化) │
│ │ created_at │ 项目创建时间 │
│ │ last_activity │ 项目最后活动时间 │
│ │ session_created_at │ 会话创建时间(合并自 sessions 表) │
│ │ session_last_activity│ 会话最后活动时间(合并自 sessions 表) │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
核心查询路径(已优化):
━━━━━━━━━━━━━━━━━━━━━━━━━━
session_id ──► projects ──► container_id
(单表查询)
原有查询路径(3表 JOIN,已废弃):
───────────────────────────────────────
session_id ──► sessions ──► project_id ──► projects ──► container_id
3.7 边界场景说明
本节说明 DuckDB 迁移中不涉及的组件和需要特殊处理的场景。
3.7.1 gRPC 连接池(不迁移)
GrpcChannelPool 保持使用 DashMap,不迁移到 DuckDB。原因如下:
- 连接对象特殊性: gRPC Channel 是有状态的连接对象,无法序列化到数据库
- 生命周期管理: 连接池需要管理连接的创建、复用和销毁,这是内存操作的优势场景
- 性能要求: 每次请求都需要获取连接,DashMap 的 O(1) 查找是最优选择
// AppState 中保持不变
pub struct AppState {
// ... 其他字段
/// gRPC 连接池 - 保持使用 DashMap
pub grpc_pool: Arc<GrpcChannelPool>,
}
// GrpcChannelPool 结构保持不变
pub struct GrpcChannelPool {
channels: DashMap<String, GrpcChannel>,
// ...
}
清理时机: 当容器被销毁时,需要同时清理对应的 gRPC 连接:
// 在容器清理流程中
async fn cleanup_container(container_id: &str, state: &AppState) {
// 1. 从 DuckDB 删除容器和关联项目
storage.cleanup_project(project_id).await?;
// 2. 清理 gRPC 连接池(DashMap 操作)
if let Some(container) = get_container_info(container_id) {
let channel_key = format!("{}:{}", container.container_ip, container.internal_port);
state.grpc_pool.remove(&channel_key);
}
// 3. 实际销毁 Docker 容器
docker_manager.remove_container(container_id).await?;
}
3.7.2 VNC 后端映射清理(ComputerAgentRunner 模式)
ComputerAgentRunner 模式下,每个用户容器可能有关联的 VNC 后端映射,需要在容器清理时一并处理:
// VNC 后端清理流程
async fn cleanup_computer_agent_container(user_id: &str, state: &AppState) {
// 1. 从 DuckDB 获取用户关联的容器信息
let projects = storage.projects().find_by_user_id(user_id)?;
// 2. 清理 VNC 后端映射(如果使用 Pingora 代理)
if let Some(pingora_service) = &state.pingora_service {
for project in &projects {
if let Some(container) = storage.containers().find_by_id(&project.container_id)? {
// 移除 VNC 代理映射
pingora_service.remove_vnc_backend(&container.container_id);
}
}
}
// 3. 清理 gRPC 连接
// ... (同上)
// 4. 从 DuckDB 删除用户相关数据
storage.cleanup_user(user_id)?;
}
VNC 映射不存入 DuckDB: VNC 后端映射由 Pingora 代理服务内部管理,属于运行时路由配置,不需要持久化到数据库。
3.7.3 清理任务保护期
新创建的容器有 5 分钟保护期,在保护期内不会被清理任务回收:
/// 查询可清理的闲置项目(带保护期)
fn find_projects_for_cleanup(
&self,
idle_threshold: Duration, // 闲置阈值(如 30 分钟)
protection_duration: Duration, // 保护期(如 5 分钟)
service_type: Option<ServiceType>,
) -> Result<Vec<ProjectRecord>, StorageError>;
对应的 SQL 查询:
-- 查询可清理的闲置项目
SELECT p.*, c.*
FROM projects p
JOIN containers c ON p.container_id = c.container_id
WHERE
-- 闲置时间超过阈值
p.last_activity < NOW() - INTERVAL ? SECONDS
-- 容器创建时间超过保护期
AND c.created_at < NOW() - INTERVAL ? SECONDS
-- 状态为空闲或空
AND (p.agent_status_code IS NULL OR p.agent_status_code = 1)
-- 可选:按服务类型过滤
AND (? IS NULL OR p.service_type = ?);
3.8 表关系说明
数据模型特点
| 特性 | 说明 |
|---|---|
| 表数量 | 2 个表(containers + projects) |
| 外键约束 | 无,通过应用层保证数据一致性 |
| 业务区分 | 通过 service_type 字段区分 RCoder 和 ComputerAgentRunner |
| 会话管理 | session 信息已合并到 projects 表 |
业务场景映射
| 场景 | 容器标识 | 项目映射 | 会话映射 |
|---|---|---|---|
| RCoder | project_id |
project_id → container_id |
session_id → project_id (同表查询) |
| ComputerAgentRunner | user_id |
user_id → container_id |
session_id → project_id (同表查询) |
设计优势
- 查询性能: SSE 消息转发从 3 表 JOIN 优化为单表查询
- 维护简单: 减少 60% 的表数量,降低维护复杂度
- 一致性: session 与 project 状态在同一记录中,无需跨表同步
- 扩展性: 新增服务类型只需修改枚举,无需改表结构
4. 接口设计
4.1 核心 Trait 定义
4.1.1 存储层 Trait
/// DuckDB 内存存储管理器
///
/// 负责管理 DuckDB 内存数据库的生命周期和连接
pub trait MemoryStorageManager: Send + Sync {
/// 初始化数据库,创建表结构
fn initialize(&self) -> Result<(), StorageError>;
/// 获取数据库连接
fn get_connection(&self) -> Result<Connection, StorageError>;
/// 关闭数据库(容器停止时调用)
fn shutdown(&self) -> Result<(), StorageError>;
/// 获取统计信息(用于监控)
fn get_stats(&self) -> StorageStats;
}
/// 容器信息仓储接口
pub trait ContainerRepository: Send + Sync {
/// 插入或更新容器信息
fn upsert(&self, container: &ContainerRecord) -> Result<(), StorageError>;
/// 根据容器 ID 查询
fn find_by_id(&self, container_id: &str) -> Result<Option<ContainerRecord>, StorageError>;
/// 根据服务类型查询所有容器
fn find_by_service_type(&self, service_type: ServiceType) -> Result<Vec<ContainerRecord>, StorageError>;
/// 删除容器记录
fn delete(&self, container_id: &str) -> Result<bool, StorageError>;
/// 检查容器是否存在
fn exists(&self, container_id: &str) -> Result<bool, StorageError>;
/// 查询闲置容器(用于清理任务)
fn find_idle_containers(
&self,
idle_threshold: Duration,
service_type: Option<ServiceType>,
) -> Result<Vec<ContainerRecord>, StorageError>;
/// 获取所有容器ID集合(用于孤立数据检测)
fn get_all_container_ids(&self) -> Result<HashSet<String>, StorageError>;
/// 查询孤立容器(无关联项目的容器)
///
/// 用于检测数据一致性问题,找出没有任何项目关联的容器
fn find_orphan_containers(&self) -> Result<Vec<ContainerRecord>, StorageError>;
/// 批量删除容器
fn bulk_delete(&self, container_ids: &[String]) -> Result<usize, StorageError>;
}
/// 统一项目仓储接口(已合并session功能)
/// 包含项目和session的所有操作,大部分操作为轻量级操作,无需事务保证
pub trait ProjectRepository: Send + Sync {
/// 插入或更新项目(轻量操作)
fn upsert(&self, project: &ProjectRecord) -> Result<(), StorageError>;
/// 根据项目 ID 查询
fn find_by_id(&self, project_id: &str) -> Result<Option<ProjectRecord>, StorageError>;
/// 根据容器 ID 查询项目
fn find_by_container_id(&self, container_id: &str) -> Result<Vec<ProjectRecord>, StorageError>;
/// 根据用户 ID 查询项目(ComputerAgentRunner 模式)
fn find_by_user_id(&self, user_id: &str) -> Result<Vec<ProjectRecord>, StorageError>;
/// 根据会话 ID 查询项目(核心查询:SSE消息转发)
fn find_by_session_id(&self, session_id: &str) -> Result<Option<ProjectRecord>, StorageError>;
/// 根据服务类型查询项目
fn find_by_service_type(&self, service_type: ServiceType) -> Result<Vec<ProjectRecord>, StorageError>;
/// 轻量更新项目最后活动时间(高频操作,无事务)
fn update_activity(&self, project_id: &str) -> Result<(), StorageError>;
/// 轻量更新会话信息(包含session_id和时间戳)
fn update_session(&self, project_id: &str, session_id: Option<&str>, session_created_at: Option<DateTime<Utc>>) -> Result<(), StorageError>;
/// 轻量更新会话最后活动时间(高频操作,无事务)
fn update_session_activity(&self, session_id: &str) -> Result<(), StorageError>;
/// 原子性更新 Agent 状态(需要事务保证)
fn update_status_atomic(&self, project_id: &str, status: Option<AgentStatus>) -> Result<(), StorageError>;
/// 按状态查询项目(支持状态码查询,性能更优)
fn find_by_status(&self, status: AgentStatus) -> Result<Vec<ProjectRecord>, StorageError>;
/// 查询非终止状态的项目(用于清理任务)
fn find_active_and_idle(&self) -> Result<Vec<ProjectRecord>, StorageError>;
/// 删除项目记录
fn delete(&self, project_id: &str) -> Result<bool, StorageError>;
/// 查询闲置项目(利用状态码索引优化性能)
fn find_idle_projects(
&self,
idle_threshold: Duration,
service_type: Option<ServiceType>,
) -> Result<Vec<ProjectRecord>, StorageError>;
/// 获取所有项目
fn find_all(&self, service_type: Option<ServiceType>) -> Result<Vec<ProjectRecord>, StorageError>;
/// 统计项目数量
fn count(&self) -> Result<usize, StorageError>;
/// 按状态统计项目数量
fn count_by_status(&self) -> Result<std::collections::HashMap<AgentStatus, usize>, StorageError>;
/// 获取会话对应的容器ID(优化后的核心方法)
fn get_container_id_by_session(&self, session_id: &str) -> Result<Option<String>, StorageError>;
/// 批量插入项目(使用 DuckDB Appender API)
fn bulk_insert(&self, projects: &[ProjectRecord]) -> Result<(), StorageError>;
/// 获取所有项目ID集合(用于孤立容器检测)
fn get_all_project_ids(&self) -> Result<HashSet<String>, StorageError>;
/// 批量删除项目(用于孤立数据清理)
fn bulk_delete(&self, project_ids: &[String]) -> Result<usize, StorageError>;
/// 查询可清理的闲置项目(带保护期)
///
/// # 参数
/// - `idle_threshold`: 闲置时间阈值(如 30 分钟)
/// - `protection_duration`: 新建容器保护期(如 5 分钟)
/// - `service_type`: 可选的服务类型过滤
fn find_projects_for_cleanup(
&self,
idle_threshold: Duration,
protection_duration: Duration,
service_type: Option<ServiceType>,
) -> Result<Vec<ProjectRecord>, StorageError>;
}
## SessionRepository 已合并到 ProjectRepository
由于session信息已合并到projects表,SessionRepository接口已废弃。所有session相关的操作都通过ProjectRepository完成,这样可以:
1. **减少表关联查询**: 从3表JOIN变为2表JOIN
2. **简化数据一致性**: session与project状态在一个事务中维护
3. **优化核心查询**: `session_id -> container_id` 直接查询,无需关联
4.1.2 事务管理接口
基于 DuckDB-RS 的事务机制,设计更贴近实际使用的接口:
/// 事务管理器
///
/// 封装 DuckDB-RS 的事务行为
pub trait TransactionManager: Send + Sync {
/// 执行事务操作
/// 支持自动提交和回滚行为
fn execute_transaction<F, T>(&self, f: F) -> Result<T, StorageError>
where
F: FnOnce(&Transaction) -> Result<T, StorageError>;
/// 执行状态变更事务(强制提交)
/// 专门用于 agent_status 等需要强一致性的操作
fn execute_status_update<F, T>(&self, f: F) -> Result<T, StorageError>
where
F: FnOnce(&Transaction) -> Result<T, StorageError>;
}
/// 事务对象
/// 封装 DuckDB 事务,支持显式提交/回滚
pub trait Transaction {
/// 提交事务
fn commit(self) -> Result<(), StorageError>;
/// 回滚事务
fn rollback(self) -> Result<(), StorageError>;
/// 设置丢弃行为(自动提交/回滚)
fn set_drop_behavior(&mut self, behavior: DropBehavior);
}
/// 丢弃行为枚举
#[derive(Debug, Clone, Copy)]
pub enum DropBehavior {
/// 自动提交
Commit,
/// 自动回滚
Rollback,
}
#### 4.1.3 连接管理接口
基于 DuckDB-RS 的连接特性,设计连接管理接口:
```rust
/// 连接管理器
///
/// 管理 DuckDB 连接的生命周期和并发访问
pub trait ConnectionManager: Send + Sync {
/// 获取主连接
fn get_connection(&self) -> Result<&Connection, StorageError>;
/// 创建工作连接(用于并发操作)
fn create_worker_connection(&self) -> Result<WorkerConnection, StorageError>;
/// 健康检查
fn health_check(&self) -> Result<(), StorageError>;
}
/// 工作连接
///
/// 支持独立关闭的连接副本
pub struct WorkerConnection {
connection: Option<Arc<Mutex<Connection>>>,
}
impl WorkerConnection {
/// 执行查询
pub fn execute<F, T>(&self, f: F) -> Result<T, StorageError>
where
F: FnOnce(&Connection) -> Result<T, StorageError>;
/// 关闭连接
pub fn close(self) -> Result<(), StorageError>;
}
4.1.3 轻量存储门面接口
/// 轻量存储接口
///
/// 提供对所有仓储的统一访问,大部分操作无需事务
pub trait UnifiedStorage: Send + Sync {
/// 获取容器仓储
fn containers(&self) -> &dyn ContainerRepository;
/// 获取项目仓储
fn projects(&self) -> &dyn ProjectRepository;
/// session功能已合并到projects仓储中
/// 获取事务管理器(仅状态变更使用)
fn transaction(&self) -> &dyn TransactionManager;
/// 轻量项目创建:顺序执行,无强事务保证
fn create_project_with_container(
&self,
project: &ProjectRecord,
container: &ContainerRecord,
) -> Result<(), StorageError>;
/// 轻量会话创建
fn create_session_for_project(
&self,
session: &SessionRecord,
) -> Result<(), StorageError>;
/// 原子性状态变更:使用事务保证状态变更的原子性
fn update_project_status_atomic(
&self,
project_id: &str,
new_status: Option<AgentStatus>,
) -> Result<(), StorageError>;
/// 轻量项目清理
fn cleanup_project(&self, project_id: &str) -> Result<CleanupResult, StorageError>;
/// ComputerAgentRunner 用户清理
fn cleanup_user(&self, user_id: &str) -> Result<CleanupResult, StorageError>;
/// 根据 session_id 获取项目信息
fn get_project_info_by_session(
&self,
session_id: &str,
) -> Result<Option<ProjectInfo>, StorageError>;
/// 根据项目 ID 获取项目信息
fn get_project_info_by_id(&self, project_id: &str) -> Result<Option<ProjectInfo>, StorageError>;
/// 获取指定服务类型的所有项目
fn get_projects_by_service_type(&self, service_type: ServiceType) -> Result<Vec<ProjectInfo>, StorageError>;
/// 获取用户的所有项目
fn get_user_projects(&self, user_id: &str) -> Result<Vec<ProjectInfo>, StorageError>;
}
4.2 数据记录结构
基于模块化设计原则,将数据结构按使用范围进行分类:
4.2.1 shared_types 模块(公共结构体)
// crates/shared_types/src/storage.rs
/// 容器记录 - 公共结构体,多模块使用
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContainerRecord {
pub container_id: String,
pub container_name: String,
pub container_ip: String,
pub internal_port: u16,
pub external_port: u16,
pub service_type: ServiceType,
pub status: String,
pub service_url: String,
pub created_at: DateTime<Utc>,
pub last_activity: DateTime<Utc>,
}
/// Agent 状态枚举
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AgentStatus {
/// 活跃状态 - 正在处理请求
Active = 0,
/// 空闲状态 - 等待新请求
Idle = 1,
/// 正在终止
Terminating = 2,
}
impl AgentStatus {
/// 获取状态码
pub fn code(&self) -> i32 {
*self as i32
}
/// 获取状态名称
pub fn name(&self) -> &'static str {
match self {
AgentStatus::Active => "Active",
AgentStatus::Idle => "Idle",
AgentStatus::Terminating => "Terminating",
}
}
/// 从状态码创建枚举
pub fn from_code(code: i32) -> Option<Self> {
match code {
0 => Some(AgentStatus::Active),
1 => Some(AgentStatus::Idle),
2 => Some(AgentStatus::Terminating),
_ => None,
}
}
/// 从状态名称创建枚举
pub fn from_name(name: &str) -> Option<Self> {
match name {
"Active" => Some(AgentStatus::Active),
"Idle" => Some(AgentStatus::Idle),
"Terminating" => Some(AgentStatus::Terminating),
_ => None,
}
}
}
/// 统一项目记录 - 公共结构体,业务逻辑层和存储层都需要
/// 遵循第一范式设计,通过 service_type 字段区分不同业务场景
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectRecord {
pub project_id: String,
pub service_type: ServiceType,
pub container_id: String,
pub user_id: Option<String>, // ComputerAgentRunner 模式时使用,RCoder 模式为 None
pub session_id: Option<String>,
pub agent_status: Option<AgentStatus>, // 内存中使用枚举,数据库中存储为code和name
pub request_id: Option<String>,
pub model_provider: Option<ModelProviderConfig>,
pub created_at: DateTime<Utc>,
pub last_activity: DateTime<Utc>,
}
/// 统一会话记录 - 公共结构体
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionRecord {
pub session_id: String,
pub project_id: String,
pub container_id: String,
pub service_type: ServiceType,
pub created_at: DateTime<Utc>,
pub last_activity: DateTime<Utc>,
}
/// 项目信息(统一的返回类型)- 公共结构体
#[derive(Debug, Clone)]
pub struct ProjectInfo {
pub project: ProjectRecord,
pub container: ContainerRecord,
}
4.2.2 duckdb_manager 模块(专用结构体)
// crates/duckdb_manager/src/models.rs
/// 清理结果 - 存储层专用
#[derive(Debug, Clone)]
pub struct CleanupResult {
pub containers_deleted: usize,
pub projects_deleted: usize,
pub sessions_deleted: usize,
}
/// 存储统计信息 - 存储层专用
#[derive(Debug, Clone, Default)]
pub struct StorageStats {
pub total_containers: usize,
pub total_projects: usize,
pub active_sessions: usize, // 有活跃 session_id 的项目数
pub active_containers: usize, // 状态为 Active 的项目关联的容器数
pub idle_containers: usize, // 状态为 Idle 的项目关联的容器数
/// 按服务类型统计的项目数
pub projects_by_service_type: std::collections::HashMap<ServiceType, usize>,
}
/// 数据库专用配置 - 存储层专用
#[derive(Debug, Clone)]
pub struct DuckDbConfig {
pub max_connections: u32,
pub connection_timeout: Duration,
pub enable_wal_mode: bool,
}
4.3 错误类型
4.3.1 shared_types 模块(公共错误类型)
// crates/shared_types/src/storage.rs
/// 存储层公共错误类型 - 多模块使用
#[derive(Debug, thiserror::Error)]
pub enum StorageError {
#[error("连接错误: {0}")]
ConnectionError(String),
#[error("未找到: 实体 {entity}, ID {id}")]
NotFound { entity: String, id: String },
#[error("约束违反: {0}")]
ConstraintViolation(String),
}
4.3.2 duckdb_manager 模块(专用错误类型)
// crates/duckdb_manager/src/error.rs
/// DuckDB 专用错误类型 - 存储层内部使用
#[derive(Debug, thiserror::Error)]
pub enum DuckDbError {
#[error("数据库连接错误: {0}")]
ConnectionError(String),
#[error("SQL 查询错误: {0}")]
QueryError(String),
#[error("事务错误: {0}")]
TransactionError(String),
#[error("配置错误: {0}")]
ConfigError(String),
#[error("数据迁移错误: {0}")]
MigrationError(String),
#[error("序列化错误: {0}")]
SerializationError(String),
#[error("初始化错误: {0}")]
InitializationError(String),
#[error("重复记录: {entity} with id {id}")]
DuplicateRecord { entity: String, id: String },
// === 新增的错误类型(基于 DuckDB-RS 实际使用) ===
#[error("Appender 错误: {0}")]
AppenderError(String),
#[error("并发访问错误: {0}")]
ConcurrencyError(String),
#[error("连接克隆失败: {0}")]
ConnectionCloneError(String),
#[error("类型转换错误: 列 {column} 期望 {expected}, 实际 {actual}")]
TypeConversionError {
column: String,
expected: String,
actual: String,
},
#[error("查询结果映射错误: {0}")]
RowMappingError(String),
#[error("参数绑定错误: 参数 {index} - {message}")]
ParameterBindError { index: usize, message: String },
}
impl From<duckdb::Error> for DuckDbError {
fn from(err: duckdb::Error) -> Self {
match err {
duckdb::Error::QueryReturnedNoRows => {
DuckDbError::QueryError("查询结果为空".to_string())
}
duckdb::Error::InvalidColumnType(idx, name, ty) => {
DuckDbError::TypeConversionError {
column: name,
expected: format!("column {}", idx),
actual: format!("{:?}", ty),
}
}
_ => DuckDbError::QueryError(err.to_string()),
}
}
}
/// 将 DuckDbError 转换为公共 StorageError
impl From<DuckDbError> for StorageError {
fn from(err: DuckDbError) -> Self {
match err {
DuckDbError::ConnectionError(msg) => StorageError::ConnectionError(msg),
DuckDbError::DuplicateRecord { entity, id } => {
StorageError::ConstraintViolation(format!("重复的 {} 记录: {}", entity, id))
}
_ => StorageError::ConnectionError(err.to_string()),
}
}
}
4.4 结构体依赖关系说明
4.4.1 公共结构体 (shared_types)
以下结构体被多个模块使用,应放在 crates/shared_types 中:
| 结构体 | 使用模块 | 说明 |
|---|---|---|
ContainerRecord |
rcoder, duckdb_manager | 容器基本信息 |
ProjectRecord |
rcoder, duckdb_manager | 项目状态信息 |
SessionRecord |
rcoder, duckdb_manager | 会话映射信息 |
ProjectInfo |
rcoder, duckdb_manager | 完整的项目信息 |
StorageError |
rcoder, duckdb_manager | 存储层公共错误 |
4.4.2 专用结构体 (duckdb_manager)
以下结构体仅在存储层内部使用:
| 结构体 | 说明 |
|---|---|
CleanupResult |
清理操作结果统计 |
StorageStats |
存储层统计信息 |
DuckDbConfig |
数据库连接配置 |
DuckDbError |
数据库专用错误 |
4.4.3 依赖关系图
crates/rcoder
├── 使用 shared_types::ContainerRecord
├── 使用 shared_types::ProjectRecord
├── 使用 shared_types::StorageError
└── 调用 duckdb_manager 接口
crates/duckdb_manager
├── 定义专用结构体 (CleanupResult, StorageStats等)
├── 使用 shared_types 公共结构体
└── 实现基于 shared_types 的接口
crates/shared_types
└── 定义多模块共享的结构体和错误类型
5. 实现结构设计
5.1 模块架构
crates/
├── duckdb_manager/ # 新增:DuckDB 管理模块
│ ├── Cargo.toml # 模块配置
│ └── src/
│ ├── lib.rs # 库入口,导出公共接口
│ ├── error.rs # 数据库错误类型定义
│ ├── models.rs # 数据模型定义
│ ├── connection.rs # 数据库连接管理
│ ├── schema.rs # 表结构定义和初始化
│ ├── repositories/ # 仓储层实现
│ │ ├── mod.rs # 仓储模块入口
│ │ ├── container.rs # 容器仓储实现
│ │ └── project.rs # 统一项目仓储实现(已包含session功能)
│ ├── storage.rs # 统一存储门面实现
│ └── manager.rs # 存储管理器,全局实例管理
└── rcoder/ # 业务层使用模块
└── src/
├── storage/ # 适配层(可选)
│ ├── mod.rs # 适配器模块
│ ├── adapters.rs # DashMap 兼容适配器
│ └── bridge.rs # 与 duckdb_manager 的桥接
└── ... # 其他业务代码
5.2 duckdb_manager 模块职责
crates/duckdb_manager 作为独立的数据库管理模块,提供:
- 数据访问接口: 统一的仓储接口和数据模型,遵循第一范式设计
- 连接管理: DuckDB 连接的创建、维护和管理
- 事务支持: 跨表操作的事务管理
- 数据模型定义: 通过 service_type 字段区分不同业务场景,避免表分裂
- 错误处理: 统一的数据库错误类型和处理
- 初始化管理: 数据库表结构初始化和管理,维护数据完整性约束
5.3 rcoder 模块职责
crates/rcoder 作为业务层模块:
- 业务逻辑: 实现具体的业务规则和流程
- 适配层: 通过适配器提供与 DashMap 兼容的接口
- 桥接调用: 调用
duckdb_manager提供的接口 - 状态管理: 管理应用状态,协调各个组件
5.4 核心实现结构
5.4.1 duckdb_manager 核心结构
// lib.rs - 主要导出接口
pub mod error;
pub mod models;
pub mod repositories;
pub mod storage;
pub use storage::{StorageManager, ContainerRepository, /* ... */};
pub use manager::{init_storage, get_storage};
/// 存储管理器接口(已优化)
pub trait StorageManager: Send + Sync {
fn containers(&self) -> &dyn ContainerRepository;
fn projects(&self) -> &dyn ProjectRepository; // 已包含session功能
fn transaction(&self) -> &dyn TransactionManager;
}
/// 全局存储管理
pub struct DuckDbManager {
storage: Arc<DuckDbStorageManager>,
}
impl DuckDbManager {
pub fn new() -> Result<Self, StorageError>;
pub fn get_storage(&self) -> Arc<dyn StorageManager>;
}
/// 轻量数据操作实现示例
impl StorageManager for DuckDbManager {
/// 轻量项目创建(大部分操作无需事务)
fn create_project(&self, project: &ProjectRecord) -> Result<(), StorageError> {
// 1. 轻量检查容器是否存在
if self.containers().find_by_id(&project.container_id)?.is_none() {
return Err(StorageError::NotFound {
entity: "Container".to_string(),
id: project.container_id.clone(),
});
}
// 2. 业务规则通过 CHECK 约束保证,无需额外检查
// 3. 创建项目(轻量操作)
self.projects().upsert(project)
}
/// 原子性状态更新(需要事务保证)
fn update_project_status_atomic(&self, project_id: &str, status: Option<AgentStatus>) -> Result<(), StorageError> {
// 使用事务保证状态变更的原子性
self.transaction().execute_status_update(|| {
self.projects().update_status_atomic(project_id, status)
})
}
/// 内存模式初始化:每次启动都是新数据库
fn initialize_memory_database(&self) -> Result<(), StorageError> {
info!("初始化 DuckDB 内存数据库");
// 创建内存数据库连接
// 初始化表结构和索引
// 返回成功表示初始化完成
Ok(())
}
}
5.4.2 rcoder 适配层结构
/// rcoder 模块中的适配器
pub mod storage {
use duckdb_manager::{StorageManager, /* ... */};
/// DashMap 兼容适配器
pub struct RCoderProjectAdapter {
storage: Arc<dyn StorageManager>,
}
impl RCoderProjectAdapter {
pub fn get(&self, project_id: &str) -> Option<Arc<ProjectAndContainerInfo>>;
pub fn insert(&self, project_id: String, info: Arc<ProjectAndContainerInfo>);
// ... 其他 DashMap 兼容方法
}
}
5.5 依赖关系
crates/rcoder
↓ (使用接口)
crates/duckdb_manager
↓ (封装)
duckdb crate
duckdb_manager只依赖duckdbcrate 和基础类型rcoder依赖duckdb_manager提供的接口- 其他模块通过
rcoder提供的适配器使用数据
6. 迁移方案
6.1 迁移步骤
阶段一:并行运行(保守策略)
- 添加 DuckDB 依赖: 在
Cargo.toml中添加duckdbcrate - 实现存储层: 完成所有 Trait 的 DuckDB 实现
- 双写模式: 在业务层同时写入 DashMap 和 DuckDB
- 读取验证: 定期对比两个存储的数据一致性
阶段二:切换读取
- 切换读取源: 将读取操作切换到 DuckDB
- 保留 DashMap 写入: 作为备份和回滚方案
- 监控验证: 确保功能正常运行
阶段三:完全迁移
- 移除 DashMap 写入: 只保留 DuckDB 写入
- 移除 DashMap 字段: 从 AppState 中移除 DashMap
- 清理代码: 删除旧的 DashMap 相关代码
6.2 兼容性适配
为了平滑迁移,提供与原 DashMap 操作兼容的适配层:
6.2.1 统一项目适配器
/// 统一项目适配器
///
/// 模拟原有的 project_and_agent_map DashMap 的行为
pub struct ProjectAdapter {
storage: Arc<dyn UnifiedStorage>,
}
impl ProjectAdapter {
/// 模拟 DashMap.get() 操作
pub fn get(&self, key: &str) -> Option<Arc<ProjectAndContainerInfo>> {
// 先尝试按项目ID查找
if let Ok(Some(project_info)) = self.storage.get_project_info_by_id(key) {
return Some(Arc::new(self.build_project_and_container_info(project_info)));
}
// 如果没找到,尝试按用户ID查找(ComputerAgentRunner模式)
if let Ok(projects) = self.storage.get_user_projects(key) {
if !projects.is_empty() {
// 返回用户的第一个项目作为代表
return Some(Arc::new(self.build_user_container_info(key, &projects[0])));
}
}
None
}
/// 模拟 DashMap.insert() 操作
pub fn insert(&self, key: String, info: Arc<ProjectAndContainerInfo>) -> Result<(), StorageError> {
// 根据 ProjectAndContainerInfo 的内容判断是哪种类型的插入
let project_record = self.extract_project_record(&info)?;
let container_record = self.extract_container_record(&info)?;
self.storage.create_project_with_container(&project_record, &container_record)
}
/// 模拟 DashMap.entry() 操作
pub fn entry(&self, key: String) -> ProjectEntry;
/// 模拟 DashMap.remove() 操作
pub fn remove(&self, key: &str) -> Option<Arc<ProjectAndContainerInfo>>;
/// 模拟 DashMap.iter() 操作
pub fn iter(&self) -> impl Iterator<Item = (String, Arc<ProjectAndContainerInfo>)>;
/// 模拟 DashMap.contains_key() 操作
pub fn contains_key(&self, key: &str) -> bool;
// === Session 相关方法(合并到 ProjectAdapter)===
/// 根据 session_id 获取项目信息(替代原 sessions DashMap)
pub fn get_by_session_id(&self, session_id: &str) -> Option<Arc<ProjectAndContainerInfo>> {
self.storage.get_project_info_by_session(session_id)
.ok()
.flatten()
.map(|info| Arc::new(self.build_project_and_container_info(info)))
}
/// 根据 session_id 获取 container_id(替代原 session_to_container_id DashMap)
pub fn get_container_id_by_session(&self, session_id: &str) -> Option<String> {
self.storage.projects().get_container_id_by_session(session_id).ok().flatten()
}
/// 更新项目的 session 信息
pub fn update_session(&self, project_id: &str, session_id: &str) -> Result<(), StorageError> {
self.storage.projects().update_session(project_id, Some(session_id), Some(Utc::now()))
}
}
6.2.3 AppState 适配器(简化版)
/// 适配后的 AppState
///
/// 通过统一的 ProjectAdapter 替代原来的 3 个 DashMap:
/// - project_and_agent_map → projects.get() / projects.insert()
/// - sessions → projects.get_by_session_id()
/// - session_to_container_id → projects.get_container_id_by_session()
pub struct AdaptedAppState {
/// 统一项目适配器(包含项目和会话的所有操作)
pub projects: ProjectAdapter,
/// 其他原有字段保持不变
pub config: AppConfig,
pub pingora_service: Option<Arc<PingoraProxyService>>,
pub grpc_pool: Arc<GrpcChannelPool>, // gRPC 连接池保持使用 DashMap
}
impl AdaptedAppState {
/// 兼容原 sessions DashMap 的访问方式
pub fn get_project_by_session(&self, session_id: &str) -> Option<Arc<ProjectAndContainerInfo>> {
self.projects.get_by_session_id(session_id)
}
/// 兼容原 session_to_container_id DashMap 的访问方式
pub fn get_container_id_by_session(&self, session_id: &str) -> Option<String> {
self.projects.get_container_id_by_session(session_id)
}
}
7. 查询优化
7.1 常用查询 SQL
7.1.1 统计查询(无时间范围限制)
-- 统计各服务类型的容器数量
SELECT service_type, COUNT(*) as container_count
FROM containers
GROUP BY service_type;
-- 统计 RCoder 模式的项目数量
SELECT COUNT(*) as rcoder_project_count
FROM projects
WHERE service_type = 'RCoder';
-- 统计 ComputerAgentRunner 模式的项目数量
SELECT COUNT(*) as computer_project_count
FROM projects
WHERE service_type = 'ComputerAgentRunner';
-- 按服务类型统计项目数量
SELECT service_type, COUNT(*) as project_count
FROM projects
GROUP BY service_type;
7.1.2 闲置清理查询(带时间范围和保护期)
-- 通用闲置项目查询(带保护期)
SELECT p.*, c.*
FROM projects p
JOIN containers c ON p.container_id = c.container_id
WHERE p.last_activity < NOW() - INTERVAL ? SECONDS -- 闲置阈值
AND c.created_at < NOW() - INTERVAL ? SECONDS -- 保护期
AND (p.agent_status_code IS NULL OR p.agent_status_code = 1) -- Idle 或空状态
AND (? IS NULL OR p.service_type = ?); -- 可选服务类型过滤
-- RCoder 模式:闲置项目查询
SELECT p.*, c.*
FROM projects p
JOIN containers c ON p.container_id = c.container_id
WHERE p.service_type = 'RCoder'
AND p.last_activity < NOW() - INTERVAL ? SECONDS
AND c.created_at < NOW() - INTERVAL ? SECONDS -- 5分钟保护期
AND (p.agent_status_code IS NULL OR p.agent_status_code = 1);
-- ComputerAgentRunner 模式:闲置项目查询
SELECT p.*, c.*
FROM projects p
JOIN containers c ON p.container_id = c.container_id
WHERE p.service_type = 'ComputerAgentRunner'
AND p.last_activity < NOW() - INTERVAL ? SECONDS
AND c.created_at < NOW() - INTERVAL ? SECONDS -- 5分钟保护期
AND (p.agent_status_code IS NULL OR p.agent_status_code = 1);
7.1.3 业务查询
-- 根据 session_id 查找项目和容器信息(单表优化后)
SELECT p.*, c.*
FROM projects p
JOIN containers c ON p.container_id = c.container_id
WHERE p.session_id = ?;
-- 获取 RCoder 模式的所有项目
SELECT p.*, c.*
FROM projects p
JOIN containers c ON p.container_id = c.container_id
WHERE p.service_type = 'RCoder'
ORDER BY p.last_activity DESC;
-- 获取 ComputerAgentRunner 模式的所有项目
SELECT p.*, c.*
FROM projects p
JOIN containers c ON p.container_id = c.container_id
WHERE p.service_type = 'ComputerAgentRunner'
ORDER BY p.last_activity DESC;
-- 获取指定用户的所有项目(ComputerAgentRunner 模式)
SELECT p.*, c.*
FROM projects p
JOIN containers c ON p.container_id = c.container_id
WHERE p.user_id = ?
ORDER BY p.last_activity DESC;
7.1.4 维护查询
-- 查找孤立容器(没有关联项目的容器)
SELECT c.*
FROM containers c
LEFT JOIN projects p ON c.container_id = p.container_id
WHERE p.container_id IS NULL;
-- 查找没有容器的项目(数据一致性检查)
SELECT p.*
FROM projects p
LEFT JOIN containers c ON p.container_id = c.container_id
WHERE c.container_id IS NULL;
-- 查找 session 已过期但项目仍存在的记录
SELECT p.*
FROM projects p
WHERE p.session_id IS NOT NULL
AND p.session_last_activity < NOW() - INTERVAL ? SECONDS;
7.2 索引策略
基于数据量较小的约束,索引策略以查询效率和维护简单性为优先:
7.2.1 主要索引
| 表名 | 索引列 | 用途 | 优先级 |
|---|---|---|---|
| containers | container_id (PK) |
容器查找 | 高 |
service_type |
按类型统计 | 中 | |
last_activity |
闲置检测 | 中 | |
| projects | project_id (PK) |
项目查找 | 高 |
container_id |
容器关联 | 高 | |
session_id |
会话查找(SSE消息转发核心路径) | 高 | |
user_id |
用户关联(ComputerAgentRunner模式) | 中 | |
last_activity |
闲置检测 | 中 | |
service_type |
类型筛选 | 中 | |
agent_status_code |
状态查询 | 中 | |
(agent_status_code, last_activity) |
清理任务复合查询 | 中 | |
(service_type, last_activity) |
按类型闲置检测 | 低 |
7.2.2 索引设计原则
- 主键索引: 所有表的主键自动建立索引
- 关联字段索引: 重要关联字段建立索引,保证关联查询性能
- 查询索引: 针对频繁查询字段建立索引
- 简化策略: 数据量小的情况下,避免过度索引,减少维护成本
8. 性能考量
8.1 轻量性能特点
基于数据量小和轻量事务要求的约束,设计重点关注以下性能特点:
8.1.1 优势场景
- 高频轻量更新:
last_activity时间戳更新,无事务开销 - 简单查询: 基于主键或少量索引的快速查询
- 统计查询: 全表统计,无需复杂的时间范围过滤
- 内存模式: DuckDB 内存数据库的快速访问特性
8.1.2 关键性能点
- 事务仅用于状态变更: 只有
agent_status更新需要事务,其他操作轻量 - 状态码索引优化: 数字状态码查询比字符串更高效
- 复合索引: 状态+时间的复合索引优化清理任务
- 索引优化: 基于数据量小,精简索引设计
- 并发友好: 大部分操作无需锁,减少竞争
8.2 性能优化策略
8.2.1 查询优化
- 预编译语句: 对常用查询预编译,提高执行效率
- 批量操作: 使用事务批量处理多条记录
- 异步包装: 虽然数据量小,仍使用
spawn_blocking避免阻塞 Tokio 线程 - 结果缓存: 对于统计查询,可以考虑内存缓存结果
8.2.2 索引优化
基于数据量小但查询频繁的特点:
- 核心索引: 主键、关联字段建立索引
- 查询索引:
session_id、last_activity等查询频繁字段 - 简化策略: 避免过度索引,平衡查询和维护成本
8.2.3 内存和存储优化
- 内存模式: 充分利用 DuckDB 内存模式特性
- 数据清理: 及时清理过期数据,控制内存占用
- 简单设计: 无需复杂的分页、压缩等大数据优化策略
8.2.4 业务层优化
- 异步更新: 活动时间更新异步处理,避免影响主流程
- 延迟写入: 非关键更新可以延迟批量提交
- 读写分离: 考虑读写操作的并发优化
8.2 优化策略
- 连接池: 使用连接池避免频繁创建连接
- 预编译语句: 缓存常用 SQL 的预编译语句
- 批量操作: 使用事务批量处理多条记录
- 异步包装: 使用
spawn_blocking包装同步 DuckDB 调用
9. 测试计划
9.1 单元测试
9.1.1 仓储层测试
- ContainerRepository: 容器 CRUD 操作测试
- ProjectRepository: 统一项目管理(支持RCoder和ComputerAgentRunner两种模式)
- ProjectRepository: 项目和会话统一 CRUD 操作测试
9.1.2 业务逻辑测试
- 事务操作的原子性测试(跨表操作)
- 错误处理和边界条件测试
- 数据一致性约束测试
9.2 集成测试
9.2.1 业务场景测试
- RCoder 模式: 完整的项目创建到清理流程
- ComputerAgentRunner 模式: 用户容器创建到项目管理的完整流程
- 混合模式: 两种模式并存时的相互影响测试
9.2.2 适配层测试
- DashMap 兼容适配器的功能测试
- 双写模式下的数据一致性测试
- 迁移过程中的数据同步测试
9.2.3 清理任务测试
- 闲置检测逻辑的准确性测试
- 清理操作的完整性测试(级联删除)
- 保护期机制的正确性测试
9.3 性能测试
9.3.1 基础性能测试
- 单操作性能: CRUD 操作的响应时间测试
- 并发性能: 正常业务负载下的并发读写测试
- 统计查询性能: 全表统计查询的响应时间测试
9.3.2 业务场景性能测试
- 频繁更新场景: 活动时间更新的性能测试
- 清理任务场景: 闲置检测和清理操作的性能测试
- 混合负载场景: 正常业务操作的综合性能测试
9.3.3 与 DashMap 对比测试
- 功能对比: 确保所有操作功能正确
- 性能对比: 正常数据量下的性能对比测试
- 内存占用对比: 相同数据量下的内存使用情况对比
10. 风险评估
10.1 轻量设计风险评估
| 风险 | 影响程度 | 缓解措施 |
|---|---|---|
| 状态变更原子性丢失 | 高 | 明确标识需要事务的操作 |
| 模块接口不匹配 | 中 | 定义清晰的轻量接口 |
| 跨模块依赖管理 | 低 | 简单的依赖关系 |
| 轻量操作一致性 | 中 | 业务层保证基本一致性 |
| DuckDB 兼容性问题 | 低 | 版本锁定 + 充分测试 |
| 内存模式连接问题 | 低 | 每次启动都是新数据库,连接问题影响小 |
10.2 回滚方案
- 保留 DashMap 代码至少一个版本周期
- 通过配置开关控制使用哪个存储后端
- 准备快速回滚脚本和文档
11. 依赖管理
11.1 模块依赖配置
11.1.1 crates/duckdb_manager/Cargo.toml
[package]
name = "duckdb_manager"
version = "0.1.0"
edition = "2021"
[dependencies]
# 基础依赖
serde = { version = "1.0", features = ["derive"] }
chrono = { version = "0.4", features = ["serde"] }
thiserror = "1.0"
tokio = { version = "1.0", features = ["sync"] }
# DuckDB 相关
duckdb = { version = "1.1", features = ["bundled"] }
# 共享类型 - 必须依赖,包含公共结构体和错误类型
shared_types = { path = "../shared_types" }
[lib]
name = "duckdb_manager"
path = "src/lib.rs"
11.1.2 crates/rcoder/Cargo.toml (更新)
[dependencies]
# 共享类型 - 包含公共结构体 (ContainerRecord, ProjectRecord, StorageError等)
shared_types = { path = "../shared_types" }
# Docker 管理器
docker_manager = { path = "../docker_manager" }
# 其他原有依赖...
# 新增:DuckDB 存储管理器 - 提供存储接口实现
duckdb_manager = { path = "../duckdb_manager" }
11.2 Feature 说明
bundled: 内置 DuckDB 库,无需系统安装- 模块分离:
duckdb_manager作为独立 crate,只包含数据库相关逻辑 - 接口依赖:
rcoder通过接口调用duckdb_manager,不直接依赖duckdb
12. 时间规划
12.1 开发阶段规划
| 阶段 | 任务 | 预计时间 | 主要交付物 |
|---|---|---|---|
| 1 | 需求分析与设计评审 | 2 天 | 完整的数据库设计文档 |
| 2 | duckdb_manager 模块搭建 | 4 天 | 新模块创建、基础结构、依赖配置 |
| 3 | 数据模型与接口定义 | 3 天 | 数据结构、Trait 定义、错误类型 |
| 4 | 数据库核心实现 | 5 天 | 仓储实现、连接管理、事务支持 |
| 5 | rcoder 适配层实现 | 4 天 | DashMap 兼容适配器、桥接调用 |
| 6 | 单元测试 | 4 天 | 两个模块的完整测试覆盖 |
| 7 | 集成测试 | 4 天 | 跨模块集成测试、接口验证 |
| 8 | 性能测试与优化 | 3 天 | 性能基准测试、查询优化 |
| 9 | 灰度迁移 | 3 天 | 双写模式、数据一致性验证 |
| 10 | 完全迁移 | 2 天 | 移除 DashMap、清理遗留代码 |
总计: 约 30 个工作日 (增加模块分离设计,时间略有增加)
12.2 里程碑
里程碑 1: 核心框架完成 (第 5 天)
- ✅ 数据库表结构设计完成
- ✅ 所有 Trait 定义完成
- ✅ 核心数据结构实现
- ✅ 单元测试框架搭建
里程碑 2: 仓储层完成 (第 10 天)
- ✅ 所有 Repository 实现完成
- ✅ 事务支持实现
- ✅ 基础 CRUD 操作测试通过
- ✅ 性能基准测试完成
里程碑 3: 适配层完成 (第 13 天)
- ✅ DashMap 兼容适配器实现
- ✅ 业务逻辑适配完成
- ✅ 集成测试通过
里程碑 4: 测试验证完成 (第 20 天)
- ✅ 完整测试覆盖
- ✅ 性能优化完成
- ✅ 与现有系统集成验证
里程碑 5: 生产就绪 (第 30 天)
- ✅ 灰度迁移完成
- ✅ 完全迁移完成
- ✅ 监控和运维文档完成
12.3 风险控制
| 风险点 | 应对措施 | 时间缓冲 |
|---|---|---|
| DuckDB 性能不达标 | 提前进行性能测试,有备选方案 | +2 天 |
| 业务逻辑适配复杂 | 分阶段实现,先 RCoder 后 ComputerAgentRunner | +3 天 |
| 测试覆盖不足 | 制定详细测试计划,自动化测试 | +2 天 |
| 迁移过程数据不一致 | 实现数据校验工具,双写模式验证 | +3 天 |
13. 模块架构设计
13.1 crates/duckdb_manager 模块设计
13.1.1 模块职责
crates/duckdb_manager 是专门的数据库管理模块,职责如下:
- 数据访问抽象: 提供统一的仓储接口,封装所有数据库操作
- 连接生命周期管理: 负责 DuckDB 连接的创建、维护和关闭
- 事务管理: 支持跨表操作的原子性事务
- 数据模型定义: 定义所有数据结构和类型
- 错误处理: 统一的数据库错误类型和处理逻辑
- 模式管理: 数据库表结构初始化和管理
13.1.2 接口设计原则
// lib.rs - 主要的公共接口
pub mod error;
pub mod models;
pub mod repositories;
pub mod storage;
// 核心接口导出
pub use storage::{StorageManager, ContainerRepository, /* ... */};
pub use manager::{init_storage, get_storage};
13.1.3 仓储接口示例
/// 容器仓储接口
pub trait ContainerRepository: Send + Sync {
fn upsert(&self, container: &ContainerRecord) -> Result<(), StorageError>;
fn find_by_id(&self, container_id: &str) -> Result<Option<ContainerRecord>, StorageError>;
fn find_by_service_type(&self, service_type: ServiceType) -> Result<Vec<ContainerRecord>, StorageError>;
fn count(&self) -> Result<usize, StorageError>;
fn bulk_insert(&self, containers: &[ContainerRecord]) -> Result<(), StorageError>;
// ... 其他方法
}
13.1.4 存储管理器接口
/// 统一存储管理器接口
pub trait StorageManager: Send + Sync {
fn containers(&self) -> &dyn ContainerRepository;
fn projects(&self) -> &dyn ProjectRepository; // 已合并所有功能
fn transaction(&self) -> &dyn TransactionManager;
}
13.2 crates/rcoder 适配层设计
13.2.1 适配器模式
crates/rcoder 通过适配器模式提供与现有代码兼容的接口:
/// DashMap 兼容适配器
pub struct RCoderProjectAdapter {
storage: Arc<dyn StorageManager>, // 调用 duckdb_manager 接口
}
impl RCoderProjectAdapter {
/// 模拟 DashMap.get() 操作
pub fn get(&self, project_id: &str) -> Option<Arc<ProjectAndContainerInfo>> {
// 内部调用 duckdb_manager 的接口
// 转换数据格式后返回
}
}
13.2.2 桥接设计
/// 桥接模块:连接业务逻辑和数据访问
pub mod bridge {
use duckdb_manager::StorageManager;
pub struct StorageBridge {
manager: Arc<dyn StorageManager>,
}
impl StorageBridge {
// 业务特定的复合操作(通过 service_type 区分业务场景)
pub fn create_project_with_session(&self, /* ... */) -> Result<(), BridgeError>;
}
}
13.3 依赖关系与隔离
13.3.1 依赖层次
crates/rcoder (业务层)
↓ (接口调用)
crates/duckdb_manager (数据访问层)
↓ (直接依赖)
duckdb crate + shared_types
13.3.2 隔离原则
- 接口隔离:
rcoder只依赖duckdb_manager的接口,不依赖具体实现 - 数据隔离: 数据库操作完全封装在
duckdb_manager中 - 错误隔离: 数据库错误转换为业务错误,避免泄露实现细节
- 测试隔离: 各模块可独立测试,通过接口 mock 解耦
13.3.3 优势
- 职责分离: 数据库操作与业务逻辑分离
- 可维护性: 修改数据库实现不影响业务代码
- 可测试性: 接口化设计便于单元测试和集成测试
- 可扩展性: 可轻松更换底层数据库实现
14. 设计合理性分析
14.1 DuckDB方案的合理性评估
基于实际使用场景分析,我们的DuckDB设计是合理的选择:
14.1.1 场景匹配度
| 需求特点 | DashMap | 简单内存结构 | SQLite内存 | DuckDB内存 | 评估 |
|---|---|---|---|---|---|
| 并发访问 | ✅ 原生支持 | ❌ 需要锁 | ⚠️ 有限支持 | ✅ 连接克隆 | DuckDB优 |
| 复杂查询 | ❌ 仅精确查找 | ❌ 仅精确查找 | ✅ SQL查询 | ✅ SQL查询 | DuckDB/SQLite优 |
| 事务保证 | ⚠️ 应用层保证 | ❌ 无 | ✅ ACID | ✅ ACID | SQLite/DuckDB优 |
| 统计功能 | ❌ 手动实现 | ❌ 手动实现 | ✅ SQL聚合 | ✅ SQL聚合 | DuckDB/SQLite优 |
| 内存模式 | ✅ | ✅ | ✅ | ✅ | 都支持 |
| 开发复杂度 | ⚠️ 高(业务逻辑复杂) | ✅ 低 | ⚠️ 中 | ⚠️ 中 | 内存结构最简单 |
| 维护成本 | ✅ 低 | ✅ 低 | ⚠️ 中 | ⚠️ 中 | DashMap最简单 |
14.1.2 DuckDB的核心优势
- SQL查询能力: 支持复杂的统计和筛选,而不需要应用层实现
- 事务支持: 只在需要时使用事务,保证重要操作的原子性
- 类型安全: 通过Repository模式提供类型安全的数据访问
- 并发友好: 通过连接克隆支持并发访问
- 生态成熟: DuckDB-RS有完善的Rust绑定和活跃的社区
14.1.3 替代方案对比
方案A: 继续使用DashMap
// 优点:简单直接,性能极好
pub struct AppState {
pub sessions: DashMap<String, Arc<ProjectAndContainerInfo>>,
pub project_and_agent_map: DashMap<String, Arc<ProjectAndContainerInfo>>,
pub session_to_container_id: DashMap<String, String>,
}
优点:
- 性能最佳(内存操作,无序列化开销)
- 实现最简单,无额外依赖
- 并发访问原生支持
缺点:
- 统计查询需要手动遍历所有数据
- 复杂查询能力有限
- 类型安全依赖人工保证
方案B: 简单内存HashMap + 锁
// 优点:控制力强,资源占用少
pub struct InMemoryStorage {
projects: RwLock<HashMap<String, ProjectRecord>>,
containers: RwLock<HashMap<String, ContainerRecord>>,
sessions: RwLock<HashMap<String, SessionRecord>>,
}
优点:
- 完全控制,无外部依赖
- 内存占用最小
- 性能良好
缺点:
- 需要手动实现并发控制
- 统计查询效率低(需要遍历)
- 错误处理复杂
方案C: SQLite内存模式
-- SQLite内存数据库
.open :memory:
优点:
- 轻量级,SQL查询能力
- 文件格式兼容性好
- 生态完善
缺点:
- DuckDB在分析查询上有优势
- Rust绑定可能不如DuckDB-RS成熟
14.1.4 为什么DuckDB是最佳选择
- 功能平衡: 在性能、功能、复杂度间取得最佳平衡
- 渐进迁移: 可以逐步从DashMap迁移,不需要大爆炸式重构
- 扩展性: 为未来可能的持久化需求留有空间
- 学习成本: 基于SQL的查询语言,团队容易理解
14.2 设计优化建议
14.2.1 当前设计的合理改进
- 简化Repository接口: 移除一些不常用的方法
- 优化索引策略: 基于实际查询模式调整索引
- 连接池优化: 评估是否需要连接池
14.2.2 可能的进一步优化
- 异步接口: 如果业务层需要异步,可以考虑r2d2连接池
- 查询缓存: 对频繁查询添加内存缓存层
- 批量更新: 进一步优化批量操作的性能
15. 最终结论
15.1 设计决策总结
经过全面分析,我们的DuckDB内存模式设计并结合表合并优化是优秀且高性能的选择:
✅ 正确的决策
- 内存模式: 完全符合容器重启清空数据的需求
- 轻量事务: 只有
agent_status变更需要事务,其他操作无开销 - 表合并优化: projects和sessions表合并,大幅提升查询性能
- 状态字段优化: 状态码+状态名的设计提升查询性能和扩展性
- Repository模式: 提供了良好的抽象和类型安全
- 模块分离: duckdb_manager与业务逻辑解耦
🎯 核心优势
- 高性能查询: SSE消息转发等核心查询减少JOIN操作,性能显著提升
- 渐进式迁移: 可以平滑从DashMap过渡
- 功能完备: 支持所有现有需求,包括统计查询
- 架构简化: 从5表优化到2表(containers+projects),维护成本大幅降低
- 状态字段优化: 状态码+状态名的设计提升查询性能和扩展性
- 维护友好: SQL查询直观,调试容易
📊 与替代方案的对比
| 方案 | 复杂度 | 功能 | 性能 | 维护成本 | 推荐指数 |
|---|---|---|---|---|---|
| DuckDB内存 | 中 | 高 | 高 | 中 | ⭐⭐⭐⭐⭐ |
| DashMap继续使用 | 低 | 低 | 高 | 低 | ⭐⭐⭐ |
| 简单内存结构 | 低 | 低 | 高 | 低 | ⭐⭐ |
| SQLite内存 | 中 | 高 | 中 | 中 | ⭐⭐⭐⭐ |
15.2 实施建议
- 从小开始: 先实现核心CRUD操作,验证性能
- 逐步迁移: 可以先迁移部分功能,逐步替换DashMap
- 监控指标: 添加基本的性能监控(查询耗时、连接数等)
- 回滚计划: 保留DashMap代码一段时间,确保可以快速回滚
15.3 总结
DuckDB内存模式的设计是经过深思熟虑的解决方案,它在功能、性能、复杂度之间取得了良好的平衡。相比简单的内存结构,它提供了更强的查询能力和更好的可维护性;相比复杂的持久化方案,它保持了轻量和高效。
这个设计既解决了当前的技术债务,又为未来的扩展留下了空间,是一个务实且前瞻性的选择。
文档版本: 1.4 (补充边界场景、孤立容器检测、错误类型扩展、DuckDB-RS实现细节) 最后更新: 2025-12-19 状态: ✅ 设计完善,等待实施
14.1 重新设计的核心改进
基于用户反馈,这次重新设计主要解决了以下问题:
原始设计问题:
- 违反第一范式: 将业务相似的容器管理数据分散到多个表中
- 维护复杂性:
rcoder_projects和computer_users表差异很小,却需要分别维护 - 扩展困难: 新增业务场景需要新增表结构
重新设计解决方案:
- 第一范式优化: 合并为统一的
projects表,通过service_type字段区分业务场景 - 表合并优化: 将
sessions表合并到projects表,从 3 个表减少到 2 个表 - 查询性能提升: SSE消息转发等核心查询减少一次JOIN操作
- 状态字段优化: 状态码+状态名的设计提升查询性能和扩展性
- 无外键约束: 通过应用层保证数据一致性,避免数据库级联操作的复杂性
- 维护成本降低: 统一的仓储接口和业务逻辑,减少重复代码
- 扩展性提升: 新增服务类型和状态只需修改枚举,无需改表结构
具体改进对比:
| 方面 | 原始设计 | 重新设计 | 改进效果 |
|---|---|---|---|
| 表数量 | 5个表 (containers, rcoder_projects, computer_users, computer_projects, sessions) | 2个表 (containers, projects) | 减少60%的表数量 |
| 状态字段 | 单字符串字段 | 状态码+状态名双字段 | 查询性能提升,扩展性增强 |
| 业务区分 | 表级区分 | 字段级区分 | 更符合第一范式 |
| 外键约束 | 有外键约束 | 无外键约束,应用层保证 | 减少数据库复杂性,增加应用层控制 |
| 维护复杂度 | 高 (需维护多个相似表+外键+关联) | 低 (统一表结构+应用层逻辑) | 大幅降低维护成本 |
| 扩展性 | 差 (需新增表) | 好 (只需修改枚举) | 显著提升扩展性 |
14.2 轻量设计原则与约束影响
本设计以 数据量较小、轻量事务 和 模块架构分离 的重要约束为基础,遵循以下轻量设计原则:
- 轻量事务理念: 只有
agent_status变更需要强事务保证,其他操作均为轻量级 - 模块职责分离: 数据库操作与业务逻辑完全分离,
duckdb_manager专注数据访问 - 简洁性优先: 基于数据量小的约束,避免过度设计和复杂优化
- 功能完整性: 确保满足所有业务需求,包括统计查询的全数据覆盖
- 内存模式优势: 使用内存模式,每次重启都是全新状态,无需持久化容错处理
约束对设计的影响:
- 事务策略: 大部分操作无需事务,
agent_status变更使用轻量事务 - 第一范式设计: 统一项目表,通过
service_type字段区分业务场景,避免表分裂 - 无外键约束: 通过 CHECK 约束和应用层保证数据一致性,增加灵活性但简化维护
- 模块架构: 创建独立的
duckdb_manager模块,封装所有数据库操作 - 接口设计: 定义轻量的仓储接口,区分事务和非事务操作
- 索引策略: 简化为主,核心查询字段建立索引即可
- 查询设计: 包含全表统计查询,无需时间范围过滤
- 数据一致性: 通过 CHECK 约束保证业务规则,应用层保证基本关联完整性
- DuckDB-RS 适配: 接口设计完全兼容 DuckDB-RS 的连接管理和事务机制
- 结构体分类: 公共结构体集中管理,专用结构体模块隔离,提高代码复用性
- 维护成本: 大幅降低系统复杂度和维护成本,提高代码可维护性
15. 实施指南
15.1 crates/duckdb_manager 模块创建
15.1.1 创建模块目录结构
cd crates/
mkdir -p duckdb_manager/src/repositories
15.1.2 创建 Cargo.toml
# crates/duckdb_manager/Cargo.toml
[package]
name = "duckdb_manager"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
chrono = { version = "0.4", features = ["serde"] }
thiserror = "1.0"
tokio = { version = "1.0", features = ["sync"] }
duckdb = { version = "1.1", features = ["bundled"] }
shared_types = { path = "../shared_types" }
15.1.3 主要文件结构
crates/duckdb_manager/src/
├── lib.rs # 库入口,导出接口
├── error.rs # 错误定义
├── models.rs # 数据模型
├── connection.rs # 连接管理
├── schema.rs # 表结构定义
├── repositories/
│ ├── mod.rs # 仓储模块入口
│ ├── container.rs # 容器仓储
│ └── project.rs # 统一项目仓储(已包含session功能)
├── storage.rs # 统一存储接口
└── manager.rs # 全局管理器
15.1.4 lib.rs 示例
// crates/duckdb_manager/src/lib.rs
pub mod error;
pub mod models;
pub mod repositories;
pub mod storage;
mod connection;
mod schema;
mod manager;
// 主要接口导出
pub use storage::{
StorageManager, ContainerRepository, ProjectRepository,
TransactionManager
};
pub use manager::{init_storage, get_storage};
pub use error::StorageError;
15.2 rcoder 模块适配
15.2.1 更新依赖
在 crates/rcoder/Cargo.toml 中添加:
[dependencies]
# ... 现有依赖
duckdb_manager = { path = "../duckdb_manager" }
15.2.2 创建适配层
// crates/rcoder/src/storage/mod.rs
pub mod adapters;
pub mod bridge;
// 导出适配器(统一的项目适配器,通过 service_type 区分业务场景)
pub use adapters::{ProjectAdapter, ContainerAdapter};
pub use bridge::StorageBridge;
15.2.3 AppState 更新
// crates/rcoder/src/router.rs
use crate::storage::{ProjectAdapter, ContainerAdapter};
pub struct AdaptedAppState {
// 原有的字段保持不变
pub config: AppConfig,
pub pingora_service: Option<Arc<PingoraProxyService>>,
pub grpc_pool: Arc<GrpcChannelPool>, // gRPC 连接池保持使用 DashMap
// 替换原来的 DashMap(统一适配器)
pub projects: ProjectAdapter, // 统一项目适配器(包含 session 信息)
pub containers: ContainerAdapter, // 容器适配器
}
16. 附录
13.1 参考资料
15.1 系统约束
| 约束类型 | 具体要求 | 对设计的影响 |
|---|---|---|
| 数据规模 | 数据量较小,无需考虑大数据量优化 | 简化索引策略,优先考虑简洁性 |
| 统计查询 | 统计所有数据,不受时间范围限制 | 查询设计中包含全表统计场景 |
| 查询模式 | 主要为精确查询和全表统计 | 避免过度复杂的范围查询优化 |
| 模块架构 | 使用 crates/duckdb_manager 封装数据库操作 | 接口化设计,业务逻辑与数据访问分离 |
| 外键约束 | 禁止使用数据库外键 | 通过应用层保证数据一致性 |
| 外键约束 | 禁止使用数据库外键 | 通过应用层保证数据一致性 |
15.2 术语表
| 术语 | 说明 |
|---|---|
| DashMap | Rust 的并发 HashMap 实现 |
| DuckDB | 嵌入式分析型数据库 |
| 内存模式 | 数据库数据仅存储在内存中 |
| Repository | 仓储模式,封装数据访问逻辑 |
| RAII | Resource Acquisition Is Initialization |