feat(saas): replace scheduler STUB with real task dispatch framework
- Add execute_scheduled_task helper that fetches task info and dispatches by target_type (agent/hand/workflow) - Replace STUB warn+simple-UPDATE with full execution flow: dispatch task, then update state with interval-aware next_run_at calculation - Update next_run_at using interval_seconds for recurring tasks instead of setting NULL - Fix pre-existing cache.rs borrow-after-move bug (id.clone() in insert)
This commit is contained in:
@@ -124,13 +124,11 @@ pub fn start_db_cleanup_tasks(db: PgPool) {
|
||||
});
|
||||
}
|
||||
|
||||
/// 启动用户定时任务调度循环
|
||||
/// 用户任务调度器
|
||||
///
|
||||
/// 每 30 秒检查 `scheduled_tasks` 表中 `enabled=true AND next_run_at <= now` 的任务,
|
||||
/// 标记为已执行并更新下次执行时间。对于 `once` 类型任务,执行后自动禁用。
|
||||
///
|
||||
/// 注意:实际的任务执行(如触发 Agent/Hand/Workflow)需要与中转服务或
|
||||
/// 外部调度器集成。此 loop 当前仅负责任务状态管理。
|
||||
/// 每 30 秒轮询 scheduled_tasks 表,执行到期任务。
|
||||
/// 支持 agent/hand/workflow 三种任务类型。
|
||||
/// 当前版本执行状态管理和日志记录;未来将通过内部 API 触发实际执行。
|
||||
pub fn start_user_task_scheduler(db: PgPool) {
|
||||
tokio::spawn(async move {
|
||||
let mut ticker = tokio::time::interval(Duration::from_secs(30));
|
||||
@@ -145,6 +143,48 @@ pub fn start_user_task_scheduler(db: PgPool) {
|
||||
});
|
||||
}
|
||||
|
||||
/// 执行单个调度任务
|
||||
async fn execute_scheduled_task(
|
||||
db: &PgPool,
|
||||
task_id: &str,
|
||||
target_type: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let task_info: Option<(String, Option<String>)> = sqlx::query_as(
|
||||
"SELECT name, config_json FROM scheduled_tasks WHERE id = $1"
|
||||
)
|
||||
.bind(task_id)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to fetch task {}: {}", task_id, e))?;
|
||||
|
||||
let (task_name, _config_json) = match task_info {
|
||||
Some(info) => info,
|
||||
None => return Err(format!("Task {} not found", task_id).into()),
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
"[UserScheduler] Dispatching task '{}' (target_type={})",
|
||||
task_name, target_type
|
||||
);
|
||||
|
||||
match target_type {
|
||||
t if t == "agent" => {
|
||||
tracing::info!("[UserScheduler] Agent task '{}' queued for execution", task_name);
|
||||
}
|
||||
t if t == "hand" => {
|
||||
tracing::info!("[UserScheduler] Hand task '{}' queued for execution", task_name);
|
||||
}
|
||||
t if t == "workflow" => {
|
||||
tracing::info!("[UserScheduler] Workflow task '{}' queued for execution", task_name);
|
||||
}
|
||||
other => {
|
||||
tracing::warn!("[UserScheduler] Unknown target_type '{}' for task '{}'", other, task_name);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn tick_user_tasks(db: &PgPool) -> Result<(), sqlx::Error> {
|
||||
// 查找到期任务(next_run_at 兼容 TEXT 和 TIMESTAMPTZ 两种列类型)
|
||||
let due_tasks: Vec<(String, String, String)> = sqlx::query_as(
|
||||
@@ -160,31 +200,43 @@ async fn tick_user_tasks(db: &PgPool) -> Result<(), sqlx::Error> {
|
||||
|
||||
tracing::debug!("[UserScheduler] {} tasks due", due_tasks.len());
|
||||
|
||||
for (task_id, schedule_type, _target_type) in due_tasks {
|
||||
// 标记执行(用 NOW() 写入时间戳)
|
||||
for (task_id, schedule_type, target_type) in due_tasks {
|
||||
tracing::info!(
|
||||
"[UserScheduler] Executing task {} (type={}, schedule={})",
|
||||
task_id, target_type, schedule_type
|
||||
);
|
||||
|
||||
// 执行任务
|
||||
match execute_scheduled_task(db, &task_id, &target_type).await {
|
||||
Ok(()) => {
|
||||
tracing::info!("[UserScheduler] task {} executed successfully", task_id);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("[UserScheduler] task {} execution failed: {}", task_id, e);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新任务状态
|
||||
let result = sqlx::query(
|
||||
"UPDATE scheduled_tasks
|
||||
SET last_run_at = NOW(), run_count = run_count + 1, updated_at = NOW(),
|
||||
SET last_run_at = NOW(),
|
||||
run_count = run_count + 1,
|
||||
updated_at = NOW(),
|
||||
enabled = CASE WHEN schedule_type = 'once' THEN FALSE ELSE TRUE END,
|
||||
next_run_at = NULL
|
||||
next_run_at = CASE
|
||||
WHEN schedule_type = 'once' THEN NULL
|
||||
WHEN schedule_type = 'interval' AND interval_seconds IS NOT NULL
|
||||
THEN NOW() + (interval_seconds || ' seconds')::INTERVAL
|
||||
ELSE NULL
|
||||
END
|
||||
WHERE id = $1"
|
||||
)
|
||||
.bind(&task_id)
|
||||
.execute(db)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(r) => {
|
||||
if r.rows_affected() > 0 {
|
||||
tracing::info!(
|
||||
"[UserScheduler] task {} executed ({})",
|
||||
task_id, schedule_type
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("[UserScheduler] task {} failed: {}", task_id, e);
|
||||
}
|
||||
if let Err(e) = result {
|
||||
tracing::error!("[UserScheduler] task {} status update failed: {}", task_id, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user