Skip to content

Feature:Add Scheduled tasks - #73

Open
mengnankkkk wants to merge 23 commits into
mainfrom
feature/add_task
Open

Feature:Add Scheduled tasks#73
mengnankkkk wants to merge 23 commits into
mainfrom
feature/add_task

Conversation

@mengnankkkk

@mengnankkkk mengnankkkk commented Aug 5, 2026

Copy link
Copy Markdown
Member

任务调度用数据库锁做轻量分布式协调:多个实例轮询时,只有抢到 lock_owner + lock_until 的实例能执行。
锁不是永久锁,任务失败或实例异常后可以靠 lock_until 自动释放。
锁时长交给部署配置,避免长任务超过固定 30 分钟后被其他实例重复执行 SQL/生成报告。
普通聊天保留 ask_user 挂起能力;定时任务关闭人工追问能力,让 Agent 自己判断无法完成并结束。
强约束放在工具层,prompt 只作为行为提示,不作为可靠控制边界。
主要修改
新增了定时任务的后端 CRUD、实体、Mapper、调度器、下次执行时间计算逻辑,以及前端定时任务管理页。调度器负责轮询到期任务、抢锁、异步执行、完成后计算下一次运行时间。AgentService 和 AskUserTool 增加了 allowUserPrompt 执行上下文,用来区分普通会话和定时任务。
#36

- add scheduled task CRUD, dispatch, run history, and owner-based run locking
- validate schedule expressions, positive intervals, and timezone-aware next runs
- add scheduled task management frontend page
- append scheduled task tables to data_source.sql
@lzq986

lzq986 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

大的PR,加一下设计思路以作记录
顺带加个issue关联

@lzq986
lzq986 requested a review from VLSMB August 5, 2026 14:13
@VLSMB

VLSMB commented Aug 8, 2026

Copy link
Copy Markdown
Member

如果allowUserPrompt字段只是用来区分普通任务或者定时任务,而没有别的功能的话,是不是这个名字体现不出来普通任务或者定时任务。我觉得可以在ctx上加一个任务类型字段,然后定义一个boolean allowUserPrompt()方法,内部根据任务类型返回true/false

Comment thread data-agent-backend/src/main/java/io/github/malonetalk/agent/AgentService.java Outdated
Comment thread data-agent-backend/src/main/java/io/github/malonetalk/agent/AgentService.java Outdated
Comment thread data-agent-backend/src/main/java/io/github/malonetalk/agent/ToolCallContext.java Outdated
- collapse AgentService chat stream overloads into ToolCallContext
- move TaskType out of ToolCallContext and restore context builder
- move scheduled task persistence logic out of controller
- add scheduled task response DTO and scheduler service operations
- split scheduled task execution from mapper-backed scheduling service
@VLSMB

VLSMB commented Aug 15, 2026

Copy link
Copy Markdown
Member

pls resolve conflicts

- rename scheduler API to ScheduledAgentTaskService
- split database polling into DatabaseScheduledAgentTaskDispatcher
- move database claim and finish state handling into DatabaseScheduledAgentTaskRunner
- keep ScheduledAgentTaskExecutor focused on agent execution only
- add configuration for dispatcher mode, batch size, and executor pool settings
@mengnankkkk

Copy link
Copy Markdown
Member Author

@codex review

