feat(scheduler): 定时任务后端持久化 + Pipeline trigger 编译修复
S4/S8 定时任务后端: - 新增 scheduled_tasks 表 (migration v7) - 新增 scheduled_task CRUD 模块 (handlers/service/types) - 注册 /api/scheduler/tasks 路由 (GET/POST/PATCH/DELETE) - 新增 start_user_task_scheduler() 30秒轮询循环 - 支持 cron/interval/once 三种调度类型 - once 类型执行后自动禁用 修复: - pipeline_commands.rs: 修复 pipeline.trigger 字段不存在的编译错误 (Pipeline 结构体无 trigger 字段,改用 metadata.tags/description)
This commit is contained in:
@@ -99,3 +99,75 @@ pub fn start_db_cleanup_tasks(db: PgPool) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 启动用户定时任务调度循环
|
||||
///
|
||||
/// 每 30 秒检查 `scheduled_tasks` 表中 `enabled=true AND next_run_at <= now` 的任务,
|
||||
/// 标记为已执行并更新下次执行时间。对于 `once` 类型任务,执行后自动禁用。
|
||||
///
|
||||
/// 注意:实际的任务执行(如触发 Agent/Hand/Workflow)需要与中转服务或
|
||||
/// 外部调度器集成。此 loop 当前仅负责任务状态管理。
|
||||
pub fn start_user_task_scheduler(db: PgPool) {
|
||||
tokio::spawn(async move {
|
||||
let mut ticker = tokio::time::interval(Duration::from_secs(30));
|
||||
ticker.tick().await; // 跳过首次立即触发
|
||||
|
||||
loop {
|
||||
ticker.tick().await;
|
||||
if let Err(e) = tick_user_tasks(&db).await {
|
||||
tracing::error!("[UserScheduler] tick error: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn tick_user_tasks(db: &PgPool) -> Result<(), sqlx::Error> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
// 查找到期任务
|
||||
let due_tasks: Vec<(String, String, String)> = sqlx::query_as(
|
||||
"SELECT id, schedule_type, target_type FROM scheduled_tasks
|
||||
WHERE enabled = TRUE AND next_run_at <= $1"
|
||||
)
|
||||
.bind(&now)
|
||||
.fetch_all(db)
|
||||
.await?;
|
||||
|
||||
if due_tasks.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tracing::debug!("[UserScheduler] {} tasks due", due_tasks.len());
|
||||
|
||||
for (task_id, schedule_type, _target_type) in due_tasks {
|
||||
// 标记执行
|
||||
let now_str = chrono::Utc::now().to_rfc3339();
|
||||
let result = sqlx::query(
|
||||
"UPDATE scheduled_tasks
|
||||
SET last_run_at = $1, run_count = run_count + 1, updated_at = $1,
|
||||
enabled = CASE WHEN schedule_type = 'once' THEN FALSE ELSE TRUE END,
|
||||
next_run_at = NULL
|
||||
WHERE id = $2"
|
||||
)
|
||||
.bind(&now_str)
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user