添加qiming-rcoder模块
This commit is contained in:
@@ -0,0 +1,403 @@
|
||||
//! Computer Agent Runner 容器管理服务
|
||||
//!
|
||||
//! 提供用户级容器的创建和管理逻辑。
|
||||
//! 与 RCoder 的 project_id 容器模式不同,ComputerAgentRunner 使用 user_id 作为容器标识。
|
||||
//!
|
||||
//! ## 与 RCoder ContainerManager 的区别
|
||||
//!
|
||||
//! | 维度 | RCoder | ComputerAgentRunner |
|
||||
//! |------|--------|---------------------|
|
||||
//! | 容器标识 | `project_id` | `user_id` |
|
||||
//! | 容器命名 | `rcoder-agent-{project_id}` | `computer-agent-runner-{user_id}` |
|
||||
//! | 工作目录 | `/app/project_workspace/{project_id}` | `/home/user` (通过 mounts 配置挂载) |
|
||||
//! | 挂载配置 | 硬编码 | config.yml mounts (配置化) |
|
||||
//! | Agent 实例 | 1 个 | 多个(按 project_id 区分) |
|
||||
|
||||
use crate::AppError;
|
||||
use crate::handler::utils::{COMPUTER_WORKSPACE_ROOT, user_dir};
|
||||
use container_runtime_api::{ContainerCreateParams, ContainerRuntime};
|
||||
use docker_manager::ContainerBasicInfo;
|
||||
use shared_types::error_codes::{ERR_CONTAINER_ERROR, ERR_WORKSPACE_ERROR};
|
||||
use shared_types::{ServiceResourceLimits, ServiceType};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
/// Computer Agent Runner 容器管理服务
|
||||
///
|
||||
/// 负责根据 `user_id` 获取或创建容器。
|
||||
/// 一个用户对应一个容器,容器内可以运行多个 project_id 的 Agent 实例。
|
||||
pub struct ComputerContainerManager;
|
||||
|
||||
impl ComputerContainerManager {
|
||||
/// 根据 user_id 或 pod_id 获取或创建容器
|
||||
///
|
||||
/// 容器命名规则: `computer-agent-runner-{pod_id}` 或 `computer-agent-runner-{user_id}`
|
||||
/// 工作区路径: `/app/computer-project-workspace/{user_id}` 或基于 isolation 的路径
|
||||
///
|
||||
/// # 参数
|
||||
/// - `user_id`: 用户唯一标识符
|
||||
/// - `resource_limits`: 可选的资源限额配置
|
||||
/// - `pod_id`: 可选的容器唯一标识,若提供则使用此 ID 作为容器标识(实现容器复用)
|
||||
/// - `isolation_type`: 隔离类型
|
||||
/// - `tenant_id`: 租户 ID
|
||||
/// - `space_id`: 空间 ID
|
||||
///
|
||||
/// # 返回
|
||||
/// 容器基本信息,包含容器 ID、IP 地址等
|
||||
///
|
||||
/// # 示例
|
||||
/// ```ignore
|
||||
/// let container_info = ComputerContainerManager::get_or_create_container_for_user(
|
||||
/// "user_123",
|
||||
/// None,
|
||||
/// None,
|
||||
/// None,
|
||||
/// None,
|
||||
/// None,
|
||||
/// ).await?;
|
||||
/// println!("Container IP: {}", container_info.container_ip);
|
||||
/// ```
|
||||
pub async fn get_or_create_container_for_user(
|
||||
user_id: &str,
|
||||
resource_limits: Option<ServiceResourceLimits>,
|
||||
pod_id: Option<&str>,
|
||||
isolation_type: Option<&str>,
|
||||
tenant_id: Option<&str>,
|
||||
space_id: Option<&str>,
|
||||
) -> Result<ContainerBasicInfo, AppError> {
|
||||
// 确定容器标识符:pod_id 有值时使用 pod_id,否则使用 user_id
|
||||
let container_identifier = pod_id.unwrap_or(user_id);
|
||||
|
||||
info!(
|
||||
"🔍 [COMPUTER_CONTAINER] Getting/creating user container: user_id={}, pod_id={:?}, container_identifier={}",
|
||||
user_id, pod_id, container_identifier
|
||||
);
|
||||
|
||||
let runtime = docker_manager::runtime::RuntimeManager::get()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("[COMPUTER_CONTAINER] Failed to get runtime: {}", e);
|
||||
AppError::with_message(
|
||||
ERR_CONTAINER_ERROR,
|
||||
format!("Failed to get runtime: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
// 1. 尝试获取现有容器
|
||||
// 使用 container_identifier 作为容器标识进行查询
|
||||
if let Ok(Some(info)) = runtime
|
||||
.get_container_info_by_identifier(container_identifier, &ServiceType::ComputerAgentRunner)
|
||||
.await
|
||||
{
|
||||
// ✅ 关键修复: 先验证 IP 是否有效,再检查容器运行状态
|
||||
// 顺序很重要:IP 为空说明容器已异常(被 kill 后网络已销毁),
|
||||
// 此时不应再调用 is_container_running_by_identifier(可能因缓存返回错误结果)
|
||||
if info.container_ip.trim().is_empty() {
|
||||
warn!(
|
||||
"⚠️ [COMPUTER_CONTAINER] Container has empty IP (likely killed externally), will recreate: container_identifier={}, container_id={}",
|
||||
container_identifier, info.container_id
|
||||
);
|
||||
// 尝试清理已失效的容器
|
||||
if let Err(e) = runtime
|
||||
.stop_container_by_identifier(container_identifier, &ServiceType::ComputerAgentRunner)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"⚠️ [COMPUTER_CONTAINER] Failed to cleanup broken container (will create new anyway): {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
// 继续创建新容器
|
||||
} else {
|
||||
// IP 非空,进一步验证容器是否真的在运行
|
||||
match runtime
|
||||
.is_container_running_by_identifier(container_identifier, &ServiceType::ComputerAgentRunner)
|
||||
.await
|
||||
{
|
||||
Ok(true) => {
|
||||
info!(
|
||||
"✅ [COMPUTER_CONTAINER] User container already exists and running: container_identifier={}, container_id={}, ip={}",
|
||||
container_identifier, info.container_id, info.container_ip
|
||||
);
|
||||
return Ok(info);
|
||||
}
|
||||
Ok(false) => {
|
||||
warn!(
|
||||
"⚠️ [COMPUTER_CONTAINER] User container exists but stopped: container_identifier={}, container_id={}, will delete and recreate",
|
||||
container_identifier, info.container_id
|
||||
);
|
||||
if let Err(e) = runtime
|
||||
.stop_container_by_identifier(container_identifier, &ServiceType::ComputerAgentRunner)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"⚠️ [COMPUTER_CONTAINER] Failed to delete old container (will create new container anyway): {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
// 继续创建新容器
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"⚠️ [COMPUTER_CONTAINER] Failed to check container status: container_identifier={}, error={}, will try creating new container",
|
||||
container_identifier, e
|
||||
);
|
||||
// 继续创建新容器
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 容器不存在或已停止,创建新容器
|
||||
info!(
|
||||
"🏗️ [COMPUTER_CONTAINER] Creating new user container: container_identifier={}",
|
||||
container_identifier
|
||||
);
|
||||
Self::create_container_for_user(
|
||||
user_id,
|
||||
&runtime,
|
||||
resource_limits,
|
||||
pod_id,
|
||||
isolation_type,
|
||||
tenant_id,
|
||||
space_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// 强制为用户创建新容器(跳过检查)
|
||||
///
|
||||
/// 直接调用内部创建逻辑,用于重启等需要强制重建的场景。
|
||||
/// 调用前应确保旧容器已被移除。
|
||||
pub async fn force_create_container_for_user(
|
||||
user_id: &str,
|
||||
resource_limits: Option<ServiceResourceLimits>,
|
||||
pod_id: Option<&str>,
|
||||
isolation_type: Option<&str>,
|
||||
tenant_id: Option<&str>,
|
||||
space_id: Option<&str>,
|
||||
) -> Result<ContainerBasicInfo, AppError> {
|
||||
let container_identifier = pod_id.unwrap_or(user_id);
|
||||
info!(
|
||||
"🏗️ [COMPUTER_CONTAINER] Force creating new user container: container_identifier={}",
|
||||
container_identifier
|
||||
);
|
||||
|
||||
let runtime = docker_manager::runtime::RuntimeManager::get()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("[COMPUTER_CONTAINER] Failed to get runtime: {}", e);
|
||||
AppError::with_message(
|
||||
ERR_CONTAINER_ERROR,
|
||||
format!("Failed to get runtime: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Self::create_container_for_user(
|
||||
user_id,
|
||||
&runtime,
|
||||
resource_limits,
|
||||
pod_id,
|
||||
isolation_type,
|
||||
tenant_id,
|
||||
space_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// 为用户创建容器
|
||||
///
|
||||
/// 内部方法,负责实际的容器创建逻辑。
|
||||
async fn create_container_for_user(
|
||||
user_id: &str,
|
||||
runtime: &Arc<dyn ContainerRuntime>,
|
||||
resource_limits: Option<ServiceResourceLimits>,
|
||||
pod_id: Option<&str>,
|
||||
isolation_type: Option<&str>,
|
||||
tenant_id: Option<&str>,
|
||||
space_id: Option<&str>,
|
||||
) -> Result<ContainerBasicInfo, AppError> {
|
||||
// 确定容器标识符:pod_id 有值时使用 pod_id,否则使用 user_id
|
||||
let container_identifier = pod_id.unwrap_or(user_id);
|
||||
|
||||
// 1. 准备用户级工作目录(仍需在 rcoder 容器内创建)
|
||||
// 在容器内创建目录,绑定挂载会自动同步到宿主机
|
||||
Self::create_user_workspace(user_id).await?;
|
||||
|
||||
info!(
|
||||
"📁 [COMPUTER_CONTAINER] User workspace prepared: /app/computer-project-workspace/{}",
|
||||
user_id
|
||||
);
|
||||
|
||||
// 2. 调用 DockerManager 启动容器
|
||||
// 注意:不再传递 host_path,挂载由 config.yml 的 mounts 配置管理
|
||||
// 使用 container_identifier 作为 project_id(用于容器名称生成)
|
||||
let mut params_builder = ContainerCreateParams::builder()
|
||||
.project_id(container_identifier) // 用于容器名称生成和查找
|
||||
.user_id(user_id) // user_id 用于容器内配置
|
||||
.host_workspace_path("")
|
||||
.service_type(ServiceType::ComputerAgentRunner);
|
||||
|
||||
// 只有在有资源限制时才设置
|
||||
if let Some(limits) = resource_limits {
|
||||
params_builder = params_builder.resource_limits(limits);
|
||||
}
|
||||
|
||||
// 设置可选的隔离参数
|
||||
if let Some(pid) = pod_id {
|
||||
params_builder = params_builder.pod_id(pid);
|
||||
}
|
||||
if let Some(it) = isolation_type {
|
||||
params_builder = params_builder.isolation_type(it);
|
||||
}
|
||||
if let Some(tid) = tenant_id {
|
||||
params_builder = params_builder.tenant_id(tid);
|
||||
}
|
||||
if let Some(sid) = space_id {
|
||||
params_builder = params_builder.space_id(sid);
|
||||
}
|
||||
|
||||
let params = params_builder.build();
|
||||
|
||||
let container_info = runtime
|
||||
.create_container(params)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("[COMPUTER_CONTAINER] Failed to start container: {}", e);
|
||||
AppError::with_message(
|
||||
ERR_CONTAINER_ERROR,
|
||||
format!("Failed to start container: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
info!(
|
||||
"🚀 [COMPUTER_CONTAINER] User container created successfully: user_id={}, container_id={}, ip={}",
|
||||
user_id, container_info.container_id, container_info.container_ip
|
||||
);
|
||||
|
||||
Ok(container_info)
|
||||
}
|
||||
|
||||
/// 获取用户工作区路径
|
||||
///
|
||||
/// 路径格式: `/app/computer-project-workspace/{user_id}`
|
||||
///
|
||||
/// 注意:project_id 作为子目录由容器内的 agent 自己管理
|
||||
pub async fn get_user_workspace(user_id: &str) -> Result<PathBuf, AppError> {
|
||||
Ok(PathBuf::from(user_dir(user_id)))
|
||||
}
|
||||
|
||||
/// 创建用户工作区目录
|
||||
///
|
||||
/// 创建 `/app/computer-project-workspace/{user_id}` 目录
|
||||
pub async fn create_user_workspace(user_id: &str) -> Result<PathBuf, AppError> {
|
||||
let workspace_root = PathBuf::from(COMPUTER_WORKSPACE_ROOT);
|
||||
|
||||
// 确保根目录存在
|
||||
tokio::fs::create_dir_all(&workspace_root)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
"[COMPUTER_CONTAINER] Failed to create workspace directory: {:?}",
|
||||
e
|
||||
);
|
||||
AppError::with_message(
|
||||
ERR_WORKSPACE_ERROR,
|
||||
format!("Failed to create workspace directory: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
// 创建用户目录
|
||||
let user_workspace = PathBuf::from(user_dir(user_id));
|
||||
tokio::fs::create_dir_all(&user_workspace)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
"[COMPUTER_CONTAINER] Failed to create user directory: {:?}",
|
||||
e
|
||||
);
|
||||
AppError::with_message(
|
||||
ERR_WORKSPACE_ERROR,
|
||||
format!("Failed to create user directory: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
debug!(
|
||||
"📁 [COMPUTER_CONTAINER] User workspace created successfully: {:?}",
|
||||
user_workspace
|
||||
);
|
||||
|
||||
Ok(user_workspace)
|
||||
}
|
||||
|
||||
/// 获取容器信息
|
||||
///
|
||||
/// 通过 user_id 查询容器是否存在
|
||||
pub async fn get_container_info(user_id: &str) -> Result<Option<ContainerBasicInfo>, AppError> {
|
||||
debug!(
|
||||
"[COMPUTER_CONTAINER] get container: user_id={}",
|
||||
user_id
|
||||
);
|
||||
|
||||
let runtime = docker_manager::runtime::RuntimeManager::get()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("[COMPUTER_CONTAINER] Failed to get runtime: {}", e);
|
||||
AppError::with_message(
|
||||
ERR_CONTAINER_ERROR,
|
||||
format!("Failed to get runtime: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
runtime
|
||||
.get_container_info_by_identifier(user_id, &ServiceType::ComputerAgentRunner)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("[COMPUTER_CONTAINER] Failed to query container info: {}", e);
|
||||
AppError::with_message(
|
||||
ERR_CONTAINER_ERROR,
|
||||
format!("Failed to query container info: {}", e),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 单元测试
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_user_workspace_path() {
|
||||
// 测试路径格式
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
let path = ComputerContainerManager::get_user_workspace("user_123")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
path,
|
||||
PathBuf::from("/app/computer-project-workspace/user_123")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_workspace_path_with_special_chars() {
|
||||
// 测试带特殊字符的 user_id
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
let path = ComputerContainerManager::get_user_workspace("user-with-dash_123")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
path,
|
||||
PathBuf::from("/app/computer-project-workspace/user-with-dash_123")
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
303
qiming-rcoder/crates/rcoder/src/service/container_manager.rs
Normal file
303
qiming-rcoder/crates/rcoder/src/service/container_manager.rs
Normal file
@@ -0,0 +1,303 @@
|
||||
//! 容器管理服务
|
||||
//!
|
||||
//! 提供通用的容器创建、管理和复用逻辑
|
||||
//! 供各个 handler 模块使用
|
||||
|
||||
use crate::AppError;
|
||||
use anyhow::Result;
|
||||
use container_runtime_api::{ContainerCreateParams, ContainerRuntime};
|
||||
use docker_manager::ContainerBasicInfo;
|
||||
use shared_types::error_codes::{ERR_CONTAINER_ERROR, ERR_WORKSPACE_ERROR};
|
||||
use std::sync::Arc;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
/// 通用容器管理服务
|
||||
pub struct ContainerManager;
|
||||
|
||||
impl ContainerManager {
|
||||
/// 根据请求获取或创建容器
|
||||
///
|
||||
/// # 参数
|
||||
/// - `project_id`: 项目 ID
|
||||
/// - `service_type`: 服务类型
|
||||
/// - `request_resource_limits`: 资源限制
|
||||
/// - `pod_id`: 容器唯一标识(可选,有值时优先使用)
|
||||
/// - `isolation_type`: 隔离类型(可选,tenant/space 时路径不同)
|
||||
/// - `tenant_id`: 租户 ID(可选)
|
||||
/// - `space_id`: 空间 ID(可选)
|
||||
/// - `container_work_path`: 容器内工作路径
|
||||
pub async fn get_or_create_container(
|
||||
project_id: &str,
|
||||
service_type: &shared_types::ServiceType,
|
||||
request_resource_limits: Option<shared_types::ServiceResourceLimits>,
|
||||
pod_id: Option<&str>,
|
||||
isolation_type: Option<&str>,
|
||||
tenant_id: Option<&str>,
|
||||
space_id: Option<&str>,
|
||||
container_work_path: &str,
|
||||
) -> Result<ContainerBasicInfo, AppError> {
|
||||
info!(
|
||||
"🔍 [CONTAINER_MGR] Starting container processing: project_id={}, service_type={:?}, pod_id={:?}, isolation_type={:?}",
|
||||
project_id, service_type, pod_id, isolation_type
|
||||
);
|
||||
|
||||
// 确定容器标识符:pod_id 有值时使用 pod_id,否则使用 project_id
|
||||
let container_identifier = pod_id.unwrap_or(project_id);
|
||||
|
||||
// 检查或创建容器
|
||||
let container_info = ensure_container_exists(
|
||||
project_id,
|
||||
container_identifier,
|
||||
service_type,
|
||||
request_resource_limits,
|
||||
pod_id,
|
||||
isolation_type,
|
||||
tenant_id,
|
||||
space_id,
|
||||
container_work_path,
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!(
|
||||
"✅ [CONTAINER_MGR] Container ready: project_id={}, container_id={}, container_identifier={}",
|
||||
project_id, container_info.container_id, container_identifier
|
||||
);
|
||||
|
||||
Ok(container_info)
|
||||
}
|
||||
|
||||
/// 获取容器信息
|
||||
pub async fn get_container_info(
|
||||
project_id: &str,
|
||||
) -> Result<Option<ContainerBasicInfo>, AppError> {
|
||||
debug!(
|
||||
"[CONTAINER_MGR] get container: project_id={}",
|
||||
project_id
|
||||
);
|
||||
|
||||
let runtime = docker_manager::runtime::RuntimeManager::get()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("[CONTAINER_MGR] Failed to get global runtime: {}", e);
|
||||
AppError::with_message(
|
||||
ERR_CONTAINER_ERROR,
|
||||
format!("Failed to get global runtime: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
runtime
|
||||
.get_container_info(project_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("[CONTAINER_MGR] Failed to query container info: {}", e);
|
||||
AppError::with_message(
|
||||
ERR_CONTAINER_ERROR,
|
||||
format!("Failed to query container info: {}", e),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据 project_id 检查对应容器是否存在,不存在就动态创建容器
|
||||
async fn ensure_container_exists(
|
||||
project_id: &str,
|
||||
container_identifier: &str,
|
||||
service_type: &shared_types::ServiceType,
|
||||
request_resource_limits: Option<shared_types::ServiceResourceLimits>,
|
||||
pod_id: Option<&str>,
|
||||
isolation_type: Option<&str>,
|
||||
tenant_id: Option<&str>,
|
||||
space_id: Option<&str>,
|
||||
container_work_path: &str,
|
||||
) -> Result<ContainerBasicInfo, AppError> {
|
||||
let runtime = docker_manager::runtime::RuntimeManager::get()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("[CONTAINER_MGR] Failed to get global runtime: {}", e);
|
||||
AppError::with_message(
|
||||
ERR_CONTAINER_ERROR,
|
||||
format!("Failed to get global runtime: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
// 1. 尝试获取现有容器(使用 container_identifier 查找)
|
||||
if let Ok(Some(info)) = runtime.get_container_info(container_identifier).await {
|
||||
info!(
|
||||
"[CONTAINER_MGR] container already exists: container_identifier={}, container_id={}",
|
||||
container_identifier, info.container_id
|
||||
);
|
||||
return Ok(info);
|
||||
}
|
||||
|
||||
// 2. 创建新容器
|
||||
info!(
|
||||
"🏗️ [CONTAINER_MGR] Container does not exist, creating new container: container_identifier={}, service_type={:?}, pod_id={:?}",
|
||||
container_identifier, service_type, pod_id
|
||||
);
|
||||
|
||||
create_container_for_request(
|
||||
project_id,
|
||||
container_identifier,
|
||||
service_type,
|
||||
&runtime,
|
||||
request_resource_limits,
|
||||
pod_id,
|
||||
isolation_type,
|
||||
tenant_id,
|
||||
space_id,
|
||||
container_work_path,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// 为请求创建容器
|
||||
async fn create_container_for_request(
|
||||
project_id: &str,
|
||||
container_identifier: &str,
|
||||
service_type: &shared_types::ServiceType,
|
||||
runtime: &Arc<dyn ContainerRuntime>,
|
||||
request_resource_limits: Option<shared_types::ServiceResourceLimits>,
|
||||
pod_id: Option<&str>,
|
||||
isolation_type: Option<&str>,
|
||||
tenant_id: Option<&str>,
|
||||
space_id: Option<&str>,
|
||||
container_work_path: &str,
|
||||
) -> Result<ContainerBasicInfo, AppError> {
|
||||
// 1. 准备工作目录(在 rcoder 容器内创建)
|
||||
// 注意:container_work_path 已经是完整的路径
|
||||
create_workspace_dir(container_work_path).await.map_err(|e| {
|
||||
AppError::with_message(
|
||||
ERR_WORKSPACE_ERROR,
|
||||
format!("Failed to create workspace directory: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
info!(
|
||||
"📁 [CONTAINER_MGR] Project workspace prepared: {}",
|
||||
container_work_path
|
||||
);
|
||||
|
||||
// 2. 调用容器运行时启动容器
|
||||
// 注意:project_id 始终使用实际的 project_id(不被 pod_id 覆盖)
|
||||
// 容器命名由 runtime 层通过 pod_id 处理
|
||||
let mut params_builder = ContainerCreateParams::builder()
|
||||
.project_id(project_id)
|
||||
.host_workspace_path("") // 空字符串,表示不使用硬编码挂载
|
||||
.service_type(service_type.clone());
|
||||
|
||||
// 只有在有资源限制时才设置
|
||||
if let Some(limits) = request_resource_limits {
|
||||
params_builder = params_builder.resource_limits(limits);
|
||||
}
|
||||
|
||||
// 设置可选的隔离参数(如果提供的话)
|
||||
if let Some(pid) = pod_id {
|
||||
params_builder = params_builder.pod_id(pid);
|
||||
}
|
||||
if let Some(it) = isolation_type {
|
||||
params_builder = params_builder.isolation_type(it);
|
||||
}
|
||||
if let Some(tid) = tenant_id {
|
||||
params_builder = params_builder.tenant_id(tid);
|
||||
}
|
||||
if let Some(sid) = space_id {
|
||||
params_builder = params_builder.space_id(sid);
|
||||
}
|
||||
|
||||
let params = params_builder.build();
|
||||
|
||||
let container_info = runtime
|
||||
.create_container(params)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("[CONTAINER_MGR] Failed to start container: {}", e);
|
||||
AppError::with_message(
|
||||
ERR_CONTAINER_ERROR,
|
||||
format!("Failed to start container: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
info!(
|
||||
"🚀 [CONTAINER_MGR] Container created successfully: container_identifier={}, container_id={}, ip={}",
|
||||
container_identifier, container_info.container_id, container_info.container_ip
|
||||
);
|
||||
|
||||
Ok(container_info)
|
||||
}
|
||||
|
||||
/// 创建工作目录(使用完整路径)
|
||||
async fn create_workspace_dir(full_path: &str) -> Result<std::path::PathBuf, AppError> {
|
||||
let path = std::path::PathBuf::from(full_path);
|
||||
|
||||
// 确保父目录存在
|
||||
if let Some(parent) = path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await.map_err(|e| {
|
||||
error!(
|
||||
"[CONTAINER_MGR] Failed to create parent directory: {:?}",
|
||||
e
|
||||
);
|
||||
AppError::with_message(
|
||||
ERR_WORKSPACE_ERROR,
|
||||
format!("Failed to create parent directory: {}", e),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
// 创建完整路径目录
|
||||
tokio::fs::create_dir_all(&path).await.map_err(|e| {
|
||||
error!(
|
||||
"[CONTAINER_MGR] Failed to create workspace directory: {:?}",
|
||||
e
|
||||
);
|
||||
AppError::with_message(
|
||||
ERR_WORKSPACE_ERROR,
|
||||
format!("Failed to create workspace directory: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// 生成新的项目ID(UUID去除连字符)
|
||||
pub fn generate_project_id() -> String {
|
||||
uuid::Uuid::new_v4().to_string().replace('-', "")
|
||||
}
|
||||
|
||||
/// 获取 project_id 的 workspace_path
|
||||
pub async fn get_project_workspace(project_id: &str) -> Result<std::path::PathBuf, AppError> {
|
||||
let workspace_dir = std::path::PathBuf::from("/app/project_workspace");
|
||||
let project_dir = workspace_dir.join(project_id);
|
||||
Ok(project_dir)
|
||||
}
|
||||
|
||||
/// 创建项目工作目录
|
||||
pub async fn create_project_workspace(project_id: &str) -> Result<std::path::PathBuf, AppError> {
|
||||
let workspace_dir = std::path::PathBuf::from("/app/project_workspace");
|
||||
|
||||
tokio::fs::create_dir_all(&workspace_dir)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
"[CONTAINER_MGR] Failed to create workspace directory: {:?}",
|
||||
e
|
||||
);
|
||||
AppError::with_message(
|
||||
ERR_WORKSPACE_ERROR,
|
||||
format!("Failed to create workspace directory: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
let project_dir = workspace_dir.join(project_id);
|
||||
tokio::fs::create_dir_all(&project_dir).await.map_err(|e| {
|
||||
error!(
|
||||
"[CONTAINER_MGR] Failed to create project directory: {:?}",
|
||||
e
|
||||
);
|
||||
AppError::with_message(
|
||||
ERR_WORKSPACE_ERROR,
|
||||
format!("Failed to create project directory: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(project_dir)
|
||||
}
|
||||
@@ -0,0 +1,638 @@
|
||||
//! 容器状态检查器
|
||||
//!
|
||||
//! 定期查询 Agent Runner 的容器状态,如果容器有活跃任务则更新活动时间。
|
||||
//! 这样可以防止正在执行长时间任务的容器被清理任务误判为闲置而销毁。
|
||||
//!
|
||||
//! ## 优化特性
|
||||
//!
|
||||
//! 1. **Docker 主动查询**:gRPC 失败时主动查询 Docker 容器是否存在
|
||||
//! 2. **失败计数器**:为每个容器维护健康状态,记录连续失败次数
|
||||
//! 3. **智能跳过**:连续失败超过阈值后暂时跳过检查
|
||||
//! 4. **自动清理**:容器不存在时立即清理 gRPC 连接池和健康状态
|
||||
//! 5. **分级日志**:根据失败次数输出不同级别的日志
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use dashmap::DashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::time;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
/// 格式化日期时间为标准格式(如:2026-01-12 15:04:30)
|
||||
fn format_datetime(dt: DateTime<Utc>) -> String {
|
||||
dt.format("%Y-%m-%d %H:%M:%S").to_string()
|
||||
}
|
||||
|
||||
/// 格式化相对时间(如:5分钟前)
|
||||
fn format_relative_time(dt: DateTime<Utc>) -> String {
|
||||
let now = Utc::now();
|
||||
let duration = now.signed_duration_since(dt);
|
||||
|
||||
if duration.num_seconds() < 60 {
|
||||
format!("{}s ago", duration.num_seconds())
|
||||
} else if duration.num_minutes() < 60 {
|
||||
format!("{}m ago", duration.num_minutes())
|
||||
} else if duration.num_hours() < 24 {
|
||||
format!("{}h ago", duration.num_hours())
|
||||
} else {
|
||||
format!("{}d ago", duration.num_days())
|
||||
}
|
||||
}
|
||||
|
||||
use crate::grpc::GrpcChannelPool;
|
||||
use crate::router::AppState;
|
||||
use shared_types::grpc::GetContainerStatusRequest;
|
||||
|
||||
/// 容器健康状态
|
||||
#[derive(Debug, Clone)]
|
||||
struct ContainerHealthState {
|
||||
/// 连续失败次数
|
||||
consecutive_failures: u32,
|
||||
/// 首次失败时间
|
||||
first_failure_time: Option<DateTime<Utc>>,
|
||||
/// 最后检查时间
|
||||
last_check_time: DateTime<Utc>,
|
||||
/// 最后成功时间
|
||||
last_success_time: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl ContainerHealthState {
|
||||
/// 创建新的健康状态
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
consecutive_failures: 0,
|
||||
first_failure_time: None,
|
||||
last_check_time: Utc::now(),
|
||||
last_success_time: Some(Utc::now()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建失败状态
|
||||
fn new_failed() -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
consecutive_failures: 1,
|
||||
first_failure_time: Some(now),
|
||||
last_check_time: now,
|
||||
last_success_time: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 容器状态检查配置
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContainerStatusCheckerConfig {
|
||||
/// 检查间隔(默认 30 秒)
|
||||
pub check_interval: Duration,
|
||||
/// 查询超时(默认 5 秒)
|
||||
pub query_timeout: Duration,
|
||||
/// 连续失败阈值(默认 3 次)
|
||||
pub failure_threshold: u32,
|
||||
/// 失败容器跳过时间(默认 5 分钟)
|
||||
pub skip_duration: Duration,
|
||||
/// 健康状态重置周期(默认 30 分钟)
|
||||
pub health_reset_interval: Duration,
|
||||
}
|
||||
|
||||
impl Default for ContainerStatusCheckerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
check_interval: Duration::from_secs(30),
|
||||
query_timeout: Duration::from_secs(5),
|
||||
failure_threshold: 3,
|
||||
skip_duration: Duration::from_secs(5 * 60),
|
||||
health_reset_interval: Duration::from_secs(30 * 60),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 容器状态检查器
|
||||
struct ContainerStatusChecker {
|
||||
config: ContainerStatusCheckerConfig,
|
||||
state: Arc<AppState>,
|
||||
/// 容器健康状态映射 (lookup_key -> health_state)
|
||||
health_states: Arc<DashMap<String, ContainerHealthState>>,
|
||||
}
|
||||
|
||||
impl ContainerStatusChecker {
|
||||
/// 创建新的状态检查器
|
||||
fn new(config: ContainerStatusCheckerConfig, state: Arc<AppState>) -> Self {
|
||||
Self {
|
||||
config,
|
||||
state,
|
||||
health_states: Arc::new(DashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查所有容器的状态
|
||||
async fn check_all_containers(&self) -> anyhow::Result<()> {
|
||||
// 收集所有需要检查的容器(创建快照,使用 DuckDB 存储)
|
||||
let containers: Vec<(String, Arc<shared_types::ProjectAndContainerInfo>)> =
|
||||
self.state.projects.iter().collect();
|
||||
|
||||
if containers.is_empty() {
|
||||
debug!("📭 [STATUS_CHECKER] No containers to check");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!(
|
||||
"🔍 [STATUS_CHECKER] Starting to check {} containers",
|
||||
containers.len()
|
||||
);
|
||||
|
||||
let total_count = containers.len();
|
||||
let mut checked = 0;
|
||||
let mut skipped = 0;
|
||||
let mut updated = 0;
|
||||
let mut failed = 0;
|
||||
|
||||
for (_project_id, container_info) in containers {
|
||||
// 使用 container_key() 获取正确的容器标识符
|
||||
// - RCoder 模式:返回 project_id
|
||||
// - ComputerAgentRunner 模式:返回 user_id(用于匹配容器名称)
|
||||
let lookup_key = container_info.container_key().to_string();
|
||||
|
||||
// 🆕 检查所有类型的容器(RCoder 和 ComputerAgentRunner)
|
||||
// 两种模式都可能执行长时间任务,需要定期检查状态防止被误杀
|
||||
|
||||
// 检查是否应该跳过
|
||||
if self.should_skip_check(&lookup_key) {
|
||||
skipped += 1;
|
||||
debug!(
|
||||
"⏭️ [STATUS_CHECKER] Skipping check (recently failed): {}",
|
||||
lookup_key
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
checked += 1;
|
||||
|
||||
// 执行单个容器检查
|
||||
match self
|
||||
.check_single_container(&lookup_key, &container_info)
|
||||
.await
|
||||
{
|
||||
Ok(true) => updated += 1,
|
||||
Ok(false) => {} // 容器空闲或未更新
|
||||
Err(_) => failed += 1,
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"📊 [STATUS_CHECKER] Check completed: total={}, checked={}, skipped={}, updated={}, failed={}",
|
||||
total_count, checked, skipped, updated, failed
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 检查单个容器
|
||||
///
|
||||
/// 返回是否更新了活动时间
|
||||
async fn check_single_container(
|
||||
&self,
|
||||
lookup_key: &str,
|
||||
container_info: &Arc<shared_types::ProjectAndContainerInfo>,
|
||||
) -> anyhow::Result<bool> {
|
||||
// 获取容器信息
|
||||
let container = match container_info.container() {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
debug!("⚠️ [STATUS_CHECKER] Container info not found: {}", lookup_key);
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取最后激活时间用于日志显示
|
||||
let last_activity = container_info.last_activity();
|
||||
let last_activity_str = format_datetime(last_activity);
|
||||
let relative_time_str = format_relative_time(last_activity);
|
||||
|
||||
// 构建 gRPC 地址
|
||||
let grpc_addr = format!(
|
||||
"{}:{}",
|
||||
container.container_ip,
|
||||
shared_types::GRPC_DEFAULT_PORT
|
||||
);
|
||||
|
||||
// 提取 user_id(lookup_key 可能是 user_id 或 project_id)
|
||||
let user_id = container_info
|
||||
.user_id()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| lookup_key.to_string());
|
||||
|
||||
let project_id = container_info.project_id().to_string();
|
||||
|
||||
// 查询容器状态
|
||||
match query_container_status(
|
||||
&grpc_addr,
|
||||
&user_id,
|
||||
&project_id,
|
||||
&self.state.grpc_pool,
|
||||
&self.config,
|
||||
last_activity_str,
|
||||
relative_time_str,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(is_active) => {
|
||||
// ✅ 成功:重置失败计数器
|
||||
self.record_success(lookup_key);
|
||||
|
||||
if is_active {
|
||||
// 容器有活跃任务,更新活动时间和状态
|
||||
// 注意:使用 project_id 更新 DuckDB,而不是 lookup_key
|
||||
if let Err(e) = update_project_activity(&project_id, &self.state).await {
|
||||
warn!(
|
||||
"⚠️ [STATUS_CHECKER] Failed to update activity time: project_id={}, {}",
|
||||
project_id, e
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
// 🆕 同步更新 agent 状态为 Active
|
||||
if let Err(e) = self.state.projects.update_agent_status(
|
||||
&project_id,
|
||||
1, // Active
|
||||
"active",
|
||||
) {
|
||||
warn!(
|
||||
"⚠️ [STATUS_CHECKER] Failed to update agent status to Active: project_id={}, error={}",
|
||||
project_id, e
|
||||
);
|
||||
}
|
||||
debug!(
|
||||
"✅ [STATUS_CHECKER] Container is active, updated activity time and status: container_key={}, project_id={}",
|
||||
lookup_key, project_id
|
||||
);
|
||||
Ok(true)
|
||||
} else {
|
||||
// 🆕 同步更新 agent 状态为 Idle
|
||||
if let Err(e) = self.state.projects.update_agent_status(
|
||||
&project_id,
|
||||
0, // Idle
|
||||
"idle",
|
||||
) {
|
||||
warn!(
|
||||
"⚠️ [STATUS_CHECKER] Failed to update agent status to Idle: project_id={}, error={}",
|
||||
project_id, e
|
||||
);
|
||||
}
|
||||
debug!(
|
||||
"📭 [STATUS_CHECKER] Container is idle, updated status to Idle: container_key={}, project_id={}",
|
||||
lookup_key, project_id
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// ❌ 失败:主动查询 Docker 容器是否存在(关键优化)
|
||||
let container_exists = self
|
||||
.check_container_exists(container_info, &grpc_addr)
|
||||
.await;
|
||||
|
||||
if !container_exists {
|
||||
// 容器不存在,直接清理所有状态
|
||||
info!(
|
||||
"🗑️ [STATUS_CHECKER] Container has been destroyed, cleaning up health state: {}",
|
||||
lookup_key
|
||||
);
|
||||
self.health_states.remove(lookup_key);
|
||||
self.state.grpc_pool.remove(&grpc_addr);
|
||||
// 注意:不移除 DuckDB 存储中的项目记录,由清理任务统一处理
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// 容器存在但连接失败,记录失败(可能是网络问题)
|
||||
self.record_failure(lookup_key, &grpc_addr, &e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查 runtime 容器是否存在
|
||||
async fn check_container_exists(
|
||||
&self,
|
||||
container_info: &Arc<shared_types::ProjectAndContainerInfo>,
|
||||
grpc_addr: &str,
|
||||
) -> bool {
|
||||
match docker_manager::runtime::RuntimeManager::get().await {
|
||||
Ok(runtime) => {
|
||||
let service_type = container_info
|
||||
.service_type()
|
||||
.unwrap_or(shared_types::ServiceType::ComputerAgentRunner);
|
||||
|
||||
// 根据 service_type 使用不同的查找方法
|
||||
// - RCoder 模式:使用 project_id 查找
|
||||
// - ComputerAgentRunner 模式:使用 user_id 查找
|
||||
let exists = match service_type {
|
||||
shared_types::ServiceType::ComputerAgentRunner => {
|
||||
// ComputerAgentRunner 模式:使用 user_id 查找容器
|
||||
if let Some(user_id) = container_info.user_id() {
|
||||
match runtime.find_container(user_id, &service_type).await {
|
||||
Ok(Some(_)) => true,
|
||||
Ok(None) => false,
|
||||
Err(e) => {
|
||||
debug!("⚠️ [STATUS_CHECKER] Failed to query container: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("⚠️ [STATUS_CHECKER] ComputerAgentRunner missing user_id");
|
||||
false
|
||||
}
|
||||
}
|
||||
shared_types::ServiceType::RCoder => {
|
||||
// RCoder 模式:使用 project_id 查找容器
|
||||
match runtime
|
||||
.find_container(container_info.project_id(), &service_type)
|
||||
.await
|
||||
{
|
||||
Ok(Some(_)) => true,
|
||||
Ok(None) => false,
|
||||
Err(e) => {
|
||||
debug!("⚠️ [STATUS_CHECKER] Failed to query container: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if exists {
|
||||
debug!(
|
||||
"🔍 [STATUS_CHECKER] Runtime container exists, likely network issue: {} (service_type={:?})",
|
||||
grpc_addr, service_type
|
||||
);
|
||||
} else {
|
||||
info!(
|
||||
"🔍 [STATUS_CHECKER] Runtime container does not exist (already destroyed): {} (service_type={:?})",
|
||||
grpc_addr, service_type
|
||||
);
|
||||
}
|
||||
|
||||
exists
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("[STATUS_CHECKER] get runtime failed: {}", e);
|
||||
// 无法确定容器状态,保守地认为容器存在
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 判断是否应该跳过检查
|
||||
fn should_skip_check(&self, lookup_key: &str) -> bool {
|
||||
if let Some(health) = self.health_states.get(lookup_key) {
|
||||
let now = Utc::now();
|
||||
|
||||
// 如果连续失败次数超过阈值
|
||||
if health.consecutive_failures >= self.config.failure_threshold {
|
||||
// 检查是否还在跳过期内
|
||||
if let Some(first_failure) = health.first_failure_time {
|
||||
let elapsed = now.signed_duration_since(first_failure);
|
||||
if let Ok(skip_duration) = chrono::Duration::from_std(self.config.skip_duration)
|
||||
{
|
||||
if elapsed < skip_duration {
|
||||
return true; // 仍在跳过期内
|
||||
}
|
||||
}
|
||||
}
|
||||
// 跳过期已过,允许重新检查(但不重置失败计数器)
|
||||
}
|
||||
}
|
||||
|
||||
false // 默认不跳过
|
||||
}
|
||||
|
||||
/// 记录成功并重置失败计数器
|
||||
fn record_success(&self, lookup_key: &str) {
|
||||
let now = Utc::now();
|
||||
|
||||
use dashmap::mapref::entry::Entry;
|
||||
|
||||
match self.health_states.entry(lookup_key.to_string()) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
// 使用 get_mut 直接修改,避免克隆
|
||||
let was_failing = entry.get().consecutive_failures > 0;
|
||||
let health = entry.get_mut();
|
||||
health.consecutive_failures = 0;
|
||||
health.first_failure_time = None;
|
||||
health.last_check_time = now;
|
||||
health.last_success_time = Some(now);
|
||||
// 无需 insert,修改已生效
|
||||
|
||||
if was_failing {
|
||||
info!("[STATUS_CHECKER] Container recovered: {}", lookup_key);
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(ContainerHealthState::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 记录失败并清理连接
|
||||
fn record_failure(&self, lookup_key: &str, grpc_addr: &str, error: &anyhow::Error) {
|
||||
let now = Utc::now();
|
||||
|
||||
use dashmap::mapref::entry::Entry;
|
||||
|
||||
let consecutive_failures = match self.health_states.entry(lookup_key.to_string()) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
// 使用 get_mut 直接修改,避免克隆
|
||||
let health = entry.get_mut();
|
||||
health.consecutive_failures += 1;
|
||||
health.last_check_time = now;
|
||||
if health.first_failure_time.is_none() {
|
||||
health.first_failure_time = Some(now);
|
||||
}
|
||||
let failures = health.consecutive_failures;
|
||||
// 无需 insert,修改已生效
|
||||
failures
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(ContainerHealthState::new_failed());
|
||||
1
|
||||
}
|
||||
};
|
||||
|
||||
// 🔌 第1次失败或达到阈值时,清理 gRPC 连接池
|
||||
if consecutive_failures == 1 || consecutive_failures == self.config.failure_threshold {
|
||||
self.state.grpc_pool.remove(grpc_addr);
|
||||
info!(
|
||||
"🔌 [STATUS_CHECKER] Already cleanup connection: {}",
|
||||
grpc_addr
|
||||
);
|
||||
}
|
||||
|
||||
// 📊 分级日志输出
|
||||
match consecutive_failures {
|
||||
1 => {
|
||||
// 首次失败:INFO 级别
|
||||
info!(
|
||||
"❌ [STATUS_CHECKER] Container first query failed: {} - {}",
|
||||
lookup_key, error
|
||||
);
|
||||
}
|
||||
n if n < self.config.failure_threshold => {
|
||||
// 持续失败但未达到阈值:DEBUG 级别
|
||||
debug!(
|
||||
"❌ [STATUS_CHECKER] Container continuous failure ({}/{}): {}",
|
||||
n, self.config.failure_threshold, lookup_key
|
||||
);
|
||||
}
|
||||
n if n == self.config.failure_threshold => {
|
||||
// 达到阈值:WARN 级别
|
||||
warn!(
|
||||
"⚠️ [STATUS_CHECKER] Container continuous failures reached threshold, will skip check temporarily: {} (failures: {})",
|
||||
lookup_key, n
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
// 超过阈值后的偶发检查:DEBUG 级别
|
||||
debug!("⏭️ [STATUS_CHECKER] Skipping check for: {}", lookup_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 清理过期的健康状态
|
||||
fn cleanup_stale_health_states(&self) {
|
||||
let now = Utc::now();
|
||||
let retention_duration = match chrono::Duration::from_std(self.config.health_reset_interval)
|
||||
{
|
||||
Ok(d) => d,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let mut removed_count = 0;
|
||||
|
||||
// 收集需要移除的 key
|
||||
let keys_to_remove: Vec<String> = self
|
||||
.health_states
|
||||
.iter()
|
||||
.filter_map(|entry| {
|
||||
let lookup_key = entry.key();
|
||||
let health = entry.value();
|
||||
|
||||
// 移除条件:
|
||||
// 1. 容器已不在 DuckDB 存储中
|
||||
// 2. 最后检查时间超过健康重置周期
|
||||
let not_in_storage = !self.state.contains_project(lookup_key);
|
||||
let elapsed = now.signed_duration_since(health.last_check_time);
|
||||
let is_stale = elapsed > retention_duration;
|
||||
|
||||
if not_in_storage || is_stale {
|
||||
Some(lookup_key.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 批量移除
|
||||
for key in keys_to_remove {
|
||||
if self.health_states.remove(&key).is_some() {
|
||||
removed_count += 1;
|
||||
debug!("🧹 [STATUS_CHECKER] Cleaned up stale health state: {}", key);
|
||||
}
|
||||
}
|
||||
|
||||
if removed_count > 0 {
|
||||
info!(
|
||||
"🧹 [STATUS_CHECKER] Cleaned up stale health states: removed={}",
|
||||
removed_count
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动容器状态检查任务
|
||||
///
|
||||
/// 定期查询所有容器的 Agent Runner 状态,如果容器有活跃任务则更新活动时间
|
||||
pub fn start_container_status_checker(
|
||||
config: ContainerStatusCheckerConfig,
|
||||
state: Arc<AppState>,
|
||||
) -> tokio::task::JoinHandle<()> {
|
||||
info!(
|
||||
"🔍 [STATUS_CHECKER] Starting container status checker: interval={}s, failure_threshold={}, skip_duration={}s",
|
||||
config.check_interval.as_secs(),
|
||||
config.failure_threshold,
|
||||
config.skip_duration.as_secs()
|
||||
);
|
||||
|
||||
let checker = Arc::new(ContainerStatusChecker::new(config.clone(), state));
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut interval = time::interval(config.check_interval);
|
||||
interval.set_missed_tick_behavior(time::MissedTickBehavior::Skip);
|
||||
|
||||
let mut cleanup_counter = 0;
|
||||
let cleanup_interval = 10; // 每 10 次检查清理一次健康状态
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
// 执行容器状态检查
|
||||
if let Err(e) = checker.check_all_containers().await {
|
||||
warn!("[STATUS_CHECKER] containerstatuscheckfailed: {}", e);
|
||||
}
|
||||
|
||||
// 定期清理过期的健康状态
|
||||
cleanup_counter += 1;
|
||||
if cleanup_counter >= cleanup_interval {
|
||||
checker.cleanup_stale_health_states();
|
||||
cleanup_counter = 0;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// 查询容器状态
|
||||
///
|
||||
/// 返回容器是否活跃(有活跃任务)
|
||||
async fn query_container_status(
|
||||
grpc_addr: &str,
|
||||
user_id: &str,
|
||||
project_id: &str,
|
||||
grpc_pool: &Arc<GrpcChannelPool>,
|
||||
config: &ContainerStatusCheckerConfig,
|
||||
last_activity_str: String,
|
||||
relative_time_str: String,
|
||||
) -> anyhow::Result<bool> {
|
||||
// 获取 gRPC 客户端
|
||||
let mut client = grpc_pool.get_client(grpc_addr).await?;
|
||||
|
||||
// 构建请求
|
||||
let request = tonic::Request::new(GetContainerStatusRequest {
|
||||
user_id: user_id.to_string(),
|
||||
project_id: project_id.to_string(),
|
||||
});
|
||||
|
||||
// 发送请求(带超时)
|
||||
let response =
|
||||
tokio::time::timeout(config.query_timeout, client.get_container_status(request)).await??;
|
||||
|
||||
let status_response = response.into_inner();
|
||||
|
||||
debug!(
|
||||
"📊 [STATUS_CHECKER] Container status: user_id={}, is_active={}, active_tasks={}, status={}, last_activity={} ({})",
|
||||
user_id,
|
||||
status_response.is_active,
|
||||
status_response.active_tasks,
|
||||
status_response.status,
|
||||
last_activity_str,
|
||||
relative_time_str
|
||||
);
|
||||
|
||||
// 如果容器有活跃任务,则认为容器活跃
|
||||
Ok(status_response.is_active || status_response.active_tasks > 0)
|
||||
}
|
||||
|
||||
/// 更新项目活动时间(并同步更新关联容器的活动时间)
|
||||
///
|
||||
/// 使用 DuckDB 存储更新 projects 表的 last_activity 字段
|
||||
async fn update_project_activity(project_id: &str, state: &Arc<AppState>) -> anyhow::Result<()> {
|
||||
// 使用 ProjectAdapter 的 update_activity 方法
|
||||
// 该方法会同时更新 project 和关联 container 的 last_activity
|
||||
state.update_activity(project_id);
|
||||
Ok(())
|
||||
}
|
||||
91
qiming-rcoder/crates/rcoder/src/service/container_sync.rs
Normal file
91
qiming-rcoder/crates/rcoder/src/service/container_sync.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
//! 容器状态同步任务
|
||||
//!
|
||||
//! 定期从运行时同步容器状态。
|
||||
//! 在 K8s 模式下通过 Runtime API 获取 Pod 状态;在 Docker 模式下同样通过 Runtime 抽象层访问。
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::grpc::GrpcChannelPool;
|
||||
|
||||
/// 容器状态同步配置
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContainerSyncConfig {
|
||||
/// 同步间隔
|
||||
pub sync_interval: Duration,
|
||||
}
|
||||
|
||||
impl Default for ContainerSyncConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sync_interval: Duration::from_secs(60), // 默认 60 秒
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动容器状态同步任务
|
||||
///
|
||||
/// 定期调用 `ContainerRuntime::sync_states()` 方法,
|
||||
/// 检查缓存中的容器是否仍然存在于运行时(Docker/K8s)中,
|
||||
/// 如果不存在则从缓存中移除。
|
||||
///
|
||||
/// 同时清理已移除容器的关联资源:
|
||||
/// - gRPC 连接池中的旧连接
|
||||
pub fn start_container_sync_task(
|
||||
config: ContainerSyncConfig,
|
||||
grpc_pool: Arc<GrpcChannelPool>,
|
||||
) -> tokio::task::JoinHandle<()> {
|
||||
info!(
|
||||
"🔄 [CONTAINER_SYNC] Starting container state sync task: interval={}s",
|
||||
config.sync_interval.as_secs()
|
||||
);
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
let mut interval = tokio::time::interval(config.sync_interval);
|
||||
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
// 获取全局 Runtime
|
||||
let runtime = match docker_manager::runtime::RuntimeManager::get().await {
|
||||
Ok(rt) => rt,
|
||||
Err(e) => {
|
||||
warn!("[CONTAINER_SYNC] Failed to get runtime: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// 同步缓存状态 - 清理失效的容器记录
|
||||
debug!("[CONTAINER_SYNC] Syncing container states...");
|
||||
match runtime.sync_states().await {
|
||||
Ok((checked, removed)) => {
|
||||
if !removed.is_empty() {
|
||||
info!(
|
||||
"[CONTAINER_SYNC] Sync completed: checked={}, removed_stale={}",
|
||||
checked,
|
||||
removed.len()
|
||||
);
|
||||
|
||||
// 清理关联资源
|
||||
for container in removed {
|
||||
// 清理 gRPC 连接池
|
||||
if !container.container_ip.is_empty() {
|
||||
let grpc_addr = format!(
|
||||
"{}:{}",
|
||||
container.container_ip,
|
||||
shared_types::GRPC_DEFAULT_PORT
|
||||
);
|
||||
grpc_pool.remove(&grpc_addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("[CONTAINER_SYNC] sync failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
21
qiming-rcoder/crates/rcoder/src/service/mod.rs
Normal file
21
qiming-rcoder/crates/rcoder/src/service/mod.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
//! 服务模块
|
||||
//!
|
||||
//! 提供容器管理功能
|
||||
//!
|
||||
//! ## 模块说明
|
||||
//! - `container_manager`: RCoder 模式的容器管理(project_id -> 容器)
|
||||
//! - `computer_container_manager`: ComputerAgentRunner 模式的容器管理(user_id -> 容器)
|
||||
//! - `container_status_checker`: 容器状态检查器(定期查询容器状态,防止误杀)
|
||||
//! - `container_sync`: 容器状态同步任务(定期同步 Docker 容器状态到内存缓存)
|
||||
//! - `vnc_sync`: VNC 后端同步任务(定期同步容器 IP 到 Pingora 的 vnc_backends)
|
||||
|
||||
pub mod computer_container_manager;
|
||||
pub mod container_manager;
|
||||
pub mod container_status_checker;
|
||||
pub mod container_sync;
|
||||
pub mod vnc_sync;
|
||||
|
||||
pub use computer_container_manager::ComputerContainerManager;
|
||||
pub use container_status_checker::{ContainerStatusCheckerConfig, start_container_status_checker};
|
||||
pub use container_sync::{ContainerSyncConfig, start_container_sync_task};
|
||||
pub use vnc_sync::{VncSyncConfig, start_vnc_sync_task};
|
||||
237
qiming-rcoder/crates/rcoder/src/service/vnc_sync.rs
Normal file
237
qiming-rcoder/crates/rcoder/src/service/vnc_sync.rs
Normal file
@@ -0,0 +1,237 @@
|
||||
//! VNC 后端同步任务
|
||||
//!
|
||||
//! 定期从 Runtime 同步容器的 IP 到 Pingora 的 vnc_backends 映射。
|
||||
//! 使用容器命名规则解析业务标识符:
|
||||
//! - RCoder: `rcoder-agent-{project_id}`
|
||||
//! - ComputerAgentRunner: `computer-agent-runner-{user_id}`
|
||||
//! 解决服务重启后 VNC 映射丢失的问题,并支持容器重启后自动更新 IP。
|
||||
|
||||
use rcoder_proxy::PingoraProxyService;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
/// VNC 后端同步配置
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VncSyncConfig {
|
||||
/// 同步间隔
|
||||
pub sync_interval: Duration,
|
||||
}
|
||||
|
||||
impl Default for VncSyncConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sync_interval: Duration::from_secs(5), // 默认 5 秒
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动 VNC 后端同步任务
|
||||
///
|
||||
/// 定期扫描所有容器,使用 `container_key()` 获取正确的业务标识符,
|
||||
/// 将 user_id/project_id -> container_ip 映射同步到 Pingora 的 vnc_backends。
|
||||
///
|
||||
/// 这解决了两个问题:
|
||||
/// 1. 服务重启后 vnc_backends 丢失
|
||||
/// 2. 容器重启后 IP 变化
|
||||
pub fn start_vnc_sync_task(
|
||||
pingora_service: Arc<PingoraProxyService>,
|
||||
config: VncSyncConfig,
|
||||
rcoder_prefix: String,
|
||||
computer_prefix: String,
|
||||
) -> tokio::task::JoinHandle<()> {
|
||||
info!(
|
||||
"🔄 [VNC_SYNC] Starting VNC backend sync task: interval={}s",
|
||||
config.sync_interval.as_secs()
|
||||
);
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
let mut interval = tokio::time::interval(config.sync_interval);
|
||||
|
||||
// 首次立即执行同步(用于服务启动时恢复映射)
|
||||
sync_vnc_backends(&pingora_service, &rcoder_prefix, &computer_prefix).await;
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
sync_vnc_backends(&pingora_service, &rcoder_prefix, &computer_prefix).await;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// 同步 VNC 后端映射
|
||||
async fn sync_vnc_backends(
|
||||
pingora_service: &Arc<PingoraProxyService>,
|
||||
rcoder_prefix: &str,
|
||||
computer_prefix: &str,
|
||||
) {
|
||||
let runtime = match docker_manager::runtime::RuntimeManager::get().await {
|
||||
Ok(rt) => rt,
|
||||
Err(e) => {
|
||||
warn!("[VNC_SYNC] Failed to get runtime: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let containers = match runtime.list_containers().await {
|
||||
Ok(list) => list,
|
||||
Err(e) => {
|
||||
warn!("[VNC_SYNC] Failed to list containers from runtime: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if containers.is_empty() {
|
||||
debug!("[VNC_SYNC] Syncing containers");
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用配置化的前缀,而不是硬编码的 ServiceType::container_prefix()
|
||||
|
||||
// 预先收集运行中容器的 key 集合(用于后续清理旧映射)
|
||||
let active_user_ids: std::collections::HashSet<String> = containers
|
||||
.iter()
|
||||
.filter(|c| c.status == container_runtime_api::ContainerRuntimeStatus::Running)
|
||||
.filter_map(|c| {
|
||||
if c.container_name.starts_with(computer_prefix) {
|
||||
return c
|
||||
.container_name
|
||||
.strip_prefix(&format!("{}-", computer_prefix))
|
||||
.map(|v| v.to_string());
|
||||
}
|
||||
if c.container_name.starts_with(rcoder_prefix) {
|
||||
return c
|
||||
.container_name
|
||||
.strip_prefix(&format!("{}-", rcoder_prefix))
|
||||
.map(|v| v.to_string());
|
||||
}
|
||||
None
|
||||
})
|
||||
.filter(|k| !k.is_empty())
|
||||
.collect();
|
||||
|
||||
let mut synced_count = 0;
|
||||
let mut updated_count = 0;
|
||||
|
||||
for container_info in containers {
|
||||
let user_id = if container_info.container_name.starts_with(computer_prefix) {
|
||||
container_info
|
||||
.container_name
|
||||
.strip_prefix(&format!("{}-", computer_prefix))
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
} else if container_info.container_name.starts_with(rcoder_prefix) {
|
||||
container_info
|
||||
.container_name
|
||||
.strip_prefix(&format!("{}-", rcoder_prefix))
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
if user_id.is_empty() {
|
||||
debug!(
|
||||
"⏭️ [VNC_SYNC] Skipping container without business identifier: {}",
|
||||
container_info.container_name
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查容器是否在运行
|
||||
if container_info.status != container_runtime_api::ContainerRuntimeStatus::Running {
|
||||
debug!(
|
||||
"⏭️ [VNC_SYNC] Skipping non-running container: {} (status={})",
|
||||
container_info.container_name,
|
||||
String::from(container_info.status.clone())
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let container_ip = container_info.container_ip.clone();
|
||||
if container_ip.is_empty() {
|
||||
warn!(
|
||||
"⚠️ [VNC_SYNC] Container {} has no IP address",
|
||||
container_info.container_name
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否需要更新映射
|
||||
let needs_update = match pingora_service.get_vnc_backend(&user_id) {
|
||||
Some(existing_ip) if existing_ip == container_ip => {
|
||||
// IP 没有变化,无需更新
|
||||
false
|
||||
}
|
||||
Some(existing_ip) => {
|
||||
// IP 变化了,需要更新
|
||||
debug!(
|
||||
"🔄 [VNC_SYNC] Container IP changed: user_id={}, old={}, new={}",
|
||||
user_id, existing_ip, container_ip
|
||||
);
|
||||
true
|
||||
}
|
||||
None => {
|
||||
// 新容器,需要添加
|
||||
debug!(
|
||||
"➕ [VNC_SYNC] New container mapping: user_id={} -> {}",
|
||||
user_id, container_ip
|
||||
);
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
if needs_update {
|
||||
pingora_service.add_vnc_backend(&user_id, &container_ip);
|
||||
updated_count += 1;
|
||||
}
|
||||
synced_count += 1;
|
||||
}
|
||||
|
||||
if updated_count > 0 {
|
||||
info!(
|
||||
"🔄 [VNC_SYNC] Sync completed: checked={}, updated={}",
|
||||
synced_count, updated_count
|
||||
);
|
||||
} else if synced_count > 0 {
|
||||
debug!(
|
||||
"[VNC_SYNC] Sync completed: synced={}",
|
||||
synced_count
|
||||
);
|
||||
}
|
||||
|
||||
// === 清理已销毁容器的旧映射 ===
|
||||
// 获取当前所有 VNC 后端映射
|
||||
let current_backends = pingora_service.list_vnc_backends();
|
||||
|
||||
let mut removed_count = 0;
|
||||
for user_id in current_backends.keys() {
|
||||
if !active_user_ids.contains(user_id) {
|
||||
pingora_service.remove_vnc_backend(user_id);
|
||||
removed_count += 1;
|
||||
debug!(
|
||||
"🗑️ [VNC_SYNC] Cleaning up already destroyed container mapping: user_id={}",
|
||||
user_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if removed_count > 0 {
|
||||
info!(
|
||||
"🗑️ [VNC_SYNC] Cleanup completed: removed={} mappings",
|
||||
removed_count
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 同步单个容器的 VNC 后端映射
|
||||
///
|
||||
/// 用于在创建容器时立即更新映射,不需要等待定时任务
|
||||
pub async fn sync_single_vnc_backend(
|
||||
pingora_service: &Arc<PingoraProxyService>,
|
||||
user_id: &str,
|
||||
container_ip: &str,
|
||||
) {
|
||||
pingora_service.add_vnc_backend(user_id, container_ip);
|
||||
debug!(
|
||||
"➕ [VNC_SYNC] Single container mapping updated: user_id={} -> {}",
|
||||
user_id, container_ip
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user