public record ToolCallContext(String sessionId, Integer userId) {}
public record ToolCallContext(
String sessionId,
String userInput,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ToolCallContext定位是 “向工具传递业务上下文”, 详细看文档 “https://java.agentscope.io/v1/zh/docs/quickstart/agent.html#id4”
所以不要把 userInput、datasourceId 这种不向 工具传递业务上下文的放在这个对象。
userid需要保留在这个工具,因为后面鉴权的时候工具需要依赖它。

*/
package io.github.malonetalk.enums;

public enum ScheduledAgentScheduleType {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRON表达式已经足够表达 每天执行 这种场景了,所以这个枚举类的DAILY 可以废弃,DAILY 可以并入 CRON,但是可以保留INTERVAL ,因为分钟字段上限 59,CRON表达式表达不了

@lzq986

lzq986 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

代码评审意见

整体上这个 PR 的工程质量是好的:执行和调度拆开了(ScheduledAgentTaskExecutor 只负责跑 agent)、CRUD 用 ScheduledAgentTaskService 接口隔离、配置收敛到 @ConfigurationProperties、锁用"乐观锁抢占 + lock_until 自动释放"的思路也对。下面想重点讨论扩展点的位置——现在的口子留在了"调度层",但真正值得留的是"执行层"。

核心建议一:把"执行"抽成一个框架无关的 AgentTaskHandler

现在 ScheduledAgentTaskExecutor.runAgent() 里"调 agent"的这段,是唯一跟调度框架无关的部分。建议抽成一个接口:

public interface AgentTaskHandler {
    void execute(Long taskId, String prompt);
}

为什么:无论将来用 Quartz、XXL-Job,还是继续用轮询,最终都是"到点了调一下 agent"。Quartz 那边写一个 QuartzJobBeanexecute() 里调它;XXL-Job 那边写一个 @XxlJob 方法,方法体里也调它。执行层抽出来后,将来切换调度框架时这一层一行都不用动——这才是真正值得留的口子。

核心建议二:自研分布式锁换成 ShedLock

lock_owner / lock_until 两列,加上 lockForRun / lockForManualRun / finishRun 三句 SQL,本质是在手搓一个分布式锁。建议删掉,换成 ShedLock

为什么

  1. ShedLock 就是专门干"多实例下同一任务只跑一次"这件事的——一个 @SchedulerLock 注解 + 一张 shedlock 表,和现在手搓的 lock_owner / lock_until 是同一套东西,但它是成熟库;
  2. 现在这套自研锁有个无解的坑:lock-duration 默认 PT30M,任务一旦跑超 30 分钟锁就过期,另一个实例会重复执行同一任务(PR 描述里也提到了)。ShedLock 的 lockAtMostFor 虽然也有类似语义,但它额外处理了时钟漂移usingDbTime() 用数据库时钟做唯一时间源)和锁最短持有lockAtLeastFor 防止短任务刚结束就被抢跑)这些边界,比自己维护省心得多;
  3. 代价极低:一个依赖 + 一张表,不用引 Quartz,也不用部署任何额外服务。

核心建议三:别做"调度框架可插拔",先选一个落地

dispatcher=database 这个 @ConditionalOnProperty 开关,意图是以后能切到 Quartz / XXL-Job。但"可插拔"在调度层是做不成的,建议收回。

为什么:Quartz 和 XXL-Job 的"任务注册"模型根本不同——

  • Quartz 是内嵌库:任务用编程式 API 注册,存你自己的表,所以你得自己写 CRUD + 前端页(也就是这个 PR 正在做的);
  • XXL-Job 是外部平台:你的应用只是个执行器,用 @XxlJob 暴露 handler,任务的增删改和 cron 全在它的调度中心控制台里配,不在你的代码里。

所以硬做一个统一的 TaskScheduler 接口(register / pause / triggerNow …),Quartz 那边能填,XXL-Job 那边大半是空壳。更关键的是:切 XXL-Job 的代价不在代码,而在运维——要多部署一个调度中心服务 + 一张 MySQL,这不是改个配置键能完成的。所以"让用户运行时轻松切换"是个伪需求。

落地建议:按当前需求(定时调 agent 生成报告,任务量小、秒级精度够),二选一:

  • A(推荐):保留现在的轮询 + next_run_at 计算,把自研锁换成 ShedLock(即上面的建议二);
  • B:直接上 spring-boot-starter-quartz,CRUD 层保留,把执行包成 QuartzJobBean

两个方案都保留"执行层"这个口子,将来要换框架只换调度层一小撮适配代码。不建议同时兼容两套。

附:可顺手精简的小项

  • ScheduledAgentTaskExecutormax-pool-size 默认等于 core-pool-size(=3),线程池只有队列满才扩到 max,等于死配置,可去掉;
  • ScheduledAgentTaskMapper.xmllockForRunlockForManualRun 只差 WHERE 尾部条件,可合并成一条带 boolean 参数(若按建议二删锁,这两句一并消失);
  • DatabaseScheduledAgentTaskRunner.rootCauseMessage():手写 cause 遍历,可用 org.springframework.core.NestedExceptionUtils.getMostSpecificCause(e) 替代;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants