fleet_manager的任务队列,planner依赖和assignment状态保持
在当前阶段,fleet_namager并不是一个"高级调度器",但是它承担了整个闭环里面最关键的中间环节:
- 接任务
- 看机器人状态
- 选一个当前可用机器人
- 向planner要路径
- 发布assignment
- 等待执行状态和完成状态回来
所以如果说robot_agent是在模拟执行,path_planner是在给路线,那么fleet_manager做的事情其实是把静态任务,变成可以流入系统的动态分配过程
就fleet_manager而言,有三个问题:
- 任务是怎么进入队列的
- 为什么manager当前必须依赖planner
- 为什么manager要主动维持assignment状态,不能只被动相信topic回传
当前阶段目标
当前这个节点的目标,并不是求最优调度,而是先保证下面这条链条真的成立:
SubmitTask -> pending queue -> 选空闲robot -> 请求PlanRoute -> 发布TaskAssignment -> robot执行 -> completed回传
注意这里最重要的不是算法强弱,而是中间状态不能断
只要中间某一环没有接上,整个系统就会退化成下面几种假象:
- 任务只是被登记了,并没有真正被分发
- manager以为已经分配了,robot并没有真正收到
- robot执行结束了,manager却没有正确把资源释放出来
所以现在要关心的是状态流,而不是配置项本身.
从接口定义开始看: manager到底在处理什么数据
如果只看接口层,fleet_manager当前围绕三组核心数据工作.
1. 输入任务 Task
Task.msg当前只有4个字段:
1 | string task_id |
这说明当前任务模型非常克制. 它不是泛化工作流任务,不是复杂调度对象,而是一个非常明确的transport task:
- 有任务ID
- 有取货点
- 有放货点
- 有一个优先级字段,虽然当前调度逻辑还没有真正用起来
这里有一个值得注意的现象:接口层已经为后续调度升级留了入口,但当前实现并没有过早引入复杂策略
这类做法比较稳. 因为字段可以先保留,但调度逻辑先维持最小正确性.
2. 机器人状态 RobotState
1 | string robot_id |
这里真正对manager有意义的不是pose本身,而是下面三个域:
statuscurrent_waypointcurrent_task_id
因为manager当前要回答的不是"机器人绝对坐标是多少",而是:
- 它现在闲不闲
- 它当前在图上的哪个waypoint
- 它身上现在有没有任务没结束
所以从系统语义看,RobotState既是状态广播,也是manager做分配决策的输入缓存.
3. 分配结果 TaskAssignment
1 | string robot_id |
这个消息很关键. 它说明当前manager并不是只告诉robot:“你去做task_x”.
它实际上在告诉robot两层东西:
- 任务是什么
- 这个任务对应的离散执行路径是什么
也就是说,当前系统里manager输出的不是抽象意图,而是带route的执行指令.这会直接把planner拉进manager的关键路径里.
任务是怎么进入pending queue的
fleet_manager通过submit_task服务接收任务:
1 | fleet_msgs/Task task |
当前实现非常直接: 收到请求后把request->task压入pending_tasks_, 然后返回accepted = true, message = "task queued".
这里看起来很简单,但这个"简单"有它的意义.
第一层意义: 先把任务接入和任务执行解耦
也就是说,客户端提交任务时,manager并不承诺"立刻就派出去",而只是承诺:任务先被系统接住,进入待处理状态
这一步把外部请求和内部调度解耦开了. 否则客户端提交时就必须同步等待robot可用,planner可用,整个系统耦合会立刻变重.
第二层意义: manager获得了自己的节奏控制权
任务不是在service callback里直接分配,而是在后续定时器里由assign_pending_tasks()统一尝试分配.
这意味着:
- task intake和task dispatch是分阶段的
- manager可以在统一时机检查当前robot状态
- manager可以在同一逻辑入口里处理planner是否在线,robot是否有current_waypoint等条件
这种写法虽然不复杂,但结构上是对的.
遗留问题和现象1: 当前pending queue只是简单vector,默认FIFO
这说明目前虽然Task里有priority,但是并没有真正参与排序和抢占.
也就是说,现在的队列更接近"先来先处理",不是优先级驱动调度.
为什么manager不能跳过planner
从代码看,assign_pending_tasks()在真的发布assignment之前,会先做几件事:
- 没有pending task就返回
- 如果已有planning request in flight就返回
- 选择一个idle robot
- 检查该robot有没有
current_waypoint - 检查
plan_route服务当前是否可用 - 构造
PlanRoute::Request - 异步请求planner
- 拿到route以后才真正publish assignment
这条路径说明一个很关键的事实:当前manager的分配动作,在逻辑上依赖planner成功返回route
为什么会这样? 因为当前robot_agent接收的不是裸任务,而是TaskAssignment,里面包含route_waypoints.
如果manager不先拿到route,它就只能做两种事情:
- 发布一个不完整assignment
- 把规划逻辑重新塞回manager内部
前者会让agent端协议变得模糊,后者会破坏当前分层. 所以V1阶段用service把planner放在dispatch关键路径上,其实是个非常合理的决定.
这个依赖关系意味着什么
它意味着manager当前不是纯粹的队列管理器,而是一个有前置条件的dispatcher.
这个前置条件包括:
- planner必须在线
- planner必须能在当前图上给出路径
- robot必须知道自己的当前waypoint
这也解释了为什么代码里会出现这种推迟分配的情况:
- robot没有
current_waypoint,先不派 - planner服务还没起来,先不派
- planner返回失败,该task暂时留在队列头部
这类逻辑的本质不是保守,而是:manager宁可延迟,也不发布一个语义不完整的assignment
从工程上看,这是正确的.
为什么要有 planning_request_in_flight_
当前实现里有一个很小但很关键的布尔量:planning_request_in_flight_
它的作用是避免manager在planner结果还没回来时,重复发起新的规划请求.
别小看这个开关. 如果没有它,在定时器周期性触发下,manager可能会对同一个队首任务不断请求planner,从而产生:
- 重复规划
- 重复assignment风险
- 队列状态和回调结果之间的错位
所以这个变量本质上是在做一件事:把"队列头任务正在等待planner结果"这个中间态显式建模出来
这很像有限状态机里补了一个过渡状态. 没有这个状态,系统虽然也能写,但边界会很模糊.
manager选择robot为什么现在只做first-idle
select_idle_robot()当前的逻辑非常直接:
- 遍历
robot_states_ - 谁的
status == "idle",就返回谁 - 找不到就返回空字符串
如果从算法视角看,这个策略很弱. 但从当前项目阶段看,它的作用不是最优,而是稳定.因为当前系统真正先要确认的是:
- robot状态有没有持续上报
- manager能不能稳定缓存这些状态
- task有没有真的被分出去
- 分出去后robot会不会进入执行态
- 完成后manager会不会重新认为它空闲
在这些问题还没有全部稳定之前,过早引入距离代价,电量代价,优先级竞争,只会让故障源增加.
所以这里体现出来的是一个很典型的工程节奏:先保证分配链路成立,再追求分配质量
assignment状态保持问题,其实是这个节点最值得写的一点
fleet_manager里最有意思的一段逻辑,不是push队列,也不是call planner,而是它在接收robot_states时对状态做的修正.
大意是这样:
- 如果之前记录里这个robot已经带有
current_task_id - 但这次新收到的消息却是
status == idle并且current_task_id为空 - 那么manager不会立刻相信这条新消息,而是保留之前那个assignment相关状态
以及另一种情况:
- 如果新消息是
completed - 并且
current_task_id和之前记录中的任务一致 - 那么manager会把内部缓存状态改写成
idle + 空task_id
这个行为非常值得单独分析.
为什么不能只做"收到什么就信什么"
因为topic系统里的状态传播不是原子的.manager在发布assignment后,agent并不会在同一个瞬间立刻切到executing并回传回来. 中间存在一个时间缝隙.
如果manager在这个缝隙里又收到agent按旧节奏发出的idle状态,并且完全照单全收,就会出现一个很危险的现象:同一个robot明明刚被分配出去,却在manager看来又重新空闲了
这会导致什么?
- 同一个robot可能被重复分配
- manager内部的task ownership失真
- 整个assignment生命周期被topic时序打穿
所以manager这里做的不是"修饰日志",而是在做状态一致性兜底.
这段逻辑本质上在弥补什么
它弥补的是这个事实:dispatch状态和执行状态属于两个不同节点产生的事件,它们之间天然存在传播延迟
manager自己知道:“我刚刚给robot_x分配了task_y”.
agent自己知道:“我下一拍才会把执行状态发出来”.
如果中间没有一个节点负责记住这个过渡事实,系统就会在短时间窗口里自相矛盾.而当前manager正是在承担这个责任.所以从分析角度看,这不是简单的缓存,而更像是:manager内部维护了一份比topic表面值更强的assignment真相
这个点实际上非常适合后续继续扩展成更正式的状态机设计.
从这里可以看到当前V1的边界
虽然fleet_manager已经把最小闭环接起来了,但它的边界也很明显.
1. 还没有真正使用priority
所以任务调度语义目前并不完整.
2. 还没有冲突检测/预约
也就是说,manager现在只回答"谁闲着可以接",还没有回答"谁接了以后不会和别人撞路".
3. robot选择没有代价模型
当前没有考虑:
- 谁离pickup更近
- 谁电量更合适
- 谁整体任务完成时间更优
4. planner失败后的重试/降级策略还比较朴素
现在更接近"这次不行就先挂着",还没有更复杂的故障恢复行为.
5. 状态保持逻辑还是局部补丁式的
它已经解决了当前最直接的问题,但如果后面任务生命周期变复杂,就会需要更正式的状态模型.
遗留问题和现象2: 当前manager已经不是简单消息转发器,它正在逐步演化成一个显式维护任务生命周期的中心状态节点
最后
如果只用一句话总结当前这个fleet_manager专题,那就是:
它当前最重要的价值,不是实现了多聪明的调度,而是把任务受理,路径依赖,分配发布和状态保持这几件事放在了同一个可控节点里,从而把V1系统的主链路真正拧成了一条线
If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !