xfy 1e556aa125 feat(runner): add SSE task registry and streaming executor
progress.rs:
- EXEC_STREAMS DashMap: SSE handler pops rx by task_id (one-shot)
- StreamEntry wraps rx + created_at for TTL-based GC
- gc_old_tasks now cleans both EXEC_TASKS and EXEC_STREAMS

execute.rs:
- validate_exec_request: extracted shared whitelist + size check
- check_rate_limit_for_user: extracted shared admin-aware rate limit
- start_exec refactored to use both helpers (behavior unchanged)
- start_exec_stream: new server fn, same validation chain, spawns
  run_in_container_stream(tx) + writes EXEC_TASKS for polling fallback
2026-07-10 11:45:20 +08:00

122 lines
4.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 内存任务缓冲表:基于 DashMap 的执行任务注册中心。
//!
//! 异步执行模型StartExec / StartExecStream 立即返回 task_id容器在后台 tokio
//! task 中运行。前端通过 GetExecResult 轮询 EXEC_TASKS 读取阶段与结果,或通过
//! SSE handler 取走 EXEC_STREAMS 的 receiver 做流式输出。任务条目在 TTL 过期后
//! 由 [`gc_old_tasks`] 回收,避免内存无限增长。
use chrono::{DateTime, Duration, Utc};
use dashmap::DashMap;
use std::sync::LazyLock;
use crate::api::code_runner::{ExecResult, ExecStatus, ExecTask};
use crate::infra::docker::OutputChunk;
use crate::infra::runner_config::RUNNER_CONFIG;
/// 全局任务注册表。
pub static EXEC_TASKS: LazyLock<DashMap<String, ExecTask>> = LazyLock::new(DashMap::new);
/// 流式任务表SSE handler 用 task_id 取出 receiver一次性取出即移除
///
/// 后台 task 持有对应的 senderrun_in_container_stream 推 chunk。前端连 SSE
/// 时取走 receiver若前端从不连接或连接失败entry 由 [`gc_old_tasks`] 按
/// TTL 回收,避免 receiver 泄漏。
pub static EXEC_STREAMS: LazyLock<DashMap<String, StreamEntry>> = LazyLock::new(DashMap::new);
/// EXEC_STREAMS 条目receiver + 创建时间(供 GC
pub struct StreamEntry {
pub rx: tokio::sync::mpsc::Receiver<OutputChunk>,
pub created_at: DateTime<Utc>,
}
/// 创建一个排队中的新任务。
pub fn insert_task(id: String) {
let task = ExecTask {
id: id.clone(),
status: ExecStatus::Queued,
stage: "排队中".to_string(),
created_at: Utc::now(),
result: None,
};
EXEC_TASKS.insert(id, task);
}
/// 更新任务阶段(状态 + 描述),不改写 result。
pub fn update_task_stage(id: &str, status: ExecStatus, stage: &str) {
if let Some(mut task) = EXEC_TASKS.get_mut(id) {
task.status = status;
task.stage = stage.to_string();
}
}
/// 写入最终结果:状态置为「执行完毕」并填充 result。
pub fn update_task_result(id: &str, status: ExecStatus, result: ExecResult) {
if let Some(mut task) = EXEC_TASKS.get_mut(id) {
task.status = status;
task.stage = "执行完毕".to_string();
task.result = Some(result);
}
}
/// 回收超过 `RUNNER_CONFIG.task_ttl_secs` 的历史任务。
///
/// 同时清理 EXEC_TASKS轮询兜底与 EXEC_STREAMSSSE 流式)里的过期条目,
/// 避免 receiver 泄漏(前端连了 start_exec_stream 但从不连 SSE 的情况)。
pub fn gc_old_tasks() {
let ttl_secs = RUNNER_CONFIG.task_ttl_secs as i64;
let now = Utc::now();
EXEC_TASKS.retain(|_, task| {
let age = now - task.created_at;
age < Duration::seconds(ttl_secs)
});
EXEC_STREAMS.retain(|_, entry| {
let age = now - entry.created_at;
age < Duration::seconds(ttl_secs)
});
}
#[cfg(all(test, feature = "server"))]
mod tests {
use super::*;
#[test]
#[serial_test::serial]
fn test_task_lifecycle_and_gc() {
let task_id = "test-task-progress-123".to_string();
insert_task(task_id.clone());
assert!(EXEC_TASKS.contains_key(&task_id));
assert_eq!(
EXEC_TASKS.get(&task_id).unwrap().status,
ExecStatus::Queued
);
update_task_stage(&task_id, ExecStatus::Running, "运行中");
assert_eq!(
EXEC_TASKS.get(&task_id).unwrap().status,
ExecStatus::Running
);
let res = ExecResult {
status: ExecStatus::Success,
stdout: "hello".to_string(),
stderr: "".to_string(),
exit_code: Some(0),
duration_ms: 50,
language: "python".to_string(),
};
update_task_result(&task_id, ExecStatus::Success, res);
assert_eq!(
EXEC_TASKS.get(&task_id).unwrap().status,
ExecStatus::Success
);
assert!(EXEC_TASKS.get(&task_id).unwrap().result.is_some());
// 修改创建时间以便测试 GC
if let Some(mut task) = EXEC_TASKS.get_mut(&task_id) {
task.created_at = Utc::now() - Duration::seconds(1000);
}
gc_old_tasks();
assert!(!EXEC_TASKS.contains_key(&task_id));
}
}