refactor(scheduler): 任务执行移出 tick 循环,worker_queue_capacity 并发约束生效
原 process_tick 内联 await execute_job:长耗时 agent 任务(可达数分钟)会串行阻塞整个调度循环,拖后所有其他 job 的触发;worker_queue_capacity 配置项从未被使用。 - 到期 job 的执行与事后状态写入移入 tokio::spawn 后台任务,tick 循环只做派发 - 新增 worker_semaphore(容量=worker_queue_capacity):槽位耗尽时 job 保持 Scheduled 留待下个 tick,形成有界背压 - job 派发前置为 Running 状态,is_due 要求 Scheduled,天然防止重复派发 - after_execution 状态推进失败时回退为 Scheduled,避免 job 永久卡在 Running - execute_job 拆分为 execute_job_inner(不依赖 &self,可移入后台任务)
This commit is contained in:
parent
4b93c84447
commit
9b8c64bd21
@ -5,7 +5,7 @@ use std::sync::Arc;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::{DateTime, Duration as ChronoDuration, TimeZone, Utc};
|
use chrono::{DateTime, Duration as ChronoDuration, TimeZone, Utc};
|
||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
use tokio::sync::watch;
|
use tokio::sync::{Semaphore, watch};
|
||||||
|
|
||||||
use crate::bus::{MessageBus, OutboundMessage};
|
use crate::bus::{MessageBus, OutboundMessage};
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
@ -71,6 +71,9 @@ pub struct Scheduler {
|
|||||||
jobs: Arc<dyn SchedulerJobRepository>,
|
jobs: Arc<dyn SchedulerJobRepository>,
|
||||||
agent_task_executor: Arc<dyn AgentTaskExecutor>,
|
agent_task_executor: Arc<dyn AgentTaskExecutor>,
|
||||||
maintenance_executor: Arc<dyn MaintenanceExecutor>,
|
maintenance_executor: Arc<dyn MaintenanceExecutor>,
|
||||||
|
/// 并发执行槽位:限制同时执行的 job 数量(worker_queue_capacity)。
|
||||||
|
/// tick 循环只负责派发,job 执行在后台任务中进行,长任务不再阻塞其他 job。
|
||||||
|
worker_semaphore: Arc<Semaphore>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scheduler {
|
impl Scheduler {
|
||||||
@ -86,6 +89,7 @@ impl Scheduler {
|
|||||||
A: AgentTaskExecutor + 'static,
|
A: AgentTaskExecutor + 'static,
|
||||||
M: MaintenanceExecutor + 'static,
|
M: MaintenanceExecutor + 'static,
|
||||||
{
|
{
|
||||||
|
let worker_capacity = config.worker_queue_capacity.max(1);
|
||||||
Self {
|
Self {
|
||||||
bus,
|
bus,
|
||||||
config,
|
config,
|
||||||
@ -93,6 +97,7 @@ impl Scheduler {
|
|||||||
jobs,
|
jobs,
|
||||||
agent_task_executor: Arc::new(agent_task_executor),
|
agent_task_executor: Arc::new(agent_task_executor),
|
||||||
maintenance_executor: Arc::new(maintenance_executor),
|
maintenance_executor: Arc::new(maintenance_executor),
|
||||||
|
worker_semaphore: Arc::new(Semaphore::new(worker_capacity)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,7 +229,7 @@ impl Scheduler {
|
|||||||
let jobs = self.jobs.list_scheduler_jobs(true)?;
|
let jobs = self.jobs.list_scheduler_jobs(true)?;
|
||||||
|
|
||||||
for record in jobs {
|
for record in jobs {
|
||||||
let Some(mut job) =
|
let Some(job) =
|
||||||
RuntimeJob::from_record(&record, self.config.misfire_policy, self.timezone)?
|
RuntimeJob::from_record(&record, self.config.misfire_policy, self.timezone)?
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
@ -248,6 +253,18 @@ impl Scheduler {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 尝试获取一个 worker 槽位:并发达到上限(worker_queue_capacity)时
|
||||||
|
// 不启动该 job——保持 Scheduled 状态,留待下一个 tick 重试。
|
||||||
|
// 这样长任务不会阻塞 tick 循环,也不会无界堆积并发执行。
|
||||||
|
let Ok(permit) = self.worker_semaphore.clone().try_acquire_owned() else {
|
||||||
|
tracing::warn!(
|
||||||
|
job_id = %job.id,
|
||||||
|
capacity = self.config.worker_queue_capacity,
|
||||||
|
"Scheduler worker capacity exhausted, deferring job to next tick"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
self.jobs.update_scheduler_job_runtime(
|
self.jobs.update_scheduler_job_runtime(
|
||||||
&job.id,
|
&job.id,
|
||||||
SchedulerJobState::Running,
|
SchedulerJobState::Running,
|
||||||
@ -260,62 +277,136 @@ impl Scheduler {
|
|||||||
job.completed_at,
|
job.completed_at,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let execution_result = self.execute_job(&job).await;
|
// 执行与事后状态写入移入后台任务:tick 循环只做派发,
|
||||||
job.after_execution(
|
// 长耗时任务(agent_task 可能长达数分钟)不再串行阻塞其他 job 的触发。
|
||||||
now,
|
// job 在 DB 中已是 Running 状态,is_due 要求 Scheduled,因此不会被重复派发。
|
||||||
execution_result.as_ref().err().map(|err| err.to_string()),
|
let bus = self.bus.clone();
|
||||||
self.config.misfire_policy,
|
let jobs_repo = self.jobs.clone();
|
||||||
self.timezone,
|
let agent_executor = self.agent_task_executor.clone();
|
||||||
)?;
|
let maintenance_executor = self.maintenance_executor.clone();
|
||||||
|
let misfire_policy = self.config.misfire_policy;
|
||||||
|
let timezone = self.timezone;
|
||||||
|
let fire_at = now;
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let execution_result = Scheduler::execute_job_inner(
|
||||||
|
&bus,
|
||||||
|
&*agent_executor,
|
||||||
|
&*maintenance_executor,
|
||||||
|
&job,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
let status = if execution_result.is_ok() {
|
if let Err(error) = &execution_result {
|
||||||
Some(SchedulerJobStatus::Ok)
|
tracing::error!(job_id = %job.id, error = %error, "Scheduler job failed");
|
||||||
} else {
|
}
|
||||||
Some(SchedulerJobStatus::Error)
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(error) = &execution_result {
|
let status = if execution_result.is_ok() {
|
||||||
tracing::error!(job_id = %job.id, error = %error, "Scheduler job failed");
|
Some(SchedulerJobStatus::Ok)
|
||||||
}
|
} else {
|
||||||
|
Some(SchedulerJobStatus::Error)
|
||||||
|
};
|
||||||
|
|
||||||
self.jobs.update_scheduler_job_runtime(
|
let mut job = job;
|
||||||
&job.id,
|
match job.after_execution(
|
||||||
job.state.clone(),
|
fire_at,
|
||||||
status,
|
execution_result.as_ref().err().map(|err| err.to_string()),
|
||||||
job.last_error.as_deref(),
|
misfire_policy,
|
||||||
job.run_count,
|
timezone,
|
||||||
job.last_fired_at,
|
) {
|
||||||
job.next_fire_at,
|
Ok(()) => {
|
||||||
job.paused_at,
|
if let Err(error) = jobs_repo.update_scheduler_job_runtime(
|
||||||
job.completed_at,
|
&job.id,
|
||||||
)?;
|
job.state.clone(),
|
||||||
|
status,
|
||||||
|
job.last_error.as_deref(),
|
||||||
|
job.run_count,
|
||||||
|
job.last_fired_at,
|
||||||
|
job.next_fire_at,
|
||||||
|
job.paused_at,
|
||||||
|
job.completed_at,
|
||||||
|
) {
|
||||||
|
tracing::error!(
|
||||||
|
job_id = %job.id,
|
||||||
|
error = %error,
|
||||||
|
"Failed to persist scheduler job state after execution"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
// 兜底:状态推进失败时回退为 Scheduled,避免 job 永远卡在 Running
|
||||||
|
tracing::error!(
|
||||||
|
job_id = %job.id,
|
||||||
|
error = %error,
|
||||||
|
"Failed to compute post-execution scheduler state, resetting to Scheduled"
|
||||||
|
);
|
||||||
|
if let Err(update_error) = jobs_repo.update_scheduler_job_runtime(
|
||||||
|
&job.id,
|
||||||
|
SchedulerJobState::Scheduled,
|
||||||
|
Some(SchedulerJobStatus::Error),
|
||||||
|
Some(&error.to_string()),
|
||||||
|
job.run_count,
|
||||||
|
job.last_fired_at,
|
||||||
|
job.next_fire_at,
|
||||||
|
job.paused_at,
|
||||||
|
job.completed_at,
|
||||||
|
) {
|
||||||
|
tracing::error!(
|
||||||
|
job_id = %job.id,
|
||||||
|
error = %update_error,
|
||||||
|
"Failed to persist scheduler job state after execution failure"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// permit 持有到执行与状态写入全部完成后才释放
|
||||||
|
drop(permit);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 执行单个 job(测试直接调用入口;生产派发走 process_tick 的后台任务)。
|
||||||
|
#[cfg(test)]
|
||||||
async fn execute_job(&self, job: &RuntimeJob) -> anyhow::Result<()> {
|
async fn execute_job(&self, job: &RuntimeJob) -> anyhow::Result<()> {
|
||||||
|
Self::execute_job_inner(
|
||||||
|
&self.bus,
|
||||||
|
self.agent_task_executor.as_ref(),
|
||||||
|
self.maintenance_executor.as_ref(),
|
||||||
|
job,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// job 执行主体:不依赖 &self,便于移入 tokio::spawn 的后台任务。
|
||||||
|
async fn execute_job_inner(
|
||||||
|
bus: &Arc<MessageBus>,
|
||||||
|
agent_task_executor: &dyn AgentTaskExecutor,
|
||||||
|
maintenance_executor: &dyn MaintenanceExecutor,
|
||||||
|
job: &RuntimeJob,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
match job.kind {
|
match job.kind {
|
||||||
SchedulerJobKind::OutboundMessage => {
|
SchedulerJobKind::OutboundMessage => {
|
||||||
let message = build_outbound_message(job)?;
|
let message = build_outbound_message(job)?;
|
||||||
// publish_outbound 失败(bus 满或关闭)不视为 job 失败:
|
// publish_outbound 失败(bus 满或关闭)不视为 job 失败:
|
||||||
// 通知丢弃是预期的背压行为,标记 job 失败会触发 misfire 重试风暴
|
// 通知丢弃是预期的背压行为,标记 job 失败会触发 misfire 重试风暴
|
||||||
if let Err(e) = self.bus.publish_outbound(message).await {
|
if let Err(e) = bus.publish_outbound(message).await {
|
||||||
tracing::warn!(error = %e, job_id = %job.id, "Dropping outbound for scheduler job");
|
tracing::warn!(error = %e, job_id = %job.id, "Dropping outbound for scheduler job");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SchedulerJobKind::InternalEvent => {
|
SchedulerJobKind::InternalEvent => {
|
||||||
execute_internal_event(self.maintenance_executor.as_ref(), job).await?;
|
execute_internal_event(maintenance_executor, job).await?;
|
||||||
}
|
}
|
||||||
SchedulerJobKind::AgentTask => {
|
SchedulerJobKind::AgentTask => {
|
||||||
let outbound_messages = execute_agent_task(
|
let outbound_messages = execute_agent_task(
|
||||||
self.agent_task_executor.as_ref(),
|
agent_task_executor,
|
||||||
job,
|
job,
|
||||||
required_notification_chat_id(job, "agent_task")?,
|
required_notification_chat_id(job, "agent_task")?,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
for message in outbound_messages {
|
for message in outbound_messages {
|
||||||
if let Err(e) = self.bus.publish_outbound(message).await {
|
if let Err(e) = bus.publish_outbound(message).await {
|
||||||
tracing::warn!(error = %e, job_id = %job.id, "Dropping outbound for scheduler agent task");
|
tracing::warn!(error = %e, job_id = %job.id, "Dropping outbound for scheduler agent task");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -339,7 +430,7 @@ impl Scheduler {
|
|||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if let Err(notify_error) =
|
if let Err(notify_error) =
|
||||||
self.notify_silent_agent_task_failure(job, &e).await
|
Self::notify_silent_agent_task_failure(bus, job, &e).await
|
||||||
{
|
{
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
job_id = %job.id,
|
job_id = %job.id,
|
||||||
@ -354,7 +445,7 @@ impl Scheduler {
|
|||||||
Ok(o) => o,
|
Ok(o) => o,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if let Err(notify_error) =
|
if let Err(notify_error) =
|
||||||
self.notify_silent_agent_task_failure(job, &e).await
|
Self::notify_silent_agent_task_failure(bus, job, &e).await
|
||||||
{
|
{
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
job_id = %job.id,
|
job_id = %job.id,
|
||||||
@ -366,8 +457,7 @@ impl Scheduler {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(error) = self
|
if let Err(error) = agent_task_executor
|
||||||
.agent_task_executor
|
|
||||||
.execute_silent(
|
.execute_silent(
|
||||||
job.target.channel.as_deref().unwrap_or_default(),
|
job.target.channel.as_deref().unwrap_or_default(),
|
||||||
&session_chat_id,
|
&session_chat_id,
|
||||||
@ -378,7 +468,7 @@ impl Scheduler {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
if let Err(notify_error) =
|
if let Err(notify_error) =
|
||||||
self.notify_silent_agent_task_failure(job, &error).await
|
Self::notify_silent_agent_task_failure(bus, job, &error).await
|
||||||
{
|
{
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
job_id = %job.id,
|
job_id = %job.id,
|
||||||
@ -395,7 +485,7 @@ impl Scheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn notify_silent_agent_task_failure(
|
async fn notify_silent_agent_task_failure(
|
||||||
&self,
|
bus: &Arc<MessageBus>,
|
||||||
job: &RuntimeJob,
|
job: &RuntimeJob,
|
||||||
error: &anyhow::Error,
|
error: &anyhow::Error,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
@ -413,8 +503,7 @@ impl Scheduler {
|
|||||||
"silent_agent_task".to_string(),
|
"silent_agent_task".to_string(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Err(e) = self
|
if let Err(e) = bus
|
||||||
.bus
|
|
||||||
.publish_outbound(OutboundMessage::error_notification(
|
.publish_outbound(OutboundMessage::error_notification(
|
||||||
channel,
|
channel,
|
||||||
chat_id,
|
chat_id,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user