6.3 Action 长任务
Service 适合"一问一答、立刻能答"的事。但如果小莫要做的是一件很耗时的事呢?比如"走到门口"——这得走好几秒。要是用 Service,小莫就得傻等到走完才收到一个"到了",中间毫无音讯,也没法喊停。
这时候需要第三种模式:Action(动作)长任务。它专为"耗时任务"设计:发一个目标,服务端边做边汇报进度,做完给最终结果,中途还能取消。
"走到门口""把手臂抬到 90 度"这类活儿都得花时间。我可不想布置完就干等着——我想随时知道"走到一半了""快到了",必要时还能喊"停,别走了!"
学习目标
学完本节,你将能够:
- 说清 Action 的三件套:目标(goal)→ 反馈(feedback)→ 结果(result);
- 理解
goal_id(目标编号) 和goal_status(目标状态) 两个关键元数据; - 用纯 Python 实现一个"边做边报进度"的长任务服务端;
- 理解 Action 的取消(cancel)机制。
前置要求
- 完成 6.2 Service,理解
request_id配对与元数据的用法; - Action 的编号机制和 Service 很像,建议先弄懂 Service 再学本节。
先回到黑板教室
第一章这样点过 Action:
老师布置一张卷子,同学边做边举手报进度:"做一半了""快了""交卷!"
对比前两种模式,Action 的特别之处是过程中有多次反馈:
Action 有三种消息:
- 目标(goal):客户端布置的任务("做这张卷子");
- 反馈(feedback):服务端做的过程中,不断汇报的进度("做一半了");
- 结果(result):任务做完时的最终结论("交卷,得分 95")。
两个关键元数据:goal_id 和 goal_status
和 Service 用 request_id 配对一样,Action 也要靠元数据来协调。它用两个:
goal_status 有三种约定好的值(必须小写、拼写精确):
必须用精确的小写字符串:"succeeded"、"aborted"、"canceled"。写成 "Succeeded" 或拼错,接收方就认不出,任务状态会乱。
动手实现:一个倒计时任务
我们做个直观的例子:客户端布置"从 N 倒数到 0"的目标,服务端每步汇报"还剩几",数到 0 就报告完成。目录 course/ch06-action。
服务端 server.py
服务端要"边做边报"。它的诀窍是:不要在一次事件里把整个长任务做完(那样会卡住),而是每次被触发就推进一步,靠自己维护"还剩几"的状态。
核心思路:active_goals 字典记住每个目标"还剩几",tick 每次到点就让每个目标前进一步——这样长任务被拆成了许多小步,既能持续汇报,又不会卡住事件循环。
客户端 client.py
连成数据流 dataflow.yml
Action 的连线比 Service 更"热闹"——客户端有 goal/cancel 两个输出,服务端有 feedback/result 两个输出,双向都要连:
跑起来
你会看到任务边做边报、最后完成:
从"布置"到"边做边报"再到"完成"——这就是 Action 的完整生命周期!
看到那一条条"还剩 X"了吗?这就是我要的!布置完不用干等,全程都知道进展到哪了,心里踏实多啦。
取消(cancel):中途喊停
Action 比 Service 强的另一点,是能中途取消。机制很简单:
- 客户端往
cancel输出发一条消息,带上要取消的goal_id; - 服务端在处理中检查到 cancel,就停止那个目标,并发一个
goal_status = "canceled"的结果。
我们的服务端代码已经写好了 cancel 分支。你可以改客户端,在收到"还剩 3"时发一条取消试试:
(需要你把 goal_id 存成一个变量 current_goal_id 以便这里引用。)
真实机器人里,"取消"是安全刚需。比如小莫正伸手去抓杯子,你突然喊停——必须能立刻中止,否则可能撞坏东西。Action 天生支持这种"随时喊停",这是它比 Service 更适合真实动作任务的关键原因。
三种模式的递进关系
到这里,前三种模式的关系就清晰了——它们是层层加码的:
- Topic 什么都不要;
- Service 加了"配对编号",换来一个回复;
- Action 再加"状态 + 多次反馈 + 取消",换来对长任务的完整掌控。
它们底层都还是 Topic(pub/sub)+ 元数据约定——你已经能看出这套设计的优雅了。
进阶延伸:并发多个目标与容错(可跳过)
我们的服务端用 active_goals 字典,其实天然支持同时进行多个目标——每个 goal_id 独立倒计时、独立汇报。DORA 官方的 action 示例服务端能同时管理多达 64 个目标。
真实系统还要考虑:服务端崩溃重启后,进行中的目标怎么办?成熟做法会用超时和"服务端重启"信号来兜底(Rust API 有 recv_action_result 辅助)。这些等你做真实项目再深入,现在掌握"目标→反馈→结果 + 取消"的主干即可。
动手练习
改造服务端的反馈:除了"还剩几",再算出完成百分比(比如从 5 倒数、剩 2 时,完成度 = (5-2)/5 = 60%),一起发给客户端显示。
提示:反馈时可以发一个包含两个数的数组 pa.array([remaining, percent]),客户端两个都读出来。
参考答案思路
服务端记住每个目标的起始值(不只是剩余值),才能算百分比。可以把 active_goals[goal_id] 从"剩余值"改成一个元组 (起始值, 剩余值):
客户端 event["value"].to_pylist() 拿到 [剩余, 百分比] 两个值分别显示。
常见报错 FAQ
检查 dataflow.yml 里客户端是否订阅了 server/feedback 和 server/result,服务端是否声明了这两个 outputs。Action 双向连线多,容易漏一条。
确认服务端配了 tick 定时器,并且倒计时逻辑放在 tick 分支里"每次推进一步"。如果你把整个倒计时写成一个 for 循环一次做完,就没有"边做边报"的效果了(还会卡住事件循环)。
确认取消消息带了正确的 goal_id,且服务端的 cancel 分支能在 active_goals 里找到它。编号对不上就取消不了。
小结
- Action(长任务) 专为耗时任务设计:目标(goal)→ 反馈(feedback)→ 结果(result),还能中途取消。
- 靠
goal_id配对、靠goal_status(succeeded/aborted/canceled,小写)表示最终状态。 - 服务端要把长任务拆成小步,每次
tick推进一步并汇报,避免卡住事件循环。 - Topic → Service → Action 是层层加码的递进,底层都是 pub/sub + 元数据。
下一节学最后一种模式 Streaming(流式)——处理像语音、视频这样连续不断、还能随时打断的数据流。