ADK-gateway/backend/app/services/scheduler.py
2026-08-05 23:24:57 +08:00

102 lines
4.6 KiB
Python
Raw Permalink 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.

"""规则调度服务:标签匹配→负载过滤→最优选择→任务绑定→推送下发。"""
import logging
import redis.asyncio as aioredis
from app.constants import AgentStatus, TaskStatus
from app.models.schemas import AgentInfo
from app.repository.agent_repo import AgentRepo
from app.repository.task_repo import TaskRepo
from app.services.relay import RelayService
logger = logging.getLogger(__name__)
class Scheduler:
def __init__(
self,
redis: aioredis.Redis,
task_repo: TaskRepo | None = None,
agent_repo: AgentRepo | None = None,
relay: RelayService | None = None,
):
self.redis = redis
self.task_repo = task_repo or TaskRepo(redis)
self.agent_repo = agent_repo or AgentRepo(redis)
self.relay = relay or RelayService(redis)
async def pick_candidate(self, task_tags: list[str]) -> AgentInfo | None:
"""按规则挑选最优 Agent标签匹配→负载过滤→优先级最小且空闲。"""
candidates = await self.agent_repo.by_tags(task_tags)
# 负载过滤:剔除满载
candidates = [a for a in candidates if a.current_load < a.max_concurrent]
if not candidates:
return None
# 最优:优先级最小(越先分配),同优先级取负载最低、在线时间最长(稳定)
candidates.sort(key=lambda a: (a.priority, a.current_load, -a.create_time))
return candidates[0]
async def dispatch(self, request_id: str) -> bool:
"""将单个 pending 任务下发给匹配 Agent绑定 → 推送 payload → 失败回退)。
成功 / 无可用 Agent / 推送失败时,都会在任务上记录 error_info成功时清空
便于前端与日志定位“为什么没分配”。
"""
task = await self.task_repo.get(request_id)
if not task or task.status != TaskStatus.PENDING:
return False
agent = await self.pick_candidate(task.task_tags)
if not agent:
# 暂无可用 Agent标签不匹配 / 离线 / 满载),保持 pending等待下次调度
await self.task_repo.update(
request_id,
error_info="无可用 Agent标签不匹配或全部离线/满载",
)
logger.info("task no candidate request_id=%s tags=%s", request_id, task.task_tags)
return False
# 绑定 Agent标记 running
await self.task_repo.update(
request_id,
status=TaskStatus.RUNNING,
agent_id=agent.agent_id,
progress=0,
)
await self.task_repo.mark_running(request_id)
# 记录调度前状态,供推送失败回退时恢复
prev_status = agent.status
# 标记 Agent 为处理中
await self.agent_repo.update_status(agent.agent_id, AgentStatus.PROCESSING)
# 更新 Agent 负载
await self.agent_repo.adjust_load(agent.agent_id, 1)
# 真实推送 payload 到 Agent 端点
pushed, reason = await self.relay.dispatch_command(agent.agent_id, request_id, task.payload or {})
if not pushed:
# 推送失败:回退 pending 并释放负载,按原状态恢复 Agent等待下次调度
await self.task_repo.update(
request_id,
status=TaskStatus.PENDING,
agent_id="",
progress=0,
error_info=f"推送失败: {reason}",
)
await self.task_repo.mark_pending(request_id)
await self.agent_repo.adjust_load(agent.agent_id, -1)
# 若该 Agent 本为 PROCESSING还有并发容量保持 PROCESSING否则置回 READY
restore = prev_status if prev_status == AgentStatus.PROCESSING else AgentStatus.READY
await self.agent_repo.update_status(agent.agent_id, restore)
logger.warning("task dispatch push failed, reverted request_id=%s agent=%s reason=%s",
request_id, agent.agent_id, reason)
return False
# 推送成功:清空 error_info
await self.task_repo.update(request_id, error_info="")
logger.info("task dispatched request_id=%s -> agent=%s", request_id, agent.agent_id)
return True
async def dispatch_pending(self, batch_size: int = 20) -> int:
"""调度所有可调度的 pending 任务,返回成功下发数。"""
pending = await self.task_repo.pending_tasks()
dispatched = 0
for rid in pending[:batch_size]:
if await self.dispatch(rid):
dispatched += 1
return dispatched