添加qiming-rcoder模块
This commit is contained in:
237
qiming-rcoder/crates/duckdb_manager/src/connection.rs
Normal file
237
qiming-rcoder/crates/duckdb_manager/src/connection.rs
Normal file
@@ -0,0 +1,237 @@
|
||||
//! DuckDB 连接管理
|
||||
//!
|
||||
//! 提供线程安全的数据库连接管理
|
||||
|
||||
use crate::error::{DuckDbError, DuckDbResult};
|
||||
use duckdb::Connection;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// 线程安全的 DuckDB 连接包装器
|
||||
///
|
||||
/// 使用 `Arc<Mutex<Connection>>` 实现线程安全访问
|
||||
#[derive(Clone)]
|
||||
pub struct DuckDbConnection {
|
||||
inner: Arc<Mutex<Connection>>,
|
||||
}
|
||||
|
||||
impl DuckDbConnection {
|
||||
/// DuckDB 内存限制默认值(2GB)
|
||||
const DEFAULT_MEMORY_LIMIT: &str = "2GB";
|
||||
|
||||
/// 创建内存模式的数据库连接
|
||||
pub fn open_in_memory() -> DuckDbResult<Self> {
|
||||
let conn = Connection::open_in_memory()
|
||||
.map_err(|e| DuckDbError::ConnectionError(e.to_string()))?;
|
||||
|
||||
// 设置 DuckDB 内存限制,防止无限占用内存
|
||||
// 注意:memory_limit 只限制 buffer manager,部分数据结构(如向量、查询结果)
|
||||
// 可能在 buffer manager 外部分配,因此实际内存可能略高于此限制
|
||||
conn.execute(
|
||||
&format!("PRAGMA memory_limit='{}'", Self::DEFAULT_MEMORY_LIMIT),
|
||||
[],
|
||||
)
|
||||
.map_err(|e| DuckDbError::ConnectionError(format!("Failed to set memory_limit: {}", e)))?;
|
||||
|
||||
Ok(Self {
|
||||
inner: Arc::new(Mutex::new(conn)),
|
||||
})
|
||||
}
|
||||
|
||||
/// 创建内存模式的数据库连接(带自定义内存限制)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `memory_limit` - 内存限制字符串,如 "2GB", "512MB", "1GB"
|
||||
pub fn open_in_memory_with_limit(memory_limit: &str) -> DuckDbResult<Self> {
|
||||
let conn = Connection::open_in_memory()
|
||||
.map_err(|e| DuckDbError::ConnectionError(e.to_string()))?;
|
||||
|
||||
// 设置自定义内存限制
|
||||
conn.execute(
|
||||
&format!("PRAGMA memory_limit='{}'", memory_limit),
|
||||
[],
|
||||
)
|
||||
.map_err(|e| DuckDbError::ConnectionError(format!("Failed to set memory_limit: {}", e)))?;
|
||||
|
||||
Ok(Self {
|
||||
inner: Arc::new(Mutex::new(conn)),
|
||||
})
|
||||
}
|
||||
|
||||
/// 执行需要独占连接的操作
|
||||
///
|
||||
/// 使用闭包模式确保锁的正确释放
|
||||
pub fn with_connection<F, T>(&self, f: F) -> DuckDbResult<T>
|
||||
where
|
||||
F: FnOnce(&Connection) -> DuckDbResult<T>,
|
||||
{
|
||||
let conn = self.inner.lock();
|
||||
f(&conn)
|
||||
}
|
||||
|
||||
/// 执行需要可变连接的操作
|
||||
pub fn with_connection_mut<F, T>(&self, f: F) -> DuckDbResult<T>
|
||||
where
|
||||
F: FnOnce(&mut Connection) -> DuckDbResult<T>,
|
||||
{
|
||||
let mut conn = self.inner.lock();
|
||||
f(&mut conn)
|
||||
}
|
||||
|
||||
/// 尝试获取连接(非阻塞)
|
||||
pub fn try_with_connection<F, T>(&self, f: F) -> DuckDbResult<Option<T>>
|
||||
where
|
||||
F: FnOnce(&Connection) -> DuckDbResult<T>,
|
||||
{
|
||||
match self.inner.try_lock() {
|
||||
Some(conn) => Ok(Some(f(&conn)?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取 DuckDB 内存使用统计
|
||||
///
|
||||
/// 返回格式化的内存使用信息字符串,用于调试和监控
|
||||
pub fn get_memory_stats(&self) -> DuckDbResult<String> {
|
||||
self.with_connection(|c| {
|
||||
// 查询内存使用情况
|
||||
let mut stmt = c.prepare(
|
||||
"SELECT name, size, reservation FROM duckdb_memory() ORDER BY size DESC LIMIT 10",
|
||||
)?;
|
||||
let mut rows = stmt.query([])?;
|
||||
|
||||
let mut result = String::from("DuckDB Memory Usage (Top 10):\n");
|
||||
while let Some(row) = rows.next()? {
|
||||
let name: String = row.get(0).unwrap_or_default();
|
||||
let size: i64 = row.get(1).unwrap_or(0);
|
||||
let reservation: i64 = row.get(2).unwrap_or(0);
|
||||
result.push_str(&format!(
|
||||
" {}: size={}, reservation={}\n",
|
||||
name, size, reservation
|
||||
));
|
||||
}
|
||||
Ok(result)
|
||||
})
|
||||
}
|
||||
|
||||
/// 执行事务
|
||||
///
|
||||
/// 自动处理事务的提交和回滚
|
||||
pub fn transaction<F, T>(&self, f: F) -> DuckDbResult<T>
|
||||
where
|
||||
F: FnOnce(&Connection) -> DuckDbResult<T>,
|
||||
{
|
||||
let conn = self.inner.lock();
|
||||
|
||||
// 开始事务
|
||||
conn.execute("BEGIN TRANSACTION", [])
|
||||
.map_err(|e| DuckDbError::TransactionError(format!("failed to begin transaction: {}", e)))?;
|
||||
|
||||
// 执行操作
|
||||
match f(&conn) {
|
||||
Ok(result) => {
|
||||
// 提交事务
|
||||
conn.execute("COMMIT", [])
|
||||
.map_err(|e| DuckDbError::TransactionError(format!("failed to commit transaction: {}", e)))?;
|
||||
Ok(result)
|
||||
}
|
||||
Err(e) => {
|
||||
// 回滚事务
|
||||
let _ = conn.execute("ROLLBACK", []);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for DuckDbConnection {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("DuckDbConnection")
|
||||
.field("inner", &"<Connection>")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_open_in_memory() {
|
||||
let conn = DuckDbConnection::open_in_memory();
|
||||
assert!(conn.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_connection() {
|
||||
let conn = DuckDbConnection::open_in_memory().unwrap();
|
||||
let result = conn.with_connection(|c| {
|
||||
c.execute("SELECT 1", [])?;
|
||||
Ok(42)
|
||||
});
|
||||
assert_eq!(result.unwrap(), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transaction_commit() {
|
||||
let conn = DuckDbConnection::open_in_memory().unwrap();
|
||||
|
||||
// 创建测试表
|
||||
conn.with_connection(|c| {
|
||||
c.execute("CREATE TABLE test (id INTEGER)", [])?;
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// 使用事务插入数据
|
||||
conn.transaction(|c| {
|
||||
c.execute("INSERT INTO test VALUES (1)", [])?;
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// 验证数据存在
|
||||
let count: i32 = conn
|
||||
.with_connection(|c| {
|
||||
let mut stmt = c.prepare("SELECT COUNT(*) FROM test")?;
|
||||
let mut rows = stmt.query([])?;
|
||||
let row = rows.next()?.unwrap();
|
||||
Ok(row.get(0)?)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transaction_rollback() {
|
||||
let conn = DuckDbConnection::open_in_memory().unwrap();
|
||||
|
||||
// 创建测试表
|
||||
conn.with_connection(|c| {
|
||||
c.execute("CREATE TABLE test (id INTEGER)", [])?;
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// 使用事务插入数据,但返回错误触发回滚
|
||||
let result: DuckDbResult<()> = conn.transaction(|c| {
|
||||
c.execute("INSERT INTO test VALUES (1)", [])?;
|
||||
Err(DuckDbError::InternalError("故意触发回滚".to_string()))
|
||||
});
|
||||
|
||||
assert!(result.is_err());
|
||||
|
||||
// 验证数据不存在(已回滚)
|
||||
let count: i32 = conn
|
||||
.with_connection(|c| {
|
||||
let mut stmt = c.prepare("SELECT COUNT(*) FROM test")?;
|
||||
let mut rows = stmt.query([])?;
|
||||
let row = rows.next()?.unwrap();
|
||||
Ok(row.get(0)?)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(count, 0);
|
||||
}
|
||||
}
|
||||
68
qiming-rcoder/crates/duckdb_manager/src/error.rs
Normal file
68
qiming-rcoder/crates/duckdb_manager/src/error.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
//! DuckDB Manager error types
|
||||
//!
|
||||
//! Defines error types for DuckDB storage manager
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// DuckDB storage manager error type
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DuckDbError {
|
||||
/// Database connection error
|
||||
#[error("database connection error: {0}")]
|
||||
ConnectionError(String),
|
||||
|
||||
/// SQL execution error
|
||||
#[error("SQL execution error: {0}")]
|
||||
QueryError(String),
|
||||
|
||||
/// Data not found
|
||||
#[error("data not found: {entity} with {key} = {value}")]
|
||||
NotFound {
|
||||
entity: &'static str,
|
||||
key: &'static str,
|
||||
value: String,
|
||||
},
|
||||
|
||||
/// Data already exists
|
||||
#[error("data already exists: {entity} with {key} = {value}")]
|
||||
AlreadyExists {
|
||||
entity: &'static str,
|
||||
key: &'static str,
|
||||
value: String,
|
||||
},
|
||||
|
||||
/// Transaction error
|
||||
#[error("transaction error: {0}")]
|
||||
TransactionError(String),
|
||||
|
||||
/// Serialization/deserialization error
|
||||
#[error("serialization error: {0}")]
|
||||
SerializationError(String),
|
||||
|
||||
/// Data integrity error
|
||||
#[error("data integrity error: {0}")]
|
||||
IntegrityError(String),
|
||||
|
||||
/// Initialization error
|
||||
#[error("initialization error: {0}")]
|
||||
InitializationError(String),
|
||||
|
||||
/// Internal error
|
||||
#[error("internal error: {0}")]
|
||||
InternalError(String),
|
||||
}
|
||||
|
||||
impl From<duckdb::Error> for DuckDbError {
|
||||
fn from(err: duckdb::Error) -> Self {
|
||||
DuckDbError::QueryError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for DuckDbError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
DuckDbError::SerializationError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// DuckDB 操作结果类型
|
||||
pub type DuckDbResult<T> = Result<T, DuckDbError>;
|
||||
78
qiming-rcoder/crates/duckdb_manager/src/lib.rs
Normal file
78
qiming-rcoder/crates/duckdb_manager/src/lib.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
//! DuckDB Manager
|
||||
//!
|
||||
//! 提供基于 DuckDB 内存数据库的存储管理,用于替代 DashMap
|
||||
//!
|
||||
//! # 主要特性
|
||||
//!
|
||||
//! - **内存模式**: 使用 DuckDB 内存数据库,无需持久化
|
||||
//! - **两表设计**: `containers` 和 `projects` 表(会话信息已合并到 projects)
|
||||
//! - **线程安全**: 使用 `Arc<Mutex<Connection>>` 实现线程安全访问
|
||||
//! - **事务支持**: 对需要原子性的操作(如状态更新)提供事务支持
|
||||
//!
|
||||
//! # 示例
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use duckdb_manager::{DuckDbManager, create_storage};
|
||||
//! use duckdb_manager::models::{ContainerRecord, ProjectRecord};
|
||||
//! use shared_types::ServiceType;
|
||||
//!
|
||||
//! // 创建存储
|
||||
//! let storage = create_storage().unwrap();
|
||||
//!
|
||||
//! // 保存容器
|
||||
//! let container = ContainerRecord::new(
|
||||
//! "c1".to_string(),
|
||||
//! "container-1".to_string(),
|
||||
//! "127.0.0.1".to_string(),
|
||||
//! 8080,
|
||||
//! 8080,
|
||||
//! ServiceType::RCoder,
|
||||
//! "running".to_string(),
|
||||
//! "http://localhost:8080".to_string(),
|
||||
//! );
|
||||
//! storage.save_container(&container).unwrap();
|
||||
//!
|
||||
//! // 保存项目
|
||||
//! let project = ProjectRecord::new(
|
||||
//! "p1".to_string(),
|
||||
//! ServiceType::RCoder,
|
||||
//! "c1".to_string(),
|
||||
//! );
|
||||
//! storage.save_project(&project).unwrap();
|
||||
//!
|
||||
//! // 更新会话
|
||||
//! storage.update_session("p1", "session-1").unwrap();
|
||||
//!
|
||||
//! // 通过会话ID查询
|
||||
//! let project = storage.get_project_by_session("session-1").unwrap();
|
||||
//! ```
|
||||
//!
|
||||
//! # 模块结构
|
||||
//!
|
||||
//! - `connection`: 数据库连接管理
|
||||
//! - `error`: 错误类型定义
|
||||
//! - `models`: 数据模型定义
|
||||
//! - `schema`: 数据库 Schema 定义和初始化
|
||||
//! - `repositories`: 数据访问层(Repository 模式)
|
||||
//! - `manager`: 全局管理器
|
||||
//! - `storage`: 统一存储接口
|
||||
|
||||
pub mod connection;
|
||||
pub mod error;
|
||||
pub mod manager;
|
||||
pub mod models;
|
||||
pub mod repositories;
|
||||
pub mod schema;
|
||||
pub mod storage;
|
||||
|
||||
// 重新导出常用类型
|
||||
pub use connection::DuckDbConnection;
|
||||
pub use error::{DuckDbError, DuckDbResult};
|
||||
pub use manager::{DuckDbManager, get_global_manager, init_global_manager};
|
||||
pub use models::{
|
||||
CleanupResult, ContainerRecord, IdleContainerInfo, OrphanContainerInfo, ProjectRecord,
|
||||
StorageStats,
|
||||
};
|
||||
pub use repositories::{ContainerRepository, ProjectRepository};
|
||||
pub use schema::SchemaInitializer;
|
||||
pub use storage::{DuckDbStorage, UnifiedStorage, create_storage};
|
||||
287
qiming-rcoder/crates/duckdb_manager/src/manager.rs
Normal file
287
qiming-rcoder/crates/duckdb_manager/src/manager.rs
Normal file
@@ -0,0 +1,287 @@
|
||||
//! DuckDB 全局管理器
|
||||
//!
|
||||
//! 提供 DuckDB 存储的全局管理入口
|
||||
|
||||
use crate::connection::DuckDbConnection;
|
||||
use crate::error::DuckDbResult;
|
||||
use crate::models::StorageStats;
|
||||
use crate::repositories::{ContainerRepository, ProjectRepository};
|
||||
use crate::schema::SchemaInitializer;
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// DuckDB 全局管理器
|
||||
///
|
||||
/// 管理数据库连接和提供存储访问入口
|
||||
pub struct DuckDbManager {
|
||||
/// 数据库连接
|
||||
conn: DuckDbConnection,
|
||||
/// 是否已初始化
|
||||
initialized: Arc<RwLock<bool>>,
|
||||
}
|
||||
|
||||
impl DuckDbManager {
|
||||
/// 创建新的 DuckDB 管理器(内存模式)
|
||||
pub fn new_in_memory() -> DuckDbResult<Self> {
|
||||
let conn = DuckDbConnection::open_in_memory()?;
|
||||
|
||||
let manager = Self {
|
||||
conn,
|
||||
initialized: Arc::new(RwLock::new(false)),
|
||||
};
|
||||
|
||||
// 初始化 Schema
|
||||
manager.initialize()?;
|
||||
|
||||
Ok(manager)
|
||||
}
|
||||
|
||||
/// 初始化数据库 Schema
|
||||
fn initialize(&self) -> DuckDbResult<()> {
|
||||
let mut initialized = self.initialized.write();
|
||||
|
||||
if *initialized {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
SchemaInitializer::initialize(&self.conn)?;
|
||||
*initialized = true;
|
||||
|
||||
tracing::info!("DuckDB Manager initialized");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取容器 Repository
|
||||
///
|
||||
/// 注意:返回的 Repository 共享同一个 `Arc<Mutex<Connection>>`,
|
||||
/// 确保并发访问时的线程安全。
|
||||
pub fn containers(&self) -> DuckDbResult<ContainerRepository> {
|
||||
Ok(ContainerRepository::new(self.conn.clone()))
|
||||
}
|
||||
|
||||
/// 获取项目 Repository
|
||||
///
|
||||
/// 注意:返回的 Repository 共享同一个 `Arc<Mutex<Connection>>`,
|
||||
/// 确保并发访问时的线程安全。
|
||||
pub fn projects(&self) -> DuckDbResult<ProjectRepository> {
|
||||
Ok(ProjectRepository::new(self.conn.clone()))
|
||||
}
|
||||
|
||||
/// 获取存储统计信息
|
||||
pub fn get_stats(&self) -> DuckDbResult<StorageStats> {
|
||||
let containers = self.containers()?;
|
||||
let projects = self.projects()?;
|
||||
|
||||
let total_containers = containers.count()?;
|
||||
let total_projects = projects.count()?;
|
||||
let active_sessions = projects.count_active_sessions()?;
|
||||
let projects_by_service_type = projects.count_by_service_type()?;
|
||||
|
||||
// 计算活跃和闲置容器
|
||||
let all_containers = containers.find_all()?;
|
||||
let idle_threshold_minutes = 30; // 30 分钟闲置阈值
|
||||
|
||||
let mut active_containers = 0;
|
||||
let mut idle_containers = 0;
|
||||
|
||||
for container in &all_containers {
|
||||
if container.is_idle(idle_threshold_minutes) {
|
||||
idle_containers += 1;
|
||||
} else {
|
||||
active_containers += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(StorageStats {
|
||||
total_containers,
|
||||
total_projects,
|
||||
active_sessions,
|
||||
active_containers,
|
||||
idle_containers,
|
||||
projects_by_service_type,
|
||||
})
|
||||
}
|
||||
|
||||
/// 验证数据库状态
|
||||
pub fn verify(&self) -> DuckDbResult<bool> {
|
||||
SchemaInitializer::verify(&self.conn)
|
||||
}
|
||||
|
||||
/// 获取原始连接(用于高级操作)
|
||||
pub fn connection(&self) -> &DuckDbConnection {
|
||||
&self.conn
|
||||
}
|
||||
|
||||
/// 执行自定义 SQL 查询(用于复杂查询)
|
||||
pub fn execute_query<F, T>(&self, f: F) -> DuckDbResult<T>
|
||||
where
|
||||
F: FnOnce(&duckdb::Connection) -> DuckDbResult<T>,
|
||||
{
|
||||
self.conn.with_connection(f)
|
||||
}
|
||||
|
||||
/// 执行事务
|
||||
pub fn transaction<F, T>(&self, f: F) -> DuckDbResult<T>
|
||||
where
|
||||
F: FnOnce(&duckdb::Connection) -> DuckDbResult<T>,
|
||||
{
|
||||
self.conn.transaction(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for DuckDbManager {
|
||||
fn clone(&self) -> Self {
|
||||
// 注意:克隆后的 Manager 共享相同的底层连接
|
||||
Self {
|
||||
conn: self.conn.clone(),
|
||||
initialized: self.initialized.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for DuckDbManager {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("DuckDbManager")
|
||||
.field("initialized", &*self.initialized.read())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// 全局 DuckDB 管理器单例
|
||||
static GLOBAL_MANAGER: std::sync::OnceLock<DuckDbManager> = std::sync::OnceLock::new();
|
||||
|
||||
/// 初始化全局 DuckDB 管理器
|
||||
///
|
||||
/// 应在应用启动时调用一次
|
||||
pub fn init_global_manager() -> DuckDbResult<&'static DuckDbManager> {
|
||||
// 尝试初始化,如果已初始化则返回现有实例
|
||||
if let Some(manager) = GLOBAL_MANAGER.get() {
|
||||
return Ok(manager);
|
||||
}
|
||||
|
||||
let manager = DuckDbManager::new_in_memory()?;
|
||||
|
||||
// 尝试设置全局管理器,如果失败(已被其他线程设置)则返回现有实例
|
||||
match GLOBAL_MANAGER.set(manager) {
|
||||
Ok(()) => Ok(GLOBAL_MANAGER.get().unwrap()),
|
||||
Err(_) => Ok(GLOBAL_MANAGER.get().unwrap()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取全局 DuckDB 管理器
|
||||
///
|
||||
/// 如果未初始化,将自动初始化
|
||||
pub fn get_global_manager() -> DuckDbResult<&'static DuckDbManager> {
|
||||
match GLOBAL_MANAGER.get() {
|
||||
Some(manager) => Ok(manager),
|
||||
None => init_global_manager(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::models::{ContainerRecord, ProjectRecord};
|
||||
use shared_types::ServiceType;
|
||||
|
||||
#[test]
|
||||
fn test_new_in_memory() {
|
||||
let manager = DuckDbManager::new_in_memory().unwrap();
|
||||
assert!(manager.verify().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_stats_empty() {
|
||||
let manager = DuckDbManager::new_in_memory().unwrap();
|
||||
let stats = manager.get_stats().unwrap();
|
||||
|
||||
assert_eq!(stats.total_containers, 0);
|
||||
assert_eq!(stats.total_projects, 0);
|
||||
assert_eq!(stats.active_sessions, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_stats_with_data() {
|
||||
let manager = DuckDbManager::new_in_memory().unwrap();
|
||||
|
||||
// 添加容器
|
||||
let containers = manager.containers().unwrap();
|
||||
containers
|
||||
.upsert(&ContainerRecord::new(
|
||||
"c1".to_string(),
|
||||
"container-1".to_string(),
|
||||
"127.0.0.1".to_string(),
|
||||
8080,
|
||||
8080,
|
||||
ServiceType::RCoder,
|
||||
"running".to_string(),
|
||||
"http://localhost:8080".to_string(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
// 添加项目
|
||||
let projects = manager.projects().unwrap();
|
||||
projects
|
||||
.upsert(&ProjectRecord::new(
|
||||
"p1".to_string(),
|
||||
ServiceType::RCoder,
|
||||
"c1".to_string(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
// 添加会话
|
||||
projects.update_session("p1", "session-1").unwrap();
|
||||
|
||||
let stats = manager.get_stats().unwrap();
|
||||
assert_eq!(stats.total_containers, 1);
|
||||
assert_eq!(stats.total_projects, 1);
|
||||
assert_eq!(stats.active_sessions, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clone_shares_data() {
|
||||
let manager = DuckDbManager::new_in_memory().unwrap();
|
||||
|
||||
// 使用原始 manager 添加数据
|
||||
let containers = manager.containers().unwrap();
|
||||
containers
|
||||
.upsert(&ContainerRecord::new(
|
||||
"c1".to_string(),
|
||||
"container-1".to_string(),
|
||||
"127.0.0.1".to_string(),
|
||||
8080,
|
||||
8080,
|
||||
ServiceType::RCoder,
|
||||
"running".to_string(),
|
||||
"http://localhost:8080".to_string(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
// 克隆 manager
|
||||
let cloned = manager.clone();
|
||||
|
||||
// 通过克隆的 manager 读取数据
|
||||
let cloned_containers = cloned.containers().unwrap();
|
||||
let found = cloned_containers.find_by_id("c1").unwrap();
|
||||
assert!(found.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transaction() {
|
||||
let manager = DuckDbManager::new_in_memory().unwrap();
|
||||
|
||||
// 成功的事务
|
||||
let result = manager.transaction(|conn| {
|
||||
conn.execute(
|
||||
"INSERT INTO containers (container_id, container_name, container_ip, internal_port, external_port, service_type, status, service_url, created_at, last_activity) VALUES ('c1', 'name', '127.0.0.1', 8080, 8080, 'rcoder', 'running', 'http://localhost', NOW(), NOW())",
|
||||
[],
|
||||
)?;
|
||||
Ok(())
|
||||
});
|
||||
assert!(result.is_ok());
|
||||
|
||||
// 验证数据存在
|
||||
let containers = manager.containers().unwrap();
|
||||
assert!(containers.exists("c1").unwrap());
|
||||
}
|
||||
}
|
||||
267
qiming-rcoder/crates/duckdb_manager/src/models.rs
Normal file
267
qiming-rcoder/crates/duckdb_manager/src/models.rs
Normal file
@@ -0,0 +1,267 @@
|
||||
//! DuckDB Manager 数据模型
|
||||
//!
|
||||
//! 定义数据库表对应的记录结构和辅助类型
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shared_types::ServiceType;
|
||||
|
||||
/// 容器记录 - 对应 containers 表
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContainerRecord {
|
||||
/// 容器ID (主键)
|
||||
pub container_id: String,
|
||||
/// 容器名称
|
||||
pub container_name: String,
|
||||
/// 容器IP地址
|
||||
pub container_ip: String,
|
||||
/// 内部端口
|
||||
pub internal_port: u16,
|
||||
/// 外部端口
|
||||
pub external_port: u16,
|
||||
/// 服务类型
|
||||
pub service_type: ServiceType,
|
||||
/// 容器状态
|
||||
pub status: String,
|
||||
/// 服务URL
|
||||
pub service_url: String,
|
||||
/// 创建时间
|
||||
pub created_at: DateTime<Utc>,
|
||||
/// 最后活动时间
|
||||
pub last_activity: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl ContainerRecord {
|
||||
/// 创建新的容器记录
|
||||
pub fn new(
|
||||
container_id: String,
|
||||
container_name: String,
|
||||
container_ip: String,
|
||||
internal_port: u16,
|
||||
external_port: u16,
|
||||
service_type: ServiceType,
|
||||
status: String,
|
||||
service_url: String,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
container_id,
|
||||
container_name,
|
||||
container_ip,
|
||||
internal_port,
|
||||
external_port,
|
||||
service_type,
|
||||
status,
|
||||
service_url,
|
||||
created_at: now,
|
||||
last_activity: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查容器是否处于保护期(创建后 5 分钟内)
|
||||
pub fn is_in_protection_period(&self, protection_minutes: i64) -> bool {
|
||||
let elapsed = Utc::now().signed_duration_since(self.created_at);
|
||||
elapsed.num_minutes() < protection_minutes
|
||||
}
|
||||
|
||||
/// 检查容器是否闲置
|
||||
pub fn is_idle(&self, idle_minutes: i64) -> bool {
|
||||
let elapsed = Utc::now().signed_duration_since(self.last_activity);
|
||||
elapsed.num_minutes() >= idle_minutes
|
||||
}
|
||||
}
|
||||
|
||||
/// 项目记录 - 对应 projects 表
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProjectRecord {
|
||||
/// 项目ID (主键)
|
||||
pub project_id: String,
|
||||
/// 会话ID
|
||||
pub session_id: Option<String>,
|
||||
/// 服务类型
|
||||
pub service_type: ServiceType,
|
||||
/// 关联的容器ID
|
||||
pub container_id: String,
|
||||
/// 用户ID (ComputerAgentRunner 模式)
|
||||
pub user_id: Option<String>,
|
||||
/// Pod ID (共享容器模式)
|
||||
pub pod_id: Option<String>,
|
||||
/// Agent 状态码
|
||||
pub agent_status_code: Option<i32>,
|
||||
/// Agent 状态名称
|
||||
pub agent_status_name: Option<String>,
|
||||
/// 请求ID
|
||||
pub request_id: Option<String>,
|
||||
/// 模型提供商配置 (JSON)
|
||||
pub model_provider_json: Option<String>,
|
||||
/// 创建时间
|
||||
pub created_at: DateTime<Utc>,
|
||||
/// 最后活动时间
|
||||
pub last_activity: DateTime<Utc>,
|
||||
/// 会话创建时间
|
||||
pub session_created_at: Option<DateTime<Utc>>,
|
||||
/// 会话最后活动时间
|
||||
pub session_last_activity: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl ProjectRecord {
|
||||
/// 创建新的项目记录
|
||||
pub fn new(project_id: String, service_type: ServiceType, container_id: String) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
project_id,
|
||||
session_id: None,
|
||||
service_type,
|
||||
container_id,
|
||||
user_id: None,
|
||||
pod_id: None,
|
||||
agent_status_code: None,
|
||||
agent_status_name: None,
|
||||
request_id: None,
|
||||
model_provider_json: None,
|
||||
created_at: now,
|
||||
last_activity: now,
|
||||
session_created_at: None,
|
||||
session_last_activity: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建带用户ID的项目记录 (ComputerAgentRunner 模式)
|
||||
pub fn new_with_user_id(
|
||||
project_id: String,
|
||||
user_id: String,
|
||||
service_type: ServiceType,
|
||||
container_id: String,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
project_id,
|
||||
session_id: None,
|
||||
service_type,
|
||||
container_id,
|
||||
user_id: Some(user_id),
|
||||
pod_id: None,
|
||||
agent_status_code: None,
|
||||
agent_status_name: None,
|
||||
request_id: None,
|
||||
model_provider_json: None,
|
||||
created_at: now,
|
||||
last_activity: now,
|
||||
session_created_at: None,
|
||||
session_last_activity: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取容器唯一标识
|
||||
///
|
||||
/// 根据 service_type 返回不同的标识符:
|
||||
/// - RCoder 模式:返回 pod_id(如果存在,共享容器),否则返回 project_id
|
||||
/// - ComputerAgentRunner 模式:返回 user_id(如果存在)
|
||||
pub fn container_key(&self) -> &str {
|
||||
match self.service_type {
|
||||
ServiceType::ComputerAgentRunner => self.user_id.as_deref().unwrap_or(&self.project_id),
|
||||
ServiceType::RCoder => self.pod_id.as_deref().unwrap_or(&self.project_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 清理结果统计
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CleanupResult {
|
||||
/// 清理的容器数量
|
||||
pub cleaned_containers: usize,
|
||||
/// 清理的项目数量
|
||||
pub cleaned_projects: usize,
|
||||
/// 清理的孤立容器数量
|
||||
pub orphan_containers: usize,
|
||||
/// 清理的 gRPC 连接数量
|
||||
pub grpc_connections: usize,
|
||||
/// 清理的 VNC 后端数量
|
||||
pub vnc_backends: usize,
|
||||
/// 错误信息
|
||||
pub errors: Vec<String>,
|
||||
}
|
||||
|
||||
impl CleanupResult {
|
||||
/// 创建新的清理结果
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// 添加错误信息
|
||||
pub fn add_error(&mut self, error: String) {
|
||||
self.errors.push(error);
|
||||
}
|
||||
|
||||
/// 合并另一个清理结果
|
||||
pub fn merge(&mut self, other: CleanupResult) {
|
||||
self.cleaned_containers += other.cleaned_containers;
|
||||
self.cleaned_projects += other.cleaned_projects;
|
||||
self.orphan_containers += other.orphan_containers;
|
||||
self.grpc_connections += other.grpc_connections;
|
||||
self.vnc_backends += other.vnc_backends;
|
||||
self.errors.extend(other.errors);
|
||||
}
|
||||
|
||||
/// 是否有错误
|
||||
pub fn has_errors(&self) -> bool {
|
||||
!self.errors.is_empty()
|
||||
}
|
||||
|
||||
/// 总清理数量
|
||||
pub fn total_cleaned(&self) -> usize {
|
||||
self.cleaned_containers + self.cleaned_projects + self.orphan_containers
|
||||
}
|
||||
}
|
||||
|
||||
/// 存储统计信息
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct StorageStats {
|
||||
/// 总容器数量
|
||||
pub total_containers: usize,
|
||||
/// 总项目数量
|
||||
pub total_projects: usize,
|
||||
/// 活跃会话数量
|
||||
pub active_sessions: usize,
|
||||
/// 活跃容器数量
|
||||
pub active_containers: usize,
|
||||
/// 闲置容器数量
|
||||
pub idle_containers: usize,
|
||||
/// 按服务类型统计的项目数量
|
||||
pub projects_by_service_type: std::collections::HashMap<ServiceType, usize>,
|
||||
}
|
||||
|
||||
impl StorageStats {
|
||||
/// 创建新的统计信息
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// 闲置容器信息 - 用于清理任务
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IdleContainerInfo {
|
||||
/// 容器ID
|
||||
pub container_id: String,
|
||||
/// 容器名称
|
||||
pub container_name: String,
|
||||
/// 服务类型
|
||||
pub service_type: ServiceType,
|
||||
/// 闲置时长(分钟)
|
||||
pub idle_minutes: i64,
|
||||
/// 关联的项目ID列表
|
||||
pub project_ids: Vec<String>,
|
||||
}
|
||||
|
||||
/// 孤立容器信息 - 用于清理任务
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OrphanContainerInfo {
|
||||
/// 容器ID
|
||||
pub container_id: String,
|
||||
/// 容器名称
|
||||
pub container_name: String,
|
||||
/// 服务类型
|
||||
pub service_type: ServiceType,
|
||||
/// 创建时间
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
//! 容器 Repository
|
||||
//!
|
||||
//! 提供容器表的数据访问操作
|
||||
|
||||
use crate::connection::DuckDbConnection;
|
||||
use crate::error::{DuckDbError, DuckDbResult};
|
||||
use crate::models::{ContainerRecord, IdleContainerInfo};
|
||||
use chrono::{DateTime, Utc};
|
||||
use duckdb::params;
|
||||
use shared_types::ServiceType;
|
||||
|
||||
/// 容器 Repository
|
||||
pub struct ContainerRepository {
|
||||
conn: DuckDbConnection,
|
||||
}
|
||||
|
||||
impl ContainerRepository {
|
||||
/// 创建新的 ContainerRepository
|
||||
pub fn new(conn: DuckDbConnection) -> Self {
|
||||
Self { conn }
|
||||
}
|
||||
|
||||
/// 插入或更新容器记录
|
||||
pub fn upsert(&self, record: &ContainerRecord) -> DuckDbResult<()> {
|
||||
let service_type_str = record.service_type.to_string();
|
||||
let created_at_str = record.created_at.to_rfc3339();
|
||||
let last_activity_str = record.last_activity.to_rfc3339();
|
||||
|
||||
self.conn.with_connection(|c| {
|
||||
c.execute(
|
||||
r#"
|
||||
INSERT OR REPLACE INTO containers (
|
||||
container_id, container_name, container_ip,
|
||||
internal_port, external_port, service_type,
|
||||
status, service_url, created_at, last_activity
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
params![
|
||||
record.container_id,
|
||||
record.container_name,
|
||||
record.container_ip,
|
||||
record.internal_port as i32,
|
||||
record.external_port as i32,
|
||||
service_type_str,
|
||||
record.status,
|
||||
record.service_url,
|
||||
created_at_str,
|
||||
last_activity_str,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// 根据容器ID查找容器
|
||||
pub fn find_by_id(&self, container_id: &str) -> DuckDbResult<Option<ContainerRecord>> {
|
||||
self.conn.with_connection(|c| {
|
||||
let mut stmt = c.prepare(
|
||||
r#"
|
||||
SELECT container_id, container_name, container_ip,
|
||||
internal_port, external_port, service_type,
|
||||
status, service_url, created_at, last_activity
|
||||
FROM containers
|
||||
WHERE container_id = ?
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut rows = stmt.query(params![container_id])?;
|
||||
|
||||
match rows.next()? {
|
||||
Some(row) => Ok(Some(Self::row_to_record(row)?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// 删除容器
|
||||
pub fn delete(&self, container_id: &str) -> DuckDbResult<bool> {
|
||||
self.conn.with_connection(|c| {
|
||||
let affected = c.execute(
|
||||
"DELETE FROM containers WHERE container_id = ?",
|
||||
params![container_id],
|
||||
)?;
|
||||
Ok(affected > 0)
|
||||
})
|
||||
}
|
||||
|
||||
/// 检查容器是否存在
|
||||
pub fn exists(&self, container_id: &str) -> DuckDbResult<bool> {
|
||||
self.conn.with_connection(|c| {
|
||||
let mut stmt = c.prepare("SELECT 1 FROM containers WHERE container_id = ? LIMIT 1")?;
|
||||
let mut rows = stmt.query(params![container_id])?;
|
||||
Ok(rows.next()?.is_some())
|
||||
})
|
||||
}
|
||||
|
||||
/// 更新容器最后活动时间
|
||||
pub fn update_activity(&self, container_id: &str) -> DuckDbResult<bool> {
|
||||
let now_str = Utc::now().to_rfc3339();
|
||||
self.conn.with_connection(|c| {
|
||||
let affected = c.execute(
|
||||
"UPDATE containers SET last_activity = ? WHERE container_id = ?",
|
||||
params![now_str, container_id],
|
||||
)?;
|
||||
Ok(affected > 0)
|
||||
})
|
||||
}
|
||||
|
||||
/// 根据会话ID更新关联容器的最后活动时间
|
||||
pub fn update_activity_by_session(&self, session_id: &str) -> DuckDbResult<bool> {
|
||||
let now_str = Utc::now().to_rfc3339();
|
||||
self.conn.with_connection(|c| {
|
||||
let affected = c.execute(
|
||||
r#"
|
||||
UPDATE containers
|
||||
SET last_activity = ?
|
||||
WHERE container_id IN (
|
||||
SELECT container_id FROM projects WHERE session_id = ?
|
||||
)
|
||||
"#,
|
||||
params![now_str, session_id],
|
||||
)?;
|
||||
Ok(affected > 0)
|
||||
})
|
||||
}
|
||||
|
||||
/// 使用指定时间更新容器最后活动时间(用于保持项目和容器时间一致)
|
||||
pub fn update_activity_with_time(
|
||||
&self,
|
||||
container_id: &str,
|
||||
time: DateTime<Utc>,
|
||||
) -> DuckDbResult<bool> {
|
||||
let time_str = time.to_rfc3339();
|
||||
self.conn.with_connection(|c| {
|
||||
let affected = c.execute(
|
||||
"UPDATE containers SET last_activity = ? WHERE container_id = ?",
|
||||
params![time_str, container_id],
|
||||
)?;
|
||||
Ok(affected > 0)
|
||||
})
|
||||
}
|
||||
|
||||
/// 更新容器状态
|
||||
pub fn update_status(&self, container_id: &str, status: &str) -> DuckDbResult<bool> {
|
||||
let now_str = Utc::now().to_rfc3339();
|
||||
self.conn.with_connection(|c| {
|
||||
let affected = c.execute(
|
||||
"UPDATE containers SET status = ?, last_activity = ? WHERE container_id = ?",
|
||||
params![status, now_str, container_id],
|
||||
)?;
|
||||
Ok(affected > 0)
|
||||
})
|
||||
}
|
||||
|
||||
/// 获取所有容器
|
||||
pub fn find_all(&self) -> DuckDbResult<Vec<ContainerRecord>> {
|
||||
self.conn.with_connection(|c| {
|
||||
let mut stmt = c.prepare(
|
||||
r#"
|
||||
SELECT container_id, container_name, container_ip,
|
||||
internal_port, external_port, service_type,
|
||||
status, service_url, created_at, last_activity
|
||||
FROM containers
|
||||
ORDER BY created_at DESC
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut rows = stmt.query([])?;
|
||||
let mut records = Vec::new();
|
||||
|
||||
while let Some(row) = rows.next()? {
|
||||
records.push(Self::row_to_record(row)?);
|
||||
}
|
||||
|
||||
Ok(records)
|
||||
})
|
||||
}
|
||||
|
||||
/// 按服务类型查找容器
|
||||
pub fn find_by_service_type(
|
||||
&self,
|
||||
service_type: ServiceType,
|
||||
) -> DuckDbResult<Vec<ContainerRecord>> {
|
||||
let service_type_str = service_type.to_string();
|
||||
self.conn.with_connection(|c| {
|
||||
let mut stmt = c.prepare(
|
||||
r#"
|
||||
SELECT container_id, container_name, container_ip,
|
||||
internal_port, external_port, service_type,
|
||||
status, service_url, created_at, last_activity
|
||||
FROM containers
|
||||
WHERE service_type = ?
|
||||
ORDER BY created_at DESC
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut rows = stmt.query(params![service_type_str])?;
|
||||
let mut records = Vec::new();
|
||||
|
||||
while let Some(row) = rows.next()? {
|
||||
records.push(Self::row_to_record(row)?);
|
||||
}
|
||||
|
||||
Ok(records)
|
||||
})
|
||||
}
|
||||
|
||||
/// 查找闲置容器(超过指定分钟数未活动且不在保护期内)
|
||||
pub fn find_idle_containers(
|
||||
&self,
|
||||
idle_minutes: i64,
|
||||
protection_minutes: i64,
|
||||
) -> DuckDbResult<Vec<IdleContainerInfo>> {
|
||||
self.conn.with_connection(|c| {
|
||||
// 使用 DuckDB 的时间函数计算
|
||||
let mut stmt = c.prepare(
|
||||
r#"
|
||||
SELECT c.container_id, c.container_name, c.service_type,
|
||||
DATEDIFF('minute', c.last_activity, NOW()) as idle_mins
|
||||
FROM containers c
|
||||
WHERE DATEDIFF('minute', c.last_activity, NOW()) >= ?
|
||||
AND DATEDIFF('minute', c.created_at, NOW()) >= ?
|
||||
ORDER BY idle_mins DESC
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut rows = stmt.query(params![idle_minutes, protection_minutes])?;
|
||||
let mut results = Vec::new();
|
||||
|
||||
while let Some(row) = rows.next()? {
|
||||
let container_id: String = row.get(0)?;
|
||||
let container_name: String = row.get(1)?;
|
||||
let service_type_str: String = row.get(2)?;
|
||||
let idle_mins: i64 = row.get(3)?;
|
||||
|
||||
let service_type = service_type_str
|
||||
.parse::<ServiceType>()
|
||||
.map_err(|e| DuckDbError::InternalError(format!("failed to parse service type: {}", e)))?;
|
||||
|
||||
results.push(IdleContainerInfo {
|
||||
container_id,
|
||||
container_name,
|
||||
service_type,
|
||||
idle_minutes: idle_mins,
|
||||
project_ids: Vec::new(), // 稍后填充
|
||||
});
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
})
|
||||
}
|
||||
|
||||
/// 查找孤立容器(存在于 Docker 但不在数据库中)
|
||||
///
|
||||
/// 此方法用于与外部容器列表进行比对
|
||||
pub fn find_orphan_containers(
|
||||
&self,
|
||||
docker_container_ids: &[String],
|
||||
) -> DuckDbResult<Vec<String>> {
|
||||
if docker_container_ids.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
self.conn.with_connection(|c| {
|
||||
// 获取数据库中所有容器 ID
|
||||
let mut stmt = c.prepare("SELECT container_id FROM containers")?;
|
||||
let mut rows = stmt.query([])?;
|
||||
let mut db_container_ids = std::collections::HashSet::new();
|
||||
|
||||
while let Some(row) = rows.next()? {
|
||||
let id: String = row.get(0)?;
|
||||
db_container_ids.insert(id);
|
||||
}
|
||||
|
||||
// 找出在 Docker 中存在但不在数据库中的容器
|
||||
let orphans: Vec<String> = docker_container_ids
|
||||
.iter()
|
||||
.filter(|id| !db_container_ids.contains(*id))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
Ok(orphans)
|
||||
})
|
||||
}
|
||||
|
||||
/// 获取容器数量统计
|
||||
pub fn count(&self) -> DuckDbResult<usize> {
|
||||
self.conn.with_connection(|c| {
|
||||
let mut stmt = c.prepare("SELECT COUNT(*) FROM containers")?;
|
||||
let mut rows = stmt.query([])?;
|
||||
let row = rows
|
||||
.next()?
|
||||
.ok_or_else(|| DuckDbError::InternalError("unable to get container count".to_string()))?;
|
||||
let count: i64 = row.get(0)?;
|
||||
Ok(count as usize)
|
||||
})
|
||||
}
|
||||
|
||||
/// 按服务类型统计容器数量
|
||||
pub fn count_by_service_type(
|
||||
&self,
|
||||
) -> DuckDbResult<std::collections::HashMap<ServiceType, usize>> {
|
||||
self.conn.with_connection(|c| {
|
||||
let mut stmt =
|
||||
c.prepare("SELECT service_type, COUNT(*) FROM containers GROUP BY service_type")?;
|
||||
let mut rows = stmt.query([])?;
|
||||
let mut counts = std::collections::HashMap::new();
|
||||
|
||||
while let Some(row) = rows.next()? {
|
||||
let service_type_str: String = row.get(0)?;
|
||||
let count: i64 = row.get(1)?;
|
||||
|
||||
if let Ok(service_type) = service_type_str.parse::<ServiceType>() {
|
||||
counts.insert(service_type, count as usize);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(counts)
|
||||
})
|
||||
}
|
||||
|
||||
/// 从数据库行转换为 ContainerRecord
|
||||
fn row_to_record(row: &duckdb::Row<'_>) -> DuckDbResult<ContainerRecord> {
|
||||
let container_id: String = row.get(0)?;
|
||||
let container_name: String = row.get(1)?;
|
||||
let container_ip: String = row.get(2)?;
|
||||
let internal_port: i32 = row.get(3)?;
|
||||
let external_port: i32 = row.get(4)?;
|
||||
let service_type_str: String = row.get(5)?;
|
||||
let status: String = row.get(6)?;
|
||||
let service_url: String = row.get(7)?;
|
||||
|
||||
// DuckDB 返回 TIMESTAMP 类型,需要使用 get_ref 并转换
|
||||
let created_at = Self::get_timestamp_from_row(row, 8)?;
|
||||
let last_activity = Self::get_timestamp_from_row(row, 9)?;
|
||||
|
||||
let service_type = service_type_str
|
||||
.parse::<ServiceType>()
|
||||
.map_err(|e| DuckDbError::InternalError(format!("failed to parse service type: {}", e)))?;
|
||||
|
||||
Ok(ContainerRecord {
|
||||
container_id,
|
||||
container_name,
|
||||
container_ip,
|
||||
internal_port: internal_port as u16,
|
||||
external_port: external_port as u16,
|
||||
service_type,
|
||||
status,
|
||||
service_url,
|
||||
created_at,
|
||||
last_activity,
|
||||
})
|
||||
}
|
||||
|
||||
/// 从行中获取时间戳
|
||||
fn get_timestamp_from_row(row: &duckdb::Row<'_>, idx: usize) -> DuckDbResult<DateTime<Utc>> {
|
||||
// DuckDB 将 TIMESTAMP 返回为微秒级 Unix 时间戳 (i64)
|
||||
// 或者可能返回字符串,取决于存储方式
|
||||
use duckdb::types::ValueRef;
|
||||
|
||||
let value_ref = row.get_ref(idx)?;
|
||||
match value_ref {
|
||||
ValueRef::Timestamp(_, micros) => {
|
||||
// 微秒转秒和纳秒
|
||||
let secs = micros / 1_000_000;
|
||||
let nsecs = ((micros % 1_000_000) * 1000) as u32;
|
||||
Ok(DateTime::from_timestamp(secs, nsecs).unwrap_or_else(Utc::now))
|
||||
}
|
||||
ValueRef::Text(bytes) => {
|
||||
let s = std::str::from_utf8(bytes)
|
||||
.map_err(|e| DuckDbError::InternalError(format!("UTF8 parsing failed: {}", e)))?;
|
||||
DateTime::parse_from_rfc3339(s)
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
.map_err(|e| DuckDbError::InternalError(format!("timestamp parsing failed: {}", e)))
|
||||
}
|
||||
_ => Ok(Utc::now()), // 默认返回当前时间
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::schema::SchemaInitializer;
|
||||
|
||||
fn setup_test_db() -> ContainerRepository {
|
||||
let conn = DuckDbConnection::open_in_memory().unwrap();
|
||||
SchemaInitializer::initialize(&conn).unwrap();
|
||||
ContainerRepository::new(conn)
|
||||
}
|
||||
|
||||
fn create_test_record(id: &str) -> ContainerRecord {
|
||||
ContainerRecord::new(
|
||||
id.to_string(),
|
||||
format!("container-{}", id),
|
||||
"127.0.0.1".to_string(),
|
||||
8080,
|
||||
8080,
|
||||
ServiceType::RCoder,
|
||||
"running".to_string(),
|
||||
format!("http://localhost:8080/{}", id),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_upsert_and_find() {
|
||||
let repo = setup_test_db();
|
||||
let record = create_test_record("c1");
|
||||
|
||||
repo.upsert(&record).unwrap();
|
||||
|
||||
let found = repo.find_by_id("c1").unwrap();
|
||||
assert!(found.is_some());
|
||||
let found = found.unwrap();
|
||||
assert_eq!(found.container_id, "c1");
|
||||
assert_eq!(found.container_name, "container-c1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete() {
|
||||
let repo = setup_test_db();
|
||||
let record = create_test_record("c1");
|
||||
|
||||
repo.upsert(&record).unwrap();
|
||||
assert!(repo.exists("c1").unwrap());
|
||||
|
||||
repo.delete("c1").unwrap();
|
||||
assert!(!repo.exists("c1").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_activity() {
|
||||
let repo = setup_test_db();
|
||||
let record = create_test_record("c1");
|
||||
|
||||
repo.upsert(&record).unwrap();
|
||||
|
||||
let before = repo.find_by_id("c1").unwrap().unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
|
||||
repo.update_activity("c1").unwrap();
|
||||
|
||||
let after = repo.find_by_id("c1").unwrap().unwrap();
|
||||
assert!(after.last_activity >= before.last_activity);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_count() {
|
||||
let repo = setup_test_db();
|
||||
|
||||
assert_eq!(repo.count().unwrap(), 0);
|
||||
|
||||
repo.upsert(&create_test_record("c1")).unwrap();
|
||||
repo.upsert(&create_test_record("c2")).unwrap();
|
||||
|
||||
assert_eq!(repo.count().unwrap(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_by_service_type() {
|
||||
let repo = setup_test_db();
|
||||
|
||||
let mut rcoder = create_test_record("c1");
|
||||
rcoder.service_type = ServiceType::RCoder;
|
||||
|
||||
let mut agent = create_test_record("c2");
|
||||
agent.service_type = ServiceType::ComputerAgentRunner;
|
||||
|
||||
repo.upsert(&rcoder).unwrap();
|
||||
repo.upsert(&agent).unwrap();
|
||||
|
||||
let rcoder_containers = repo.find_by_service_type(ServiceType::RCoder).unwrap();
|
||||
assert_eq!(rcoder_containers.len(), 1);
|
||||
assert_eq!(rcoder_containers[0].container_id, "c1");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//! Repository 层模块
|
||||
//!
|
||||
//! 提供数据访问抽象层
|
||||
|
||||
mod container;
|
||||
mod project;
|
||||
|
||||
pub use container::ContainerRepository;
|
||||
pub use project::ProjectRepository;
|
||||
867
qiming-rcoder/crates/duckdb_manager/src/repositories/project.rs
Normal file
867
qiming-rcoder/crates/duckdb_manager/src/repositories/project.rs
Normal file
@@ -0,0 +1,867 @@
|
||||
//! 项目 Repository
|
||||
//!
|
||||
//! 提供项目表的数据访问操作
|
||||
|
||||
use crate::connection::DuckDbConnection;
|
||||
use crate::error::{DuckDbError, DuckDbResult};
|
||||
use crate::models::ProjectRecord;
|
||||
use chrono::{DateTime, Utc};
|
||||
use duckdb::params;
|
||||
use shared_types::ServiceType;
|
||||
|
||||
/// 项目 Repository
|
||||
pub struct ProjectRepository {
|
||||
conn: DuckDbConnection,
|
||||
}
|
||||
|
||||
impl ProjectRepository {
|
||||
/// 创建新的 ProjectRepository
|
||||
pub fn new(conn: DuckDbConnection) -> Self {
|
||||
Self { conn }
|
||||
}
|
||||
|
||||
/// 插入或更新项目记录
|
||||
pub fn upsert(&self, record: &ProjectRecord) -> DuckDbResult<()> {
|
||||
let service_type_str = record.service_type.to_string();
|
||||
let created_at_str = record.created_at.to_rfc3339();
|
||||
let last_activity_str = record.last_activity.to_rfc3339();
|
||||
let session_created_at_str = record.session_created_at.map(|dt| dt.to_rfc3339());
|
||||
let session_last_activity_str = record.session_last_activity.map(|dt| dt.to_rfc3339());
|
||||
|
||||
self.conn.with_connection(|c| {
|
||||
c.execute(
|
||||
r#"
|
||||
INSERT OR REPLACE INTO projects (
|
||||
project_id, session_id, service_type, container_id,
|
||||
user_id, pod_id, agent_status_code, agent_status_name,
|
||||
request_id, model_provider_json, created_at, last_activity,
|
||||
session_created_at, session_last_activity
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
params![
|
||||
record.project_id,
|
||||
record.session_id,
|
||||
service_type_str,
|
||||
record.container_id,
|
||||
record.user_id,
|
||||
record.pod_id,
|
||||
record.agent_status_code,
|
||||
record.agent_status_name,
|
||||
record.request_id,
|
||||
record.model_provider_json,
|
||||
created_at_str,
|
||||
last_activity_str,
|
||||
session_created_at_str,
|
||||
session_last_activity_str,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// 根据项目ID查找项目
|
||||
pub fn find_by_id(&self, project_id: &str) -> DuckDbResult<Option<ProjectRecord>> {
|
||||
self.conn.with_connection(|c| {
|
||||
let mut stmt = c.prepare(
|
||||
r#"
|
||||
SELECT project_id, session_id, service_type, container_id,
|
||||
user_id, pod_id, agent_status_code, agent_status_name,
|
||||
request_id, model_provider_json, created_at, last_activity,
|
||||
session_created_at, session_last_activity
|
||||
FROM projects
|
||||
WHERE project_id = ?
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut rows = stmt.query(params![project_id])?;
|
||||
|
||||
match rows.next()? {
|
||||
Some(row) => Ok(Some(Self::row_to_record(row)?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// 根据会话ID查找项目
|
||||
pub fn find_by_session_id(&self, session_id: &str) -> DuckDbResult<Option<ProjectRecord>> {
|
||||
self.conn.with_connection(|c| {
|
||||
let mut stmt = c.prepare(
|
||||
r#"
|
||||
SELECT project_id, session_id, service_type, container_id,
|
||||
user_id, pod_id, agent_status_code, agent_status_name,
|
||||
request_id, model_provider_json, created_at, last_activity,
|
||||
session_created_at, session_last_activity
|
||||
FROM projects
|
||||
WHERE session_id = ?
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut rows = stmt.query(params![session_id])?;
|
||||
|
||||
match rows.next()? {
|
||||
Some(row) => Ok(Some(Self::row_to_record(row)?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// 根据用户ID查找所有项目 (ComputerAgentRunner 模式)
|
||||
///
|
||||
/// 返回该用户的所有项目记录,按最后活动时间倒序排列
|
||||
pub fn find_projects_by_user_id(&self, user_id: &str) -> DuckDbResult<Vec<ProjectRecord>> {
|
||||
self.conn.with_connection(|c| {
|
||||
let mut stmt = c.prepare(
|
||||
r#"
|
||||
SELECT project_id, session_id, service_type, container_id,
|
||||
user_id, pod_id, agent_status_code, agent_status_name,
|
||||
request_id, model_provider_json, created_at, last_activity,
|
||||
session_created_at, session_last_activity
|
||||
FROM projects
|
||||
WHERE user_id = ?
|
||||
ORDER BY last_activity DESC
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut rows = stmt.query(params![user_id])?;
|
||||
let mut records = Vec::new();
|
||||
|
||||
while let Some(row) = rows.next()? {
|
||||
records.push(Self::row_to_record(row)?);
|
||||
}
|
||||
|
||||
Ok(records)
|
||||
})
|
||||
}
|
||||
|
||||
/// 获取用户最新活跃项目的容器ID (ComputerAgentRunner 核心用例)
|
||||
///
|
||||
/// 在 ComputerAgentRunner 模式下,一个用户对应一个容器,
|
||||
/// 此方法返回该用户最近活跃项目关联的容器ID
|
||||
pub fn get_latest_container_id_by_user_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> DuckDbResult<Option<String>> {
|
||||
self.conn.with_connection(|c| {
|
||||
let mut stmt = c.prepare(
|
||||
r#"
|
||||
SELECT container_id
|
||||
FROM projects
|
||||
WHERE user_id = ?
|
||||
ORDER BY last_activity DESC
|
||||
LIMIT 1
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut rows = stmt.query(params![user_id])?;
|
||||
|
||||
match rows.next()? {
|
||||
Some(row) => {
|
||||
let container_id: String = row.get(0)?;
|
||||
Ok(Some(container_id))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// 根据 pod_id 查找所有项目 (RCoder 共享容器模式)
|
||||
///
|
||||
/// 在 RCoder 共享容器模式下,多个项目可能共享同一个容器(通过 pod_id 标识),
|
||||
/// 返回该 Pod 下所有项目记录,按最后活动时间倒序排列
|
||||
pub fn find_projects_by_pod_id(&self, pod_id: &str) -> DuckDbResult<Vec<ProjectRecord>> {
|
||||
self.conn.with_connection(|c| {
|
||||
let mut stmt = c.prepare(
|
||||
r#"
|
||||
SELECT project_id, session_id, service_type, container_id,
|
||||
user_id, pod_id, agent_status_code, agent_status_name,
|
||||
request_id, model_provider_json, created_at, last_activity,
|
||||
session_created_at, session_last_activity
|
||||
FROM projects
|
||||
WHERE pod_id = ?
|
||||
ORDER BY last_activity DESC
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut rows = stmt.query(params![pod_id])?;
|
||||
let mut records = Vec::new();
|
||||
|
||||
while let Some(row) = rows.next()? {
|
||||
records.push(Self::row_to_record(row)?);
|
||||
}
|
||||
|
||||
Ok(records)
|
||||
})
|
||||
}
|
||||
|
||||
/// 获取 Pod 最新活跃项目的容器ID (共享容器模式)
|
||||
///
|
||||
/// 在共享容器模式下,多个项目可能共享同一个容器(通过 pod_id 标识),
|
||||
/// 此方法返回该 Pod 下最近活跃项目关联的容器ID
|
||||
pub fn get_latest_container_id_by_pod_id(
|
||||
&self,
|
||||
pod_id: &str,
|
||||
) -> DuckDbResult<Option<String>> {
|
||||
self.conn.with_connection(|c| {
|
||||
let mut stmt = c.prepare(
|
||||
r#"
|
||||
SELECT container_id
|
||||
FROM projects
|
||||
WHERE pod_id = ?
|
||||
ORDER BY last_activity DESC
|
||||
LIMIT 1
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut rows = stmt.query(params![pod_id])?;
|
||||
|
||||
match rows.next()? {
|
||||
Some(row) => {
|
||||
let container_id: String = row.get(0)?;
|
||||
Ok(Some(container_id))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// 根据容器ID查找所有关联的项目
|
||||
pub fn find_by_container_id(&self, container_id: &str) -> DuckDbResult<Vec<ProjectRecord>> {
|
||||
self.conn.with_connection(|c| {
|
||||
let mut stmt = c.prepare(
|
||||
r#"
|
||||
SELECT project_id, session_id, service_type, container_id,
|
||||
user_id, pod_id, agent_status_code, agent_status_name,
|
||||
request_id, model_provider_json, created_at, last_activity,
|
||||
session_created_at, session_last_activity
|
||||
FROM projects
|
||||
WHERE container_id = ?
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut rows = stmt.query(params![container_id])?;
|
||||
let mut records = Vec::new();
|
||||
|
||||
while let Some(row) = rows.next()? {
|
||||
records.push(Self::row_to_record(row)?);
|
||||
}
|
||||
|
||||
Ok(records)
|
||||
})
|
||||
}
|
||||
|
||||
/// 删除项目
|
||||
pub fn delete(&self, project_id: &str) -> DuckDbResult<bool> {
|
||||
self.conn.with_connection(|c| {
|
||||
let affected = c.execute(
|
||||
"DELETE FROM projects WHERE project_id = ?",
|
||||
params![project_id],
|
||||
)?;
|
||||
Ok(affected > 0)
|
||||
})
|
||||
}
|
||||
|
||||
/// 根据容器ID删除所有关联的项目
|
||||
pub fn delete_by_container_id(&self, container_id: &str) -> DuckDbResult<usize> {
|
||||
self.conn.with_connection(|c| {
|
||||
let affected = c.execute(
|
||||
"DELETE FROM projects WHERE container_id = ?",
|
||||
params![container_id],
|
||||
)?;
|
||||
Ok(affected)
|
||||
})
|
||||
}
|
||||
|
||||
/// 检查项目是否存在
|
||||
pub fn exists(&self, project_id: &str) -> DuckDbResult<bool> {
|
||||
self.conn.with_connection(|c| {
|
||||
let mut stmt = c.prepare("SELECT 1 FROM projects WHERE project_id = ? LIMIT 1")?;
|
||||
let mut rows = stmt.query(params![project_id])?;
|
||||
Ok(rows.next()?.is_some())
|
||||
})
|
||||
}
|
||||
|
||||
/// 更新项目最后活动时间,返回实际更新使用的时间戳
|
||||
pub fn update_activity(&self, project_id: &str) -> DuckDbResult<Option<DateTime<Utc>>> {
|
||||
let now = Utc::now();
|
||||
let now_str = now.to_rfc3339();
|
||||
self.conn.with_connection(|c| {
|
||||
let affected = c.execute(
|
||||
"UPDATE projects SET last_activity = ? WHERE project_id = ?",
|
||||
params![now_str, project_id],
|
||||
)?;
|
||||
Ok(if affected > 0 { Some(now) } else { None })
|
||||
})
|
||||
}
|
||||
|
||||
/// 更新会话信息
|
||||
pub fn update_session(&self, project_id: &str, session_id: &str) -> DuckDbResult<bool> {
|
||||
let now_str = Utc::now().to_rfc3339();
|
||||
self.conn.with_connection(|c| {
|
||||
let affected = c.execute(
|
||||
r#"
|
||||
UPDATE projects
|
||||
SET session_id = ?,
|
||||
last_activity = ?,
|
||||
session_created_at = COALESCE(session_created_at, ?),
|
||||
session_last_activity = ?
|
||||
WHERE project_id = ?
|
||||
"#,
|
||||
params![session_id, now_str, now_str, now_str, project_id],
|
||||
)?;
|
||||
Ok(affected > 0)
|
||||
})
|
||||
}
|
||||
|
||||
/// 清除会话信息(将 session_id 设置为 NULL)
|
||||
///
|
||||
/// 用于 Agent 停止后清理会话状态
|
||||
pub fn clear_session(&self, project_id: &str) -> DuckDbResult<bool> {
|
||||
let now_str = Utc::now().to_rfc3339();
|
||||
self.conn.with_connection(|c| {
|
||||
let affected = c.execute(
|
||||
r#"
|
||||
UPDATE projects
|
||||
SET session_id = NULL,
|
||||
last_activity = ?,
|
||||
session_last_activity = ?
|
||||
WHERE project_id = ?
|
||||
"#,
|
||||
params![now_str, now_str, project_id],
|
||||
)?;
|
||||
Ok(affected > 0)
|
||||
})
|
||||
}
|
||||
|
||||
/// 更新会话活动时间
|
||||
pub fn update_session_activity(&self, session_id: &str) -> DuckDbResult<bool> {
|
||||
let now_str = Utc::now().to_rfc3339();
|
||||
self.conn.with_connection(|c| {
|
||||
let affected = c.execute(
|
||||
r#"
|
||||
UPDATE projects
|
||||
SET last_activity = ?,
|
||||
session_last_activity = ?
|
||||
WHERE session_id = ?
|
||||
"#,
|
||||
params![now_str, now_str, session_id],
|
||||
)?;
|
||||
Ok(affected > 0)
|
||||
})
|
||||
}
|
||||
|
||||
/// 原子更新 Agent 状态(使用事务)
|
||||
///
|
||||
/// 注意:此方法不会更新 last_activity,只更新状态字段
|
||||
/// 这是为了避免每 30 秒的状态检查刷新活动时间,导致闲置容器永远不会被清理
|
||||
pub fn update_status_atomic(
|
||||
&self,
|
||||
project_id: &str,
|
||||
status_code: i32,
|
||||
status_name: &str,
|
||||
) -> DuckDbResult<bool> {
|
||||
self.conn.transaction(|c| {
|
||||
let affected = c.execute(
|
||||
r#"
|
||||
UPDATE projects
|
||||
SET agent_status_code = ?,
|
||||
agent_status_name = ?
|
||||
WHERE project_id = ?
|
||||
"#,
|
||||
params![status_code, status_name, project_id],
|
||||
)?;
|
||||
Ok(affected > 0)
|
||||
})
|
||||
}
|
||||
|
||||
/// 根据会话ID获取容器名称
|
||||
///
|
||||
/// 与 `get_container_id_by_session` 不同,此方法返回 `container_name` 而不是 `container_id`。
|
||||
/// 容器名称是稳定的(如 `computer-agent-runner-user_123`),即使容器被重建,
|
||||
/// 可以通过 Docker API 查询到新容器。而 container_id 在容器重建后会改变。
|
||||
///
|
||||
/// # 用途
|
||||
/// 用于 SSE 连接验证时,通过 container_name 实时查询 Docker API 获取容器状态。
|
||||
pub fn get_container_name_by_session(&self, session_id: &str) -> DuckDbResult<Option<String>> {
|
||||
self.conn.with_connection(|c| {
|
||||
let mut stmt = c.prepare(
|
||||
r#"
|
||||
SELECT c.container_name
|
||||
FROM projects p
|
||||
JOIN containers c ON p.container_id = c.container_id
|
||||
WHERE p.session_id = ?
|
||||
"#,
|
||||
)?;
|
||||
let mut rows = stmt.query(params![session_id])?;
|
||||
|
||||
match rows.next()? {
|
||||
Some(row) => {
|
||||
let container_name: String = row.get(0)?;
|
||||
Ok(Some(container_name))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// 查找需要清理的项目(闲置超过指定分钟数)
|
||||
pub fn find_projects_for_cleanup(&self, idle_minutes: i64) -> DuckDbResult<Vec<ProjectRecord>> {
|
||||
self.conn.with_connection(|c| {
|
||||
let mut stmt = c.prepare(
|
||||
r#"
|
||||
SELECT project_id, session_id, service_type, container_id,
|
||||
user_id, pod_id, agent_status_code, agent_status_name,
|
||||
request_id, model_provider_json, created_at, last_activity,
|
||||
session_created_at, session_last_activity
|
||||
FROM projects
|
||||
WHERE DATEDIFF('minute', last_activity, NOW()) >= ?
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut rows = stmt.query(params![idle_minutes])?;
|
||||
let mut records = Vec::new();
|
||||
|
||||
while let Some(row) = rows.next()? {
|
||||
records.push(Self::row_to_record(row)?);
|
||||
}
|
||||
|
||||
Ok(records)
|
||||
})
|
||||
}
|
||||
|
||||
/// 获取所有项目
|
||||
pub fn find_all(&self) -> DuckDbResult<Vec<ProjectRecord>> {
|
||||
self.conn.with_connection(|c| {
|
||||
let mut stmt = c.prepare(
|
||||
r#"
|
||||
SELECT project_id, session_id, service_type, container_id,
|
||||
user_id, pod_id, agent_status_code, agent_status_name,
|
||||
request_id, model_provider_json, created_at, last_activity,
|
||||
session_created_at, session_last_activity
|
||||
FROM projects
|
||||
ORDER BY created_at DESC
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut rows = stmt.query([])?;
|
||||
let mut records = Vec::new();
|
||||
|
||||
while let Some(row) = rows.next()? {
|
||||
records.push(Self::row_to_record(row)?);
|
||||
}
|
||||
|
||||
Ok(records)
|
||||
})
|
||||
}
|
||||
|
||||
/// 按服务类型查找项目
|
||||
pub fn find_by_service_type(
|
||||
&self,
|
||||
service_type: ServiceType,
|
||||
) -> DuckDbResult<Vec<ProjectRecord>> {
|
||||
let service_type_str = service_type.to_string();
|
||||
self.conn.with_connection(|c| {
|
||||
let mut stmt = c.prepare(
|
||||
r#"
|
||||
SELECT project_id, session_id, service_type, container_id,
|
||||
user_id, pod_id, agent_status_code, agent_status_name,
|
||||
request_id, model_provider_json, created_at, last_activity,
|
||||
session_created_at, session_last_activity
|
||||
FROM projects
|
||||
WHERE service_type = ?
|
||||
ORDER BY created_at DESC
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut rows = stmt.query(params![service_type_str])?;
|
||||
let mut records = Vec::new();
|
||||
|
||||
while let Some(row) = rows.next()? {
|
||||
records.push(Self::row_to_record(row)?);
|
||||
}
|
||||
|
||||
Ok(records)
|
||||
})
|
||||
}
|
||||
|
||||
/// 获取项目数量统计
|
||||
pub fn count(&self) -> DuckDbResult<usize> {
|
||||
self.conn.with_connection(|c| {
|
||||
let mut stmt = c.prepare("SELECT COUNT(*) FROM projects")?;
|
||||
let mut rows = stmt.query([])?;
|
||||
let row = rows
|
||||
.next()?
|
||||
.ok_or_else(|| DuckDbError::InternalError("unable to get project count".to_string()))?;
|
||||
let count: i64 = row.get(0)?;
|
||||
Ok(count as usize)
|
||||
})
|
||||
}
|
||||
|
||||
/// 获取活跃会话数量
|
||||
pub fn count_active_sessions(&self) -> DuckDbResult<usize> {
|
||||
self.conn.with_connection(|c| {
|
||||
let mut stmt =
|
||||
c.prepare("SELECT COUNT(*) FROM projects WHERE session_id IS NOT NULL")?;
|
||||
let mut rows = stmt.query([])?;
|
||||
let row = rows
|
||||
.next()?
|
||||
.ok_or_else(|| DuckDbError::InternalError("unable to get session count".to_string()))?;
|
||||
let count: i64 = row.get(0)?;
|
||||
Ok(count as usize)
|
||||
})
|
||||
}
|
||||
|
||||
/// 按服务类型统计项目数量
|
||||
pub fn count_by_service_type(
|
||||
&self,
|
||||
) -> DuckDbResult<std::collections::HashMap<ServiceType, usize>> {
|
||||
self.conn.with_connection(|c| {
|
||||
let mut stmt =
|
||||
c.prepare("SELECT service_type, COUNT(*) FROM projects GROUP BY service_type")?;
|
||||
let mut rows = stmt.query([])?;
|
||||
let mut counts = std::collections::HashMap::new();
|
||||
|
||||
while let Some(row) = rows.next()? {
|
||||
let service_type_str: String = row.get(0)?;
|
||||
let count: i64 = row.get(1)?;
|
||||
|
||||
if let Ok(service_type) = service_type_str.parse::<ServiceType>() {
|
||||
counts.insert(service_type, count as usize);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(counts)
|
||||
})
|
||||
}
|
||||
|
||||
/// 更新模型提供商配置
|
||||
pub fn update_model_provider(
|
||||
&self,
|
||||
project_id: &str,
|
||||
model_provider_json: Option<&str>,
|
||||
) -> DuckDbResult<bool> {
|
||||
let now_str = Utc::now().to_rfc3339();
|
||||
self.conn.with_connection(|c| {
|
||||
let affected = c.execute(
|
||||
r#"
|
||||
UPDATE projects
|
||||
SET model_provider_json = ?,
|
||||
last_activity = ?
|
||||
WHERE project_id = ?
|
||||
"#,
|
||||
params![model_provider_json, now_str, project_id],
|
||||
)?;
|
||||
Ok(affected > 0)
|
||||
})
|
||||
}
|
||||
|
||||
/// 更新请求ID
|
||||
pub fn update_request_id(
|
||||
&self,
|
||||
project_id: &str,
|
||||
request_id: Option<&str>,
|
||||
) -> DuckDbResult<bool> {
|
||||
let now_str = Utc::now().to_rfc3339();
|
||||
self.conn.with_connection(|c| {
|
||||
let affected = c.execute(
|
||||
r#"
|
||||
UPDATE projects
|
||||
SET request_id = ?,
|
||||
last_activity = ?
|
||||
WHERE project_id = ?
|
||||
"#,
|
||||
params![request_id, now_str, project_id],
|
||||
)?;
|
||||
Ok(affected > 0)
|
||||
})
|
||||
}
|
||||
|
||||
/// 从数据库行转换为 ProjectRecord
|
||||
fn row_to_record(row: &duckdb::Row<'_>) -> DuckDbResult<ProjectRecord> {
|
||||
let project_id: String = row.get(0)?;
|
||||
let session_id: Option<String> = row.get(1)?;
|
||||
let service_type_str: String = row.get(2)?;
|
||||
let container_id: String = row.get(3)?;
|
||||
let user_id: Option<String> = row.get(4)?;
|
||||
let pod_id: Option<String> = row.get(5)?;
|
||||
let agent_status_code: Option<i32> = row.get(6)?;
|
||||
let agent_status_name: Option<String> = row.get(7)?;
|
||||
let request_id: Option<String> = row.get(8)?;
|
||||
let model_provider_json: Option<String> = row.get(9)?;
|
||||
|
||||
// DuckDB 返回 TIMESTAMP 类型,需要使用 get_ref 并转换
|
||||
let created_at = Self::get_timestamp_from_row(row, 10)?;
|
||||
let last_activity = Self::get_timestamp_from_row(row, 11)?;
|
||||
let session_created_at = Self::get_optional_timestamp_from_row(row, 12)?;
|
||||
let session_last_activity = Self::get_optional_timestamp_from_row(row, 13)?;
|
||||
|
||||
let service_type = service_type_str
|
||||
.parse::<ServiceType>()
|
||||
.map_err(|e| DuckDbError::InternalError(format!("failed to parse service type: {}", e)))?;
|
||||
|
||||
Ok(ProjectRecord {
|
||||
project_id,
|
||||
session_id,
|
||||
service_type,
|
||||
container_id,
|
||||
user_id,
|
||||
pod_id,
|
||||
agent_status_code,
|
||||
agent_status_name,
|
||||
request_id,
|
||||
model_provider_json,
|
||||
created_at,
|
||||
last_activity,
|
||||
session_created_at,
|
||||
session_last_activity,
|
||||
})
|
||||
}
|
||||
|
||||
/// 从行中获取时间戳
|
||||
fn get_timestamp_from_row(row: &duckdb::Row<'_>, idx: usize) -> DuckDbResult<DateTime<Utc>> {
|
||||
use duckdb::types::ValueRef;
|
||||
|
||||
let value_ref = row.get_ref(idx)?;
|
||||
match value_ref {
|
||||
ValueRef::Timestamp(_, micros) => {
|
||||
let secs = micros / 1_000_000;
|
||||
let nsecs = ((micros % 1_000_000) * 1000) as u32;
|
||||
Ok(DateTime::from_timestamp(secs, nsecs).unwrap_or_else(Utc::now))
|
||||
}
|
||||
ValueRef::Text(bytes) => {
|
||||
let s = std::str::from_utf8(bytes)
|
||||
.map_err(|e| DuckDbError::InternalError(format!("UTF8 parsing failed: {}", e)))?;
|
||||
DateTime::parse_from_rfc3339(s)
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
.map_err(|e| DuckDbError::InternalError(format!("timestamp parsing failed: {}", e)))
|
||||
}
|
||||
_ => Ok(Utc::now()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 从行中获取可选时间戳
|
||||
fn get_optional_timestamp_from_row(
|
||||
row: &duckdb::Row<'_>,
|
||||
idx: usize,
|
||||
) -> DuckDbResult<Option<DateTime<Utc>>> {
|
||||
use duckdb::types::ValueRef;
|
||||
|
||||
let value_ref = row.get_ref(idx)?;
|
||||
match value_ref {
|
||||
ValueRef::Null => Ok(None),
|
||||
ValueRef::Timestamp(_, micros) => {
|
||||
let secs = micros / 1_000_000;
|
||||
let nsecs = ((micros % 1_000_000) * 1000) as u32;
|
||||
Ok(Some(
|
||||
DateTime::from_timestamp(secs, nsecs).unwrap_or_else(Utc::now),
|
||||
))
|
||||
}
|
||||
ValueRef::Text(bytes) => {
|
||||
let s = std::str::from_utf8(bytes)
|
||||
.map_err(|e| DuckDbError::InternalError(format!("UTF8 parsing failed: {}", e)))?;
|
||||
DateTime::parse_from_rfc3339(s)
|
||||
.map(|dt| Some(dt.with_timezone(&Utc)))
|
||||
.map_err(|e| DuckDbError::InternalError(format!("timestamp parsing failed: {}", e)))
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::schema::SchemaInitializer;
|
||||
|
||||
fn setup_test_db() -> ProjectRepository {
|
||||
let conn = DuckDbConnection::open_in_memory().unwrap();
|
||||
SchemaInitializer::initialize(&conn).unwrap();
|
||||
ProjectRepository::new(conn)
|
||||
}
|
||||
|
||||
fn create_test_record(id: &str) -> ProjectRecord {
|
||||
ProjectRecord::new(
|
||||
id.to_string(),
|
||||
ServiceType::RCoder,
|
||||
format!("container-{}", id),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_upsert_and_find() {
|
||||
let repo = setup_test_db();
|
||||
let record = create_test_record("p1");
|
||||
|
||||
repo.upsert(&record).unwrap();
|
||||
|
||||
let found = repo.find_by_id("p1").unwrap();
|
||||
assert!(found.is_some());
|
||||
let found = found.unwrap();
|
||||
assert_eq!(found.project_id, "p1");
|
||||
assert_eq!(found.container_id, "container-p1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_session() {
|
||||
let repo = setup_test_db();
|
||||
let record = create_test_record("p1");
|
||||
|
||||
repo.upsert(&record).unwrap();
|
||||
repo.update_session("p1", "session-1").unwrap();
|
||||
|
||||
let found = repo.find_by_id("p1").unwrap().unwrap();
|
||||
assert_eq!(found.session_id, Some("session-1".to_string()));
|
||||
assert!(found.session_created_at.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_by_session_id() {
|
||||
let repo = setup_test_db();
|
||||
let record = create_test_record("p1");
|
||||
|
||||
repo.upsert(&record).unwrap();
|
||||
repo.update_session("p1", "session-1").unwrap();
|
||||
|
||||
let found = repo.find_by_session_id("session-1").unwrap();
|
||||
assert!(found.is_some());
|
||||
assert_eq!(found.unwrap().project_id, "p1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_status_atomic() {
|
||||
let repo = setup_test_db();
|
||||
let record = create_test_record("p1");
|
||||
|
||||
repo.upsert(&record).unwrap();
|
||||
repo.update_status_atomic("p1", 1, "running").unwrap();
|
||||
|
||||
let found = repo.find_by_id("p1").unwrap().unwrap();
|
||||
assert_eq!(found.agent_status_code, Some(1));
|
||||
assert_eq!(found.agent_status_name, Some("running".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete() {
|
||||
let repo = setup_test_db();
|
||||
let record = create_test_record("p1");
|
||||
|
||||
repo.upsert(&record).unwrap();
|
||||
assert!(repo.exists("p1").unwrap());
|
||||
|
||||
repo.delete("p1").unwrap();
|
||||
assert!(!repo.exists("p1").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_by_container_id() {
|
||||
let repo = setup_test_db();
|
||||
|
||||
let mut p1 = create_test_record("p1");
|
||||
p1.container_id = "c1".to_string();
|
||||
let mut p2 = create_test_record("p2");
|
||||
p2.container_id = "c1".to_string();
|
||||
let mut p3 = create_test_record("p3");
|
||||
p3.container_id = "c2".to_string();
|
||||
|
||||
repo.upsert(&p1).unwrap();
|
||||
repo.upsert(&p2).unwrap();
|
||||
repo.upsert(&p3).unwrap();
|
||||
|
||||
let deleted = repo.delete_by_container_id("c1").unwrap();
|
||||
assert_eq!(deleted, 2);
|
||||
|
||||
assert!(!repo.exists("p1").unwrap());
|
||||
assert!(!repo.exists("p2").unwrap());
|
||||
assert!(repo.exists("p3").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_count_active_sessions() {
|
||||
let repo = setup_test_db();
|
||||
|
||||
repo.upsert(&create_test_record("p1")).unwrap();
|
||||
repo.upsert(&create_test_record("p2")).unwrap();
|
||||
repo.upsert(&create_test_record("p3")).unwrap();
|
||||
|
||||
assert_eq!(repo.count_active_sessions().unwrap(), 0);
|
||||
|
||||
repo.update_session("p1", "session-1").unwrap();
|
||||
repo.update_session("p2", "session-2").unwrap();
|
||||
|
||||
assert_eq!(repo.count_active_sessions().unwrap(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_projects_by_user_id() {
|
||||
let repo = setup_test_db();
|
||||
|
||||
let record1 = ProjectRecord::new_with_user_id(
|
||||
"p1".to_string(),
|
||||
"user-1".to_string(),
|
||||
ServiceType::ComputerAgentRunner,
|
||||
"c1".to_string(),
|
||||
);
|
||||
|
||||
let record2 = ProjectRecord::new_with_user_id(
|
||||
"p2".to_string(),
|
||||
"user-1".to_string(),
|
||||
ServiceType::ComputerAgentRunner,
|
||||
"c1".to_string(),
|
||||
);
|
||||
|
||||
repo.upsert(&record1).unwrap();
|
||||
repo.upsert(&record2).unwrap();
|
||||
|
||||
let found = repo.find_projects_by_user_id("user-1").unwrap();
|
||||
assert_eq!(found.len(), 2);
|
||||
|
||||
// 验证按 last_activity 倒序排列
|
||||
let project_ids: Vec<&str> = found.iter().map(|r| r.project_id.as_str()).collect();
|
||||
assert!(project_ids.contains(&"p1"));
|
||||
assert!(project_ids.contains(&"p2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_latest_container_id_by_user_id() {
|
||||
let repo = setup_test_db();
|
||||
|
||||
// 用户没有项目时返回 None
|
||||
let container_id = repo.get_latest_container_id_by_user_id("user-1").unwrap();
|
||||
assert!(container_id.is_none());
|
||||
|
||||
// 创建项目
|
||||
let record = ProjectRecord::new_with_user_id(
|
||||
"p1".to_string(),
|
||||
"user-1".to_string(),
|
||||
ServiceType::ComputerAgentRunner,
|
||||
"c1".to_string(),
|
||||
);
|
||||
repo.upsert(&record).unwrap();
|
||||
|
||||
// 现在应该返回容器ID
|
||||
let container_id = repo.get_latest_container_id_by_user_id("user-1").unwrap();
|
||||
assert_eq!(container_id, Some("c1".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_latest_container_id_by_pod_id() {
|
||||
let repo = setup_test_db();
|
||||
|
||||
// Pod 没有项目时返回 None
|
||||
let container_id = repo.get_latest_container_id_by_pod_id("pod-1").unwrap();
|
||||
assert!(container_id.is_none());
|
||||
|
||||
// 创建带 pod_id 的项目
|
||||
let mut record = ProjectRecord::new_with_user_id(
|
||||
"p1".to_string(),
|
||||
"user-1".to_string(),
|
||||
ServiceType::ComputerAgentRunner,
|
||||
"c1".to_string(),
|
||||
);
|
||||
record.pod_id = Some("pod-1".to_string());
|
||||
repo.upsert(&record).unwrap();
|
||||
|
||||
// 现在应该返回容器ID
|
||||
let container_id = repo.get_latest_container_id_by_pod_id("pod-1").unwrap();
|
||||
assert_eq!(container_id, Some("c1".to_string()));
|
||||
}
|
||||
}
|
||||
204
qiming-rcoder/crates/duckdb_manager/src/schema.rs
Normal file
204
qiming-rcoder/crates/duckdb_manager/src/schema.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
//! DuckDB Schema 定义和初始化
|
||||
//!
|
||||
//! 包含数据库表结构定义和初始化逻辑
|
||||
|
||||
use crate::connection::DuckDbConnection;
|
||||
use crate::error::{DuckDbError, DuckDbResult};
|
||||
|
||||
/// 容器表 DDL
|
||||
const CREATE_CONTAINERS_TABLE: &str = r#"
|
||||
CREATE TABLE IF NOT EXISTS 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,
|
||||
status VARCHAR NOT NULL,
|
||||
service_url VARCHAR NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
last_activity TIMESTAMP NOT NULL
|
||||
)
|
||||
"#;
|
||||
|
||||
/// 项目表 DDL
|
||||
const CREATE_PROJECTS_TABLE: &str = r#"
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
project_id VARCHAR PRIMARY KEY,
|
||||
session_id VARCHAR,
|
||||
service_type VARCHAR NOT NULL,
|
||||
container_id VARCHAR NOT NULL,
|
||||
user_id VARCHAR,
|
||||
pod_id VARCHAR,
|
||||
agent_status_code INTEGER,
|
||||
agent_status_name VARCHAR,
|
||||
request_id VARCHAR,
|
||||
model_provider_json VARCHAR,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
last_activity TIMESTAMP NOT NULL,
|
||||
session_created_at TIMESTAMP,
|
||||
session_last_activity TIMESTAMP
|
||||
)
|
||||
"#;
|
||||
|
||||
/// 容器表索引
|
||||
const CREATE_CONTAINERS_INDEXES: &[&str] = &[
|
||||
"CREATE INDEX IF NOT EXISTS idx_containers_service_type ON containers(service_type)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_containers_status ON containers(status)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_containers_last_activity ON containers(last_activity)",
|
||||
];
|
||||
|
||||
/// 项目表索引
|
||||
const CREATE_PROJECTS_INDEXES: &[&str] = &[
|
||||
"CREATE INDEX IF NOT EXISTS idx_projects_session_id ON projects(session_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_projects_container_id ON projects(container_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_projects_user_id ON projects(user_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_projects_pod_id ON projects(pod_id)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_projects_service_type ON projects(service_type)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_projects_last_activity ON projects(last_activity)",
|
||||
];
|
||||
|
||||
/// Schema 初始化器
|
||||
pub struct SchemaInitializer;
|
||||
|
||||
impl SchemaInitializer {
|
||||
/// 初始化数据库 Schema
|
||||
///
|
||||
/// 创建所有必需的表和索引
|
||||
pub fn initialize(conn: &DuckDbConnection) -> DuckDbResult<()> {
|
||||
conn.with_connection(|c| {
|
||||
// 创建表
|
||||
c.execute(CREATE_CONTAINERS_TABLE, []).map_err(|e| {
|
||||
DuckDbError::InitializationError(format!("failed to create containers table: {}", e))
|
||||
})?;
|
||||
|
||||
c.execute(CREATE_PROJECTS_TABLE, []).map_err(|e| {
|
||||
DuckDbError::InitializationError(format!("failed to create projects table: {}", e))
|
||||
})?;
|
||||
|
||||
// 创建容器表索引
|
||||
for sql in CREATE_CONTAINERS_INDEXES {
|
||||
c.execute(sql, []).map_err(|e| {
|
||||
DuckDbError::InitializationError(format!("failed to create containers table index: {}", e))
|
||||
})?;
|
||||
}
|
||||
|
||||
// 创建项目表索引
|
||||
for sql in CREATE_PROJECTS_INDEXES {
|
||||
c.execute(sql, []).map_err(|e| {
|
||||
DuckDbError::InitializationError(format!("failed to create projects table index: {}", e))
|
||||
})?;
|
||||
}
|
||||
|
||||
tracing::info!("DuckDB Schema initialized");
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// 重置数据库(仅用于测试)
|
||||
#[cfg(test)]
|
||||
pub fn reset(conn: &DuckDbConnection) -> DuckDbResult<()> {
|
||||
conn.with_connection(|c| {
|
||||
c.execute("DROP TABLE IF EXISTS projects", [])?;
|
||||
c.execute("DROP TABLE IF EXISTS containers", [])?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Self::initialize(conn)
|
||||
}
|
||||
|
||||
/// 验证 Schema 是否正确初始化
|
||||
pub fn verify(conn: &DuckDbConnection) -> DuckDbResult<bool> {
|
||||
conn.with_connection(|c| {
|
||||
// 检查 containers 表
|
||||
let containers_exists: i32 = {
|
||||
let mut stmt = c.prepare(
|
||||
"SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'containers'"
|
||||
)?;
|
||||
let mut rows = stmt.query([])?;
|
||||
let row = rows
|
||||
.next()?
|
||||
.ok_or_else(|| DuckDbError::InternalError("unable to query table info".to_string()))?;
|
||||
row.get(0)?
|
||||
};
|
||||
|
||||
// 检查 projects 表
|
||||
let projects_exists: i32 = {
|
||||
let mut stmt = c.prepare(
|
||||
"SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'projects'",
|
||||
)?;
|
||||
let mut rows = stmt.query([])?;
|
||||
let row = rows
|
||||
.next()?
|
||||
.ok_or_else(|| DuckDbError::InternalError("unable to query table info".to_string()))?;
|
||||
row.get(0)?
|
||||
};
|
||||
|
||||
Ok(containers_exists > 0 && projects_exists > 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_initialize_schema() {
|
||||
let conn = DuckDbConnection::open_in_memory().unwrap();
|
||||
let result = SchemaInitializer::initialize(&conn);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_schema() {
|
||||
let conn = DuckDbConnection::open_in_memory().unwrap();
|
||||
SchemaInitializer::initialize(&conn).unwrap();
|
||||
|
||||
let verified = SchemaInitializer::verify(&conn).unwrap();
|
||||
assert!(verified);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_schema() {
|
||||
let conn = DuckDbConnection::open_in_memory().unwrap();
|
||||
SchemaInitializer::initialize(&conn).unwrap();
|
||||
|
||||
// 插入一些数据
|
||||
conn.with_connection(|c| {
|
||||
c.execute(
|
||||
"INSERT INTO containers (container_id, container_name, container_ip, internal_port, external_port, service_type, status, service_url, created_at, last_activity) VALUES ('c1', 'name1', '127.0.0.1', 8080, 8080, 'rcoder', 'running', 'http://localhost', NOW(), NOW())",
|
||||
[],
|
||||
)?;
|
||||
Ok(())
|
||||
}).unwrap();
|
||||
|
||||
// 重置
|
||||
SchemaInitializer::reset(&conn).unwrap();
|
||||
|
||||
// 验证数据已清空
|
||||
let count: i32 = conn
|
||||
.with_connection(|c| {
|
||||
let mut stmt = c.prepare("SELECT COUNT(*) FROM containers")?;
|
||||
let mut rows = stmt.query([])?;
|
||||
let row = rows.next()?.unwrap();
|
||||
Ok(row.get(0)?)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idempotent_initialization() {
|
||||
let conn = DuckDbConnection::open_in_memory().unwrap();
|
||||
|
||||
// 多次初始化应该是幂等的
|
||||
SchemaInitializer::initialize(&conn).unwrap();
|
||||
SchemaInitializer::initialize(&conn).unwrap();
|
||||
SchemaInitializer::initialize(&conn).unwrap();
|
||||
|
||||
let verified = SchemaInitializer::verify(&conn).unwrap();
|
||||
assert!(verified);
|
||||
}
|
||||
}
|
||||
658
qiming-rcoder/crates/duckdb_manager/src/storage.rs
Normal file
658
qiming-rcoder/crates/duckdb_manager/src/storage.rs
Normal file
@@ -0,0 +1,658 @@
|
||||
//! 统一存储接口
|
||||
//!
|
||||
//! 提供 UnifiedStorage trait 和实现,用于抽象数据访问层
|
||||
|
||||
use crate::connection::DuckDbConnection;
|
||||
use crate::error::DuckDbResult;
|
||||
use crate::models::{
|
||||
CleanupResult, ContainerRecord, IdleContainerInfo, ProjectRecord, StorageStats,
|
||||
};
|
||||
use crate::repositories::{ContainerRepository, ProjectRepository};
|
||||
use crate::schema::SchemaInitializer;
|
||||
use shared_types::ServiceType;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// 统一存储接口
|
||||
///
|
||||
/// 提供高层次的数据访问抽象,隐藏底层实现细节
|
||||
pub trait UnifiedStorage: Send + Sync {
|
||||
// ========== 容器操作 ==========
|
||||
|
||||
/// 保存或更新容器
|
||||
fn save_container(&self, record: &ContainerRecord) -> DuckDbResult<()>;
|
||||
|
||||
/// 获取容器
|
||||
fn get_container(&self, container_id: &str) -> DuckDbResult<Option<ContainerRecord>>;
|
||||
|
||||
/// 删除容器
|
||||
fn delete_container(&self, container_id: &str) -> DuckDbResult<bool>;
|
||||
|
||||
/// 检查容器是否存在
|
||||
fn container_exists(&self, container_id: &str) -> DuckDbResult<bool>;
|
||||
|
||||
/// 更新容器活动时间
|
||||
fn update_container_activity(&self, container_id: &str) -> DuckDbResult<bool>;
|
||||
|
||||
/// 使用指定时间更新容器活动时间(用于保持项目和容器时间一致)
|
||||
fn update_container_activity_with_time(
|
||||
&self,
|
||||
container_id: &str,
|
||||
time: chrono::DateTime<chrono::Utc>,
|
||||
) -> DuckDbResult<bool>;
|
||||
|
||||
/// 获取所有容器
|
||||
fn get_all_containers(&self) -> DuckDbResult<Vec<ContainerRecord>>;
|
||||
|
||||
/// 按服务类型获取容器
|
||||
fn get_containers_by_service_type(
|
||||
&self,
|
||||
service_type: ServiceType,
|
||||
) -> DuckDbResult<Vec<ContainerRecord>>;
|
||||
|
||||
/// 查找闲置容器
|
||||
fn find_idle_containers(
|
||||
&self,
|
||||
idle_minutes: i64,
|
||||
protection_minutes: i64,
|
||||
) -> DuckDbResult<Vec<IdleContainerInfo>>;
|
||||
|
||||
// ========== 项目操作 ==========
|
||||
|
||||
/// 保存或更新项目
|
||||
fn save_project(&self, record: &ProjectRecord) -> DuckDbResult<()>;
|
||||
|
||||
/// 获取项目
|
||||
fn get_project(&self, project_id: &str) -> DuckDbResult<Option<ProjectRecord>>;
|
||||
|
||||
/// 删除项目
|
||||
fn delete_project(&self, project_id: &str) -> DuckDbResult<bool>;
|
||||
|
||||
/// 检查项目是否存在
|
||||
fn project_exists(&self, project_id: &str) -> DuckDbResult<bool>;
|
||||
|
||||
/// 更新项目活动时间,返回实际更新使用的时间戳
|
||||
fn update_project_activity(
|
||||
&self,
|
||||
project_id: &str,
|
||||
) -> DuckDbResult<Option<chrono::DateTime<chrono::Utc>>>;
|
||||
|
||||
/// 获取所有项目
|
||||
fn get_all_projects(&self) -> DuckDbResult<Vec<ProjectRecord>>;
|
||||
|
||||
/// 根据用户ID查找所有项目(ComputerAgentRunner模式)
|
||||
///
|
||||
/// 返回该用户的所有项目记录,按最后活动时间倒序排列
|
||||
fn find_projects_by_user_id(&self, user_id: &str) -> DuckDbResult<Vec<ProjectRecord>>;
|
||||
|
||||
/// 获取用户最新活跃项目的容器ID(ComputerAgentRunner核心用例)
|
||||
///
|
||||
/// 在 ComputerAgentRunner 模式下,一个用户对应一个容器,
|
||||
/// 此方法返回该用户最近活跃项目关联的容器ID
|
||||
fn get_latest_container_id_by_user_id(&self, user_id: &str) -> DuckDbResult<Option<String>>;
|
||||
|
||||
/// 获取 Pod 最新活跃项目的容器ID(共享容器模式)
|
||||
///
|
||||
/// 在共享容器模式下,多个项目可能共享同一个容器(通过 pod_id 标识),
|
||||
/// 此方法返回该 Pod 下最近活跃项目关联的容器ID
|
||||
fn get_latest_container_id_by_pod_id(&self, pod_id: &str) -> DuckDbResult<Option<String>>;
|
||||
|
||||
/// 根据 pod_id 查找所有项目(RCoder 共享容器模式)
|
||||
///
|
||||
/// 返回该 Pod 下所有项目记录,按最后活动时间倒序排列
|
||||
fn find_projects_by_pod_id(&self, pod_id: &str) -> DuckDbResult<Vec<ProjectRecord>>;
|
||||
|
||||
// ========== 会话操作 ==========
|
||||
|
||||
/// 根据会话ID获取项目
|
||||
fn get_project_by_session(&self, session_id: &str) -> DuckDbResult<Option<ProjectRecord>>;
|
||||
|
||||
/// 根据会话ID获取容器名称
|
||||
///
|
||||
/// 与 `get_container_id_by_session` 不同,返回稳定的 `container_name`。
|
||||
/// 即使容器被重建,container_name 保持不变,可用于通过 Docker API 查询容器状态。
|
||||
fn get_container_name_by_session(&self, session_id: &str) -> DuckDbResult<Option<String>>;
|
||||
|
||||
/// 更新会话
|
||||
fn update_session(&self, project_id: &str, session_id: &str) -> DuckDbResult<bool>;
|
||||
|
||||
/// 清除会话(将 session_id 设置为 NULL)
|
||||
fn clear_session(&self, project_id: &str) -> DuckDbResult<bool>;
|
||||
|
||||
/// 更新会话活动时间
|
||||
fn update_session_activity(&self, session_id: &str) -> DuckDbResult<bool>;
|
||||
|
||||
// ========== 状态操作 ==========
|
||||
|
||||
/// 原子更新 Agent 状态
|
||||
fn update_agent_status(
|
||||
&self,
|
||||
project_id: &str,
|
||||
status_code: i32,
|
||||
status_name: &str,
|
||||
) -> DuckDbResult<bool>;
|
||||
|
||||
// ========== 关联操作 ==========
|
||||
|
||||
/// 根据容器ID获取关联的项目
|
||||
fn get_projects_by_container(&self, container_id: &str) -> DuckDbResult<Vec<ProjectRecord>>;
|
||||
|
||||
/// 删除容器及其关联的项目
|
||||
fn delete_container_with_projects(&self, container_id: &str) -> DuckDbResult<(bool, usize)>;
|
||||
|
||||
// ========== 清理操作 ==========
|
||||
|
||||
/// 执行清理(删除闲置的容器和项目)
|
||||
fn cleanup(&self, idle_minutes: i64, protection_minutes: i64) -> DuckDbResult<CleanupResult>;
|
||||
|
||||
// ========== 统计操作 ==========
|
||||
|
||||
/// 获取存储统计信息
|
||||
fn get_stats(&self) -> DuckDbResult<StorageStats>;
|
||||
}
|
||||
|
||||
/// DuckDB 统一存储实现
|
||||
pub struct DuckDbStorage {
|
||||
conn: DuckDbConnection,
|
||||
}
|
||||
|
||||
impl DuckDbStorage {
|
||||
/// 创建新的 DuckDB 存储
|
||||
pub fn new() -> DuckDbResult<Self> {
|
||||
let conn = DuckDbConnection::open_in_memory()?;
|
||||
SchemaInitializer::initialize(&conn)?;
|
||||
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
/// 从现有连接创建存储
|
||||
pub fn from_connection(conn: DuckDbConnection) -> DuckDbResult<Self> {
|
||||
SchemaInitializer::initialize(&conn)?;
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
/// 获取容器 Repository
|
||||
///
|
||||
/// 注意:返回的 Repository 共享同一个 `Arc<Mutex<Connection>>`,
|
||||
/// 确保并发访问时的线程安全。
|
||||
fn containers(&self) -> DuckDbResult<ContainerRepository> {
|
||||
Ok(ContainerRepository::new(self.conn.clone()))
|
||||
}
|
||||
|
||||
/// 获取项目 Repository
|
||||
///
|
||||
/// 注意:返回的 Repository 共享同一个 `Arc<Mutex<Connection>>`,
|
||||
/// 确保并发访问时的线程安全。
|
||||
fn projects(&self) -> DuckDbResult<ProjectRepository> {
|
||||
Ok(ProjectRepository::new(self.conn.clone()))
|
||||
}
|
||||
|
||||
/// 执行原始 SQL 查询(仅用于调试)
|
||||
///
|
||||
/// ⚠️ 此方法仅用于开发和调试目的
|
||||
pub fn execute_raw_query(
|
||||
&self,
|
||||
sql: &str,
|
||||
) -> DuckDbResult<(Vec<String>, Vec<serde_json::Value>)> {
|
||||
self.conn.with_connection(|c| {
|
||||
let mut stmt = c.prepare(sql)?;
|
||||
let column_count = stmt.column_count();
|
||||
|
||||
// 获取列名
|
||||
let columns: Vec<String> = (0..column_count)
|
||||
.map(|i| {
|
||||
stmt.column_name(i)
|
||||
.map_or("?".to_string(), |v| v.to_string())
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 执行查询并收集结果
|
||||
let mut rows: Vec<serde_json::Value> = Vec::new();
|
||||
let mut result_rows = stmt.query([])?;
|
||||
|
||||
while let Some(row) = result_rows.next()? {
|
||||
let mut row_obj = serde_json::Map::new();
|
||||
for (i, col_name) in columns.iter().enumerate() {
|
||||
// 尝试以字符串形式获取值
|
||||
let value: serde_json::Value = if let Ok(v) = row.get::<_, String>(i) {
|
||||
serde_json::Value::String(v)
|
||||
} else if let Ok(v) = row.get::<_, i64>(i) {
|
||||
serde_json::Value::Number(v.into())
|
||||
} else if let Ok(v) = row.get::<_, f64>(i) {
|
||||
if let Some(n) = serde_json::Number::from_f64(v) {
|
||||
serde_json::Value::Number(n)
|
||||
} else {
|
||||
serde_json::Value::Null
|
||||
}
|
||||
} else if let Ok(v) = row.get::<_, bool>(i) {
|
||||
serde_json::Value::Bool(v)
|
||||
} else {
|
||||
serde_json::Value::Null
|
||||
};
|
||||
row_obj.insert(col_name.clone(), value);
|
||||
}
|
||||
rows.push(serde_json::Value::Object(row_obj));
|
||||
}
|
||||
|
||||
Ok((columns, rows))
|
||||
})
|
||||
}
|
||||
|
||||
/// 获取 DuckDB 内存使用统计(用于调试和监控)
|
||||
///
|
||||
/// 返回内存使用的详细信息,帮助定位内存问题
|
||||
pub fn get_memory_stats(&self) -> DuckDbResult<String> {
|
||||
self.conn.get_memory_stats()
|
||||
}
|
||||
}
|
||||
|
||||
impl UnifiedStorage for DuckDbStorage {
|
||||
// ========== 容器操作 ==========
|
||||
|
||||
fn save_container(&self, record: &ContainerRecord) -> DuckDbResult<()> {
|
||||
self.containers()?.upsert(record)
|
||||
}
|
||||
|
||||
fn get_container(&self, container_id: &str) -> DuckDbResult<Option<ContainerRecord>> {
|
||||
self.containers()?.find_by_id(container_id)
|
||||
}
|
||||
|
||||
fn delete_container(&self, container_id: &str) -> DuckDbResult<bool> {
|
||||
self.containers()?.delete(container_id)
|
||||
}
|
||||
|
||||
fn container_exists(&self, container_id: &str) -> DuckDbResult<bool> {
|
||||
self.containers()?.exists(container_id)
|
||||
}
|
||||
|
||||
fn update_container_activity(&self, container_id: &str) -> DuckDbResult<bool> {
|
||||
self.containers()?.update_activity(container_id)
|
||||
}
|
||||
|
||||
fn update_container_activity_with_time(
|
||||
&self,
|
||||
container_id: &str,
|
||||
time: chrono::DateTime<chrono::Utc>,
|
||||
) -> DuckDbResult<bool> {
|
||||
self.containers()?
|
||||
.update_activity_with_time(container_id, time)
|
||||
}
|
||||
|
||||
fn get_all_containers(&self) -> DuckDbResult<Vec<ContainerRecord>> {
|
||||
self.containers()?.find_all()
|
||||
}
|
||||
|
||||
fn get_containers_by_service_type(
|
||||
&self,
|
||||
service_type: ServiceType,
|
||||
) -> DuckDbResult<Vec<ContainerRecord>> {
|
||||
self.containers()?.find_by_service_type(service_type)
|
||||
}
|
||||
|
||||
fn find_idle_containers(
|
||||
&self,
|
||||
idle_minutes: i64,
|
||||
protection_minutes: i64,
|
||||
) -> DuckDbResult<Vec<IdleContainerInfo>> {
|
||||
let mut idle_containers = self
|
||||
.containers()?
|
||||
.find_idle_containers(idle_minutes, protection_minutes)?;
|
||||
|
||||
// 为每个闲置容器填充关联的项目ID
|
||||
let projects_repo = self.projects()?;
|
||||
for container in &mut idle_containers {
|
||||
let projects = projects_repo.find_by_container_id(&container.container_id)?;
|
||||
container.project_ids = projects.iter().map(|p| p.project_id.clone()).collect();
|
||||
}
|
||||
|
||||
Ok(idle_containers)
|
||||
}
|
||||
|
||||
// ========== 项目操作 ==========
|
||||
|
||||
fn save_project(&self, record: &ProjectRecord) -> DuckDbResult<()> {
|
||||
self.projects()?.upsert(record)
|
||||
}
|
||||
|
||||
fn get_project(&self, project_id: &str) -> DuckDbResult<Option<ProjectRecord>> {
|
||||
self.projects()?.find_by_id(project_id)
|
||||
}
|
||||
|
||||
fn delete_project(&self, project_id: &str) -> DuckDbResult<bool> {
|
||||
self.projects()?.delete(project_id)
|
||||
}
|
||||
|
||||
fn project_exists(&self, project_id: &str) -> DuckDbResult<bool> {
|
||||
self.projects()?.exists(project_id)
|
||||
}
|
||||
|
||||
fn update_project_activity(
|
||||
&self,
|
||||
project_id: &str,
|
||||
) -> DuckDbResult<Option<chrono::DateTime<chrono::Utc>>> {
|
||||
self.projects()?.update_activity(project_id)
|
||||
}
|
||||
|
||||
fn get_all_projects(&self) -> DuckDbResult<Vec<ProjectRecord>> {
|
||||
self.projects()?.find_all()
|
||||
}
|
||||
|
||||
fn find_projects_by_user_id(&self, user_id: &str) -> DuckDbResult<Vec<ProjectRecord>> {
|
||||
self.projects()?.find_projects_by_user_id(user_id)
|
||||
}
|
||||
|
||||
fn get_latest_container_id_by_user_id(&self, user_id: &str) -> DuckDbResult<Option<String>> {
|
||||
self.projects()?.get_latest_container_id_by_user_id(user_id)
|
||||
}
|
||||
|
||||
fn get_latest_container_id_by_pod_id(&self, pod_id: &str) -> DuckDbResult<Option<String>> {
|
||||
self.projects()?.get_latest_container_id_by_pod_id(pod_id)
|
||||
}
|
||||
|
||||
fn find_projects_by_pod_id(&self, pod_id: &str) -> DuckDbResult<Vec<ProjectRecord>> {
|
||||
self.projects()?.find_projects_by_pod_id(pod_id)
|
||||
}
|
||||
|
||||
// ========== 会话操作 ==========
|
||||
|
||||
fn get_project_by_session(&self, session_id: &str) -> DuckDbResult<Option<ProjectRecord>> {
|
||||
self.projects()?.find_by_session_id(session_id)
|
||||
}
|
||||
|
||||
fn get_container_name_by_session(&self, session_id: &str) -> DuckDbResult<Option<String>> {
|
||||
self.projects()?.get_container_name_by_session(session_id)
|
||||
}
|
||||
|
||||
fn update_session(&self, project_id: &str, session_id: &str) -> DuckDbResult<bool> {
|
||||
self.projects()?.update_session(project_id, session_id)
|
||||
}
|
||||
|
||||
fn clear_session(&self, project_id: &str) -> DuckDbResult<bool> {
|
||||
self.projects()?.clear_session(project_id)
|
||||
}
|
||||
|
||||
/// 更新会话活动时间(同时也更新关联容器的活动时间)
|
||||
fn update_session_activity(&self, session_id: &str) -> DuckDbResult<bool> {
|
||||
let updated = self.projects()?.update_session_activity(session_id)?;
|
||||
|
||||
// 如果会话活跃,同时也更新关联容器的活跃状态
|
||||
if updated {
|
||||
// 使用新方法直接通过 session_id 更新容器活动时间,无需获取 container_id
|
||||
let _ = self.containers()?.update_activity_by_session(session_id);
|
||||
}
|
||||
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
// ========== 状态操作 ==========
|
||||
|
||||
fn update_agent_status(
|
||||
&self,
|
||||
project_id: &str,
|
||||
status_code: i32,
|
||||
status_name: &str,
|
||||
) -> DuckDbResult<bool> {
|
||||
self.projects()?
|
||||
.update_status_atomic(project_id, status_code, status_name)
|
||||
}
|
||||
|
||||
// ========== 关联操作 ==========
|
||||
|
||||
fn get_projects_by_container(&self, container_id: &str) -> DuckDbResult<Vec<ProjectRecord>> {
|
||||
self.projects()?.find_by_container_id(container_id)
|
||||
}
|
||||
|
||||
fn delete_container_with_projects(&self, container_id: &str) -> DuckDbResult<(bool, usize)> {
|
||||
// 先删除关联的项目
|
||||
let deleted_projects = self.projects()?.delete_by_container_id(container_id)?;
|
||||
|
||||
// 再删除容器
|
||||
let container_deleted = self.containers()?.delete(container_id)?;
|
||||
|
||||
Ok((container_deleted, deleted_projects))
|
||||
}
|
||||
|
||||
// ========== 清理操作 ==========
|
||||
|
||||
fn cleanup(&self, idle_minutes: i64, protection_minutes: i64) -> DuckDbResult<CleanupResult> {
|
||||
let mut result = CleanupResult::new();
|
||||
|
||||
// 查找闲置容器
|
||||
let idle_containers = self.find_idle_containers(idle_minutes, protection_minutes)?;
|
||||
|
||||
for container in idle_containers {
|
||||
// 删除容器及其关联的项目
|
||||
match self.delete_container_with_projects(&container.container_id) {
|
||||
Ok((container_deleted, projects_deleted)) => {
|
||||
if container_deleted {
|
||||
result.cleaned_containers += 1;
|
||||
}
|
||||
result.cleaned_projects += projects_deleted;
|
||||
}
|
||||
Err(e) => {
|
||||
result.add_error(format!("failed to delete container {}: {}", container.container_id, e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// ========== 统计操作 ==========
|
||||
|
||||
fn get_stats(&self) -> DuckDbResult<StorageStats> {
|
||||
let containers = self.containers()?;
|
||||
let projects = self.projects()?;
|
||||
|
||||
let total_containers = containers.count()?;
|
||||
let total_projects = projects.count()?;
|
||||
let active_sessions = projects.count_active_sessions()?;
|
||||
let projects_by_service_type = projects.count_by_service_type()?;
|
||||
|
||||
// 计算活跃和闲置容器
|
||||
let all_containers = containers.find_all()?;
|
||||
let idle_threshold_minutes = 30;
|
||||
|
||||
let mut active_containers = 0;
|
||||
let mut idle_containers = 0;
|
||||
|
||||
for container in &all_containers {
|
||||
if container.is_idle(idle_threshold_minutes) {
|
||||
idle_containers += 1;
|
||||
} else {
|
||||
active_containers += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(StorageStats {
|
||||
total_containers,
|
||||
total_projects,
|
||||
active_sessions,
|
||||
active_containers,
|
||||
idle_containers,
|
||||
projects_by_service_type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for DuckDbStorage {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
conn: self.conn.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for DuckDbStorage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("DuckDbStorage").finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建共享的 UnifiedStorage 实例
|
||||
pub fn create_storage() -> DuckDbResult<Arc<dyn UnifiedStorage>> {
|
||||
Ok(Arc::new(DuckDbStorage::new()?))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_test_storage() -> DuckDbStorage {
|
||||
DuckDbStorage::new().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_container_crud() {
|
||||
let storage = create_test_storage();
|
||||
|
||||
let record = ContainerRecord::new(
|
||||
"c1".to_string(),
|
||||
"container-1".to_string(),
|
||||
"127.0.0.1".to_string(),
|
||||
8080,
|
||||
8080,
|
||||
ServiceType::RCoder,
|
||||
"running".to_string(),
|
||||
"http://localhost:8080".to_string(),
|
||||
);
|
||||
|
||||
// Create
|
||||
storage.save_container(&record).unwrap();
|
||||
|
||||
// Read
|
||||
let found = storage.get_container("c1").unwrap();
|
||||
assert!(found.is_some());
|
||||
assert_eq!(found.unwrap().container_name, "container-1");
|
||||
|
||||
// Update
|
||||
storage.update_container_activity("c1").unwrap();
|
||||
|
||||
// Delete
|
||||
storage.delete_container("c1").unwrap();
|
||||
assert!(!storage.container_exists("c1").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_project_crud() {
|
||||
let storage = create_test_storage();
|
||||
|
||||
let record = ProjectRecord::new("p1".to_string(), ServiceType::RCoder, "c1".to_string());
|
||||
|
||||
// Create
|
||||
storage.save_project(&record).unwrap();
|
||||
|
||||
// Read
|
||||
let found = storage.get_project("p1").unwrap();
|
||||
assert!(found.is_some());
|
||||
|
||||
// Update
|
||||
storage.update_project_activity("p1").unwrap();
|
||||
|
||||
// Delete
|
||||
storage.delete_project("p1").unwrap();
|
||||
assert!(!storage.project_exists("p1").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_operations() {
|
||||
let storage = create_test_storage();
|
||||
|
||||
// 创建容器 (必须先创建容器,因为项目关联到容器)
|
||||
let container = ContainerRecord::new(
|
||||
"c1".to_string(),
|
||||
"container-1".to_string(),
|
||||
"127.0.0.1".to_string(),
|
||||
8080,
|
||||
8080,
|
||||
ServiceType::RCoder,
|
||||
"running".to_string(),
|
||||
"http://localhost:8080".to_string(),
|
||||
);
|
||||
storage.save_container(&container).unwrap();
|
||||
|
||||
// 创建项目
|
||||
let record = ProjectRecord::new("p1".to_string(), ServiceType::RCoder, "c1".to_string());
|
||||
storage.save_project(&record).unwrap();
|
||||
|
||||
// 更新会话
|
||||
storage.update_session("p1", "session-1").unwrap();
|
||||
|
||||
// 通过会话ID查询
|
||||
let project = storage.get_project_by_session("session-1").unwrap();
|
||||
assert!(project.is_some());
|
||||
assert_eq!(project.unwrap().project_id, "p1");
|
||||
|
||||
// 获取容器名称
|
||||
let container_name = storage.get_container_name_by_session("session-1").unwrap();
|
||||
assert_eq!(container_name, Some("container-1".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_container_with_projects() {
|
||||
let storage = create_test_storage();
|
||||
|
||||
// 创建容器
|
||||
let container = ContainerRecord::new(
|
||||
"c1".to_string(),
|
||||
"container-1".to_string(),
|
||||
"127.0.0.1".to_string(),
|
||||
8080,
|
||||
8080,
|
||||
ServiceType::RCoder,
|
||||
"running".to_string(),
|
||||
"http://localhost:8080".to_string(),
|
||||
);
|
||||
storage.save_container(&container).unwrap();
|
||||
|
||||
// 创建多个关联项目
|
||||
for i in 1..=3 {
|
||||
let project =
|
||||
ProjectRecord::new(format!("p{}", i), ServiceType::RCoder, "c1".to_string());
|
||||
storage.save_project(&project).unwrap();
|
||||
}
|
||||
|
||||
// 删除容器及关联项目
|
||||
let (container_deleted, projects_deleted) =
|
||||
storage.delete_container_with_projects("c1").unwrap();
|
||||
|
||||
assert!(container_deleted);
|
||||
assert_eq!(projects_deleted, 3);
|
||||
|
||||
// 验证数据已删除
|
||||
assert!(!storage.container_exists("c1").unwrap());
|
||||
assert!(!storage.project_exists("p1").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_stats() {
|
||||
let storage = create_test_storage();
|
||||
|
||||
// 添加容器
|
||||
let container = ContainerRecord::new(
|
||||
"c1".to_string(),
|
||||
"container-1".to_string(),
|
||||
"127.0.0.1".to_string(),
|
||||
8080,
|
||||
8080,
|
||||
ServiceType::RCoder,
|
||||
"running".to_string(),
|
||||
"http://localhost:8080".to_string(),
|
||||
);
|
||||
storage.save_container(&container).unwrap();
|
||||
|
||||
// 添加项目
|
||||
let project = ProjectRecord::new("p1".to_string(), ServiceType::RCoder, "c1".to_string());
|
||||
storage.save_project(&project).unwrap();
|
||||
|
||||
// 添加会话
|
||||
storage.update_session("p1", "session-1").unwrap();
|
||||
|
||||
let stats = storage.get_stats().unwrap();
|
||||
|
||||
assert_eq!(stats.total_containers, 1);
|
||||
assert_eq!(stats.total_projects, 1);
|
||||
assert_eq!(stats.active_sessions, 1);
|
||||
assert_eq!(stats.active_containers, 1);
|
||||
assert_eq!(stats.idle_containers, 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user