跳到主要内容
英文版本为准

本页译自英文,由模型翻译。若与英文原文存在差异,以英文原文为准。

shell 管道

本指南把服务作为 shell 管道 producer | stage | consumer 中的一级运行。

StdioBroker 把进程的标准输入作为流读取,并写入标准输出。 订阅读取某一个流键的行,发布则为下一个进程写出一行。

示例是一个定价级:它读取订单,并用一张收据回复每个订单。

管道级​

src/main.rs
use ruststream_sea_file::stdio::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Debug, PartialEq, Deserialize, Serialize, Outgoing)]
#[outgoing(name = "orders")]
struct Order {
id: u64,
quantity: u64,
unit_cents: u64,
}

#[derive(Debug, PartialEq, Deserialize, Serialize, Outgoing)]
#[outgoing(name = "receipts")]
struct Receipt {
order: u64,
total_cents: u64,
}

#[subscriber("orders", reply)]
async fn price(order: &Order) -> Receipt {
let total_cents = order.quantity * order.unit_cents;
// The log goes to standard error; standard output carries only messages.
tracing::info!(order.id, total_cents, "order priced");
Receipt {
order: order.id,
total_cents,
}
}

#[ruststream::app]
fn app() -> impl App {
RustStream::new(AppInfo::new("pricing", "0.1.0")).with_broker(StdioBroker::new(), |b| {
// A deferred copy leaves on standard output under this key, for a later stage.
b.include(price).out_retry(Publish).to("orders.retry");
})
}

price 读取 orders 下的行,并以一张 Receipt 回复,回复以 receipts 键写到标准输出。 即使 price 从不重试,这一级也需要 out_retry(Publish).to(..):在管道上,延迟副本只能发往后面的某一级,所以每个注册都要指定这个键,没有它这一级就不会启动。

行​

一行由方括号中的消息头和其后的载荷组成(行):

[timestamp | stream_key | sequence | shard_id] payload

消息头的每个字段都是可选的,所以生产者通常只写键:

echo '[orders] {"id":7,"quantity":3,"unit_cents":250}' | ./pricing run

其他键下的行会被跳过,所以多个级可以从同一个流中各取自己的键。 没有消息头的行不指定键,客户端把它交给这一级的每个订阅。 因此,输出普通 JSON 行的工具会同时为每个处理器提供输入:

jq -c '.[]' orders.json | ./pricing run

客户端在服务看到一行之前先解析它的消息头,无法解析的行会让整个进程停止。 有两种行会这样:不是 UTF-8 的输入,以及开头的 [...] 不是消息头,例如一个 JSON 数组。 产生这类载荷的生产者要在每个载荷前面写上消息头。

输出​

这一级每次发布写出一行,消息头由客户端填写:

[2026-10-18T15:08:01 | receipts | 0] {"order":7,"total_cents":750}

时间戳是写入时间,精确到秒;序号为每个键的行从零开始计数。 读取普通 JSON 的工具取消息头之后的载荷:

./pricing run < orders.txt | sed 's/^\[[^]]*\] //' | jq .total_cents

文本行会改变的载荷放进信封发出:二进制载荷、带换行符或首尾空白的载荷,以及每条带消息头的消息。 信封是以 rs1: 或 rs2: 开头的一行,供下一个 RustStream 级使用;它打开信封,把发布时的字节和消息头交给自己的处理器。

日志通过 tracing 写到标准错误,所以从不与消息混在一起。 用 println! 打印到标准输出的一行,会作为没有键的行到达下一级,那里的每个订阅都会读到它。

多级串联​

./intake run < orders.txt | ./pricing run | ./billing run

每一级读取它订阅的键,并为下一级写出回复。 消息头和二进制载荷完整地穿过每一个管道,因为每一级都会打开上一级写出的信封。 稍后重试的一级把副本以挂载点指定的键沿管道向下发送,由后面的某一级读取,重试与死信展示了这一点。

测试这一级​

示例文件在进程内测试这一级:TestApp::start 运行它,tb.broker::<StdioBroker>().message(..) 注入一行订单,published::<Receipt>("receipts") 读取它写出的行。

实时测试把构建好的这一级作为子进程启动,标准输入和标准输出都接到管道上。 它们检查这一级回复 orders 行和没有键的行,跳过其他键,并在遇到 JSON 数组行和非 UTF-8 输入时停止。 loopback 把一级自己的输出交还给它,这样两级可以在一个进程中运行,由一个测试同时驱动。