失败、停止与收尾¶
程序运行中会遇到三类不同的事:节点算出了"做不到"的结果,某个执行域失败了,或者外部要求整个程序停下来。roplat 把它们分开表达,并保证在合作式退出时等待在途的工作、归还对象、执行收尾。
完整程序¶
第一个 System 依次执行三段运动,中间一段超出行程而失败,父层记录下来后继续下一段。第二个 System 是一个永不自行结束的监视循环,由外部在 30 ms 后请求停止。
use std::time::Duration;
use roplat::rhythm::{IntoRhythm, WithPeriod, once};
use roplat::{Completion, Execution, ExecutionContext, Lifecycle, Node, RoplatError, RoplatResult};
/// 单轴模拟机械臂。
#[derive(Default)]
struct Arm {
position: f64,
}
impl Lifecycle for Arm {
type Error = RoplatError;
}
/// 运动节点:超出行程时返回业务错误。
struct Motion;
impl Lifecycle for Motion {
type Error = RoplatError;
}
impl Node for Motion {
type Resources = (Arm,);
type Input = f64;
type Output = Result<(), RoplatError>;
async fn process<'call>(&'call mut self, goal: f64, (arm,): (&'call mut Arm,)) -> Self::Output {
if goal.abs() > 1.0 {
return Err(RoplatError::NodeProcessing(format!("目标 {goal} 超出行程")));
}
arm.position = goal;
Ok(())
}
}
/// 父层显式处理每一段的完整执行结果。
#[derive(Default)]
struct Report {
lines: Vec<String>,
}
impl Lifecycle for Report {
type Error = RoplatError;
}
impl Node for Report {
type Resources = ();
type Input = (f64, Execution<()>);
type Output = ();
async fn process(&mut self, (goal, outcome): Self::Input, _: ()) {
let line = match outcome {
Ok(Completion::Completed(())) => format!("segment {goal}: completed"),
Ok(Completion::Stopped) => format!("segment {goal}: stopped"),
Err(error) => format!("segment {goal}: failed ({error})"),
};
self.lines.push(line);
}
}
/// 一段失败不影响后面的段:每段是一个 once 子域,父层读它的 .outcome。
#[roplat::system]
async fn segments(goals: Vec<f64>) -> RoplatResult<Vec<String>> {
let mut arm = Arm::default();
let mut tasks = goals.into_iter().into_rhythm();
let mut segment = once::<f64, ()>();
let mut motion = Motion;
let mut report = Report::default();
tasks
>> |goal| {
goal >> segment
>> |goal| {
&mut arm >> motion.resources.0;
goal >> motion;
motion?
};
goal >> report.input.0;
segment.outcome >> report.input.1;
};
Ok(report.lines.clone())
}
/// 每次采样计一次数。
struct Sampler {
samples: usize,
}
impl Lifecycle for Sampler {
type Error = RoplatError;
}
impl Node for Sampler {
type Resources = ();
type Input = u64;
type Output = ();
async fn process(&mut self, _tick: u64, _: ()) {
self.samples += 1;
}
}
/// 日志节点:由 System 创建,停止时也会被关闭。
struct Log;
impl Lifecycle for Log {
type Error = RoplatError;
async fn on_shutdown(&mut self) -> RoplatResult<()> {
println!("log closed");
Ok(())
}
}
impl Node for Log {
type Resources = ();
type Input = ();
type Output = ();
async fn process(&mut self, (): (), _: ()) {}
}
/// 永不自行结束的监视循环,由外部通过上下文停止。
#[roplat::system(context = context)]
async fn monitor(context: ExecutionContext, sampler: &mut Sampler) -> Execution<()> {
let mut ticks = (0..).with_period(Duration::from_millis(2));
let mut log = Log;
ticks
>> |tick| {
tick >> sampler;
log;
};
Ok(Completion::Completed(()))
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> RoplatResult<()> {
for line in segments(vec![0.5, 3.0, -0.5]).await? {
println!("{line}");
}
let context = ExecutionContext::new();
let stopper = context.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(30)).await;
stopper.request_stop();
});
let mut sampler = Sampler { samples: 0 };
let result = monitor(context, &mut sampler).await;
assert!(sampler.samples > 0);
match result {
Ok(Completion::Stopped) => println!("monitor: stopped"),
other => println!("monitor: unexpected {other:?}"),
}
Ok(())
}
输出:
segment 0.5: completed
segment 3: failed (节点处理错误: 目标 3 超出行程)
segment -0.5: completed
log closed
monitor: stopped
三层含义¶
| 层次 | 例子 | 表达方式 |
|---|---|---|
| 业务结果 | 运动节点返回 Err,"目标超出行程" |
节点 Output 里的 Result,默认只是数据 |
| 执行失败 | 用 motion? 把业务错误提升为本域失败 |
Execution 的 Err |
| 合作式停止 | 外部调用 request_stop() |
Execution 的 Ok(Completion::Stopped) |
Execution<T> 是 Result<Completion<T>, RoplatError>,Completion<T> 是 Completed(T) 或 Stopped。停止不是错误:它没有业务值,但也没有失败。
一段失败,父层决定怎么办¶
tasks >> |goal| {
goal >> segment >> |goal| {
&mut arm >> motion.resources.0;
goal >> motion;
motion?
};
goal >> report.input.0;
segment.outcome >> report.input.1;
};
segment 是 once() 节律:每次 drive 只运行一个周期,用来把一组节点包成一个可以单独处理结果的子域。周期域以 motion? 结尾:成功时 Feed 是 (),失败时这个子域失败。
父层读取子节律结果有两种写法:
segment.output(或直接写segment):读取正常完成时的 Output。子域失败或停止时,默认向父域传播,依赖它的后续节点不会运行。segment.outcome:把完整的Execution<Output>交给父图处理。这里交给report记录,任务器继续下一段。
同一个子节律只能选其中一种写法。.outcome 只隔离这个子域自己的失败;父域已经收到的停止请求,它不能撤销。
从外部停止¶
#[roplat::system(context = context)]
async fn monitor(context: ExecutionContext, sampler: &mut Sampler) -> Execution<()> { ... }
context = context让 System 使用调用方传入的停止上下文。调用方保留它的一个 clone,就能随时request_stop()。- 返回类型写成
Execution<T>,停止时得到Ok(Completion::Stopped)。写成RoplatResult<T>也可以,但停止会被表示为Err(RoplatError::Stopped);返回普通值的 System 在失败或停止时会在收尾后 panic。 - 节律在每个周期开始前观察停止请求;正在运行的周期会完整结束,交回对象之后才退出。
- 停止是粘性的:一个上下文停止后不会恢复。要重新运行,创建新的上下文。
context.child()创建子上下文:父停止会传到子,子停止不影响父和兄弟。
收尾顺序¶
输出里 log closed 出现在 monitor: stopped 之前:System 先等在途的周期结束、取回对象,再按创建的逆序调用本层对象的 on_shutdown,最后才返回。整个过程是:
- 请求停止,不再开始新的工作。
- 已经开始的周期和分支完整返回,交回它们用到的对象。
- 创建层关闭自己创建的对象。
参数传进来的 sampler 不由 System 关闭;调用方在 System 返回后还能读取它的计数。
保证的边界¶
合作式失败和停止保证等待在途工作并归还对象。以下情况不在保证内:panic、丢弃 System 的 Future、任务被 abort,以及节点里不合作的无限循环或阻塞调用。
request_stop() 是软件层面的请求,不会自动让设备进入安全状态,也不会撤回已经发出的指令。设备怎样保持、减速、确认停止,由驱动定义。
下一篇:多设备与旁路。