内存 Broker¶
memory feature 下的 MemoryBroker 是一个完整的进程内 Broker。当队列只属于单个应用而不属于网络
时,选它即可。默认的 cargo generate 模板(templates/memory)就建立在它之上,因此新建的项目没有
外部依赖就能运行。
它保留多少¶
MemoryBroker::new() 建出的 Broker 什么都不保留。一条消息从发布活到最后一个订阅者读完为止,因此
长期运行的服务占用的内存只是处理器还没做完的那部分工作,与消息总数无关。
需要回放的服务要保留历史,并说明保留多少:
// Replaying reads what the broker kept, so this one keeps the last 64 entries of every log.
// `MemoryBroker::new()` keeps nothing, and a mount that seeks does not compile on it.
let broker = MemoryBroker::retaining(Retention::Messages(nonzero!(64)));
Retention 约束的是单个主题:Messages(n) 为每个主题保留最新的 n 条消息,Bytes(n) 保留能装进
n 字节的最新负载,MessagesAndBytes { .. } 同时应用两个上限。因此,向一千个主题发布的 Broker
会为每个主题各保留这么多。最新的一条消息始终留下,所以比字节上限还宽的负载会被单独保留,而不是在
发布时就丢掉。
两种形态是两个类型:MemoryBroker::retaining(..) 给出订阅可 Seekable 的那一个,从某个位置开始
读取、或者读取定位句柄的挂载只在它之上才能编译。在不保留日志的 Broker 上回放是编译错误,而不是一次
悄悄什么也找不到的回放。
挂载点导入的 prelude¶
ruststream::memory::prelude 是该 Broker 的 glob,形状和每个 Broker crate 的 prelude 一样。它先
重导出核心 prelude,然后是该 Broker 自己的表面(MemoryBroker、MemorySource、MemoryError、
MemoryPosition、Retention 与日志模式 Discarding / Retaining,以及上下文键 MemoryContext /
MemoryBatchContext / Position /
SeekHandle),最后是统一名字下的发布策略:Publish、TransactionalPublish 和 Request。
这三个名字是 MemoryPublish 和 MemoryRequest 的别名。该 Broker 的发布者实现了两种事务,因此
TransactionalPublish 在这里就是 Publish 那一个策略;事务配置独立的 Broker 会把该名字指向
另一个策略。
同一个 glob 还会把该 Broker 实现的能力 trait 带进作用域:TransactionalPublisher、
OwnedTransactions、Transaction、RequestReply、Positioned 和 Seeker。它们带来的操作,在
策略所在的地方同样可用。Partitioned 不在其中:它一旦进入作用域,msg.partition_key() 就会和
IncomingMessage 的同名方法产生歧义。读取分区键的服务自行导入 Partitioned。
处理器主体保留 use ruststream::prelude::*;:它写的是能力而不是策略,并不知道哪个 Broker 在运行
它。主体和挂载点写在同一个文件里时,一个 Broker glob 就够了。
语义¶
- 主题名精确匹配。 对
orders的订阅会收到发布到orders的消息。 - 投递给全部订阅者。 某个主题的每个订阅者,都会收到订阅之后发布到该主题的每条消息。
- ack 是空操作。 带
requeue: true的 nack 把同一份载荷重新投递给同一个订阅者。 retry_after由 Broker 自己完成。 延迟过后,这条投递回到同一个订阅者,其间不会重新发布任何副本。- 投递会计数。 每条投递都会报告 Broker 已经把这条消息交给该订阅者多少次,第一次投递也算在内。
因此注册上的
max_attempts(n)上限消耗在这些重投上,用尽上限的那条投递进入dead_letter(name)指定的目的地。 - 共享所有权。
MemoryBroker是引用计数的句柄,所有副本共用同一份状态。因此测试持有的副本, 能看到应用发布的一切。
处理器、中间件和解码在这里的行为,与在网络 Broker 上一致:运行时用同一条路径分发消息。
能力¶
每个能力 trait 都实现在该 Broker 自己的进程内语义之上:
- 请求-响应。
broker.requester()给出MemoryRequester。它的request发布消息,并在reply-to消息头里写上一个唯一的进程内响应主题。第一条消息投递到该主题时,request完成。 响应方从请求里读出reply-to,把回复发布到该主题。没有人应答的请求返回RequestError::Timeout错误。MemoryRequest策略构造MemoryRequester,因此带Out<impl RequestReply, ..>约束的槽位,你绑定到MemoryRequest。 - 批。
MemorySubscriber实现了BatchSubscriber:一个批是第一条到达的投递,加上此时已经 缓冲的全部消息。上限是注册处理器时用batch(n)给出的大小。未满的批也会立即投递。 - 事务。
MemoryPublish策略构造MemoryPublisher,它实现了两种事务。因此带TransactionalPublisher或OwnedTransactions约束的槽位或接线,你绑定到MemoryPublish。 事务作用域内的发布先进入缓冲:commit按发布顺序把它们一起投递给全部订阅者,abort把它们 丢弃。每个拥有式事务各自缓冲,发布者句柄的副本之间不共享事务。在发布者上乱序调用会返回MemoryError:事务已经打开时再次begin_transaction返回TransactionBusy,已打开的事务不受 影响;没有事务时commit或abort返回NoTransaction。 - 分区键。
MemoryMessage实现了Partitioned,从partition-key消息头 (memory::PARTITION_KEY_HEADER)读取键。 - 日志定位。 在保留日志的 Broker 上,
MemorySubscriber在每个主题的日志之上实现 了Seekable:开始读取之前先取得MemorySeeker,再对某个MemoryPosition调用seek。位置可以 从已投递的消息上取得(Positioned::position,此时那一条消息会重新投递),也可以直接构造 (MemoryPosition::start()/sequence(n)/end())。序号是绝对的:保留上限淘汰更旧的消息时, 同一个序号仍然指同一条消息。start()是仍然保留着的最旧一条,end()是日志末尾,在此前发布的全部 消息之后。向前定位会跳过排在目标之前的投递。定位到已被上限淘汰的序号,返回MemoryError::PositionEvicted错误,并报告仍然保留的最旧位置。定位作用在单个订阅者实例上。通过 已停止总线的句柄定位,返回MemoryError::ShutDown错误。在应用内部,MemoryContext里有消息的位置和MemorySeeker,处理器按Position和SeekHandle两个键读取(参见定位)。批量处理器读的是MemoryBatchContext:那里有SeekHandle,但没有Position,因为一个批横跨多次投递。 - 关闭。
MemoryBroker::connect(self)给出ConnectedMemoryBroker。它的shutdown消费自身 并返回ClosedMemoryBroker,报告这次关闭丢弃了多少个订阅者注册。关闭之后,通过先前发出的句柄 发布消息、提交事务或发起请求,都返回MemoryError::ShutDown或RequestError::ShutDown错误。
订阅来源¶
ConnectedMemoryBroker 实现了 Subscribe,因此 #[subscriber("orders")] 可以直接使用。同一个
订阅也可以用 MemorySource 描述符写出,形式和任何 Broker 描述订阅时一样。下面取自
routed_service 示例:
use ruststream::memory::prelude::*;
#[subscriber(MemorySource::new("orders"), publish("confirmations"))]
pub(crate) async fn confirm(
order: &Order,
ctx: &mut Context<'_, (), Repository>,
) -> Result<Confirmation, HandlerOutcome> {
let repo = ctx.state();
tracing::debug!(
order = order.id,
customer = %order.customer,
item = %order.item,
"confirming order"
);
match repo.record_order(order.id).await {
Ok(()) => Ok(Confirmation {
order_id: order.id,
accepted: order.quantity > 0,
}),
Err(e) if e.is_transient() => {
tracing::warn!(order = order.id, "store busy, asking for redelivery");
Err(HandlerOutcome::retry())
}
Err(e) => {
tracing::error!(order = order.id, error = %e, "dropping order");
Err(HandlerOutcome::drop())
}
}
}
use ruststream::memory::prelude::*;
struct Confirm;
// The state is named on the body, not on a definition: this one reads a `Repository`, so it is a
// `Handle` for that state alone and mounts only on an application that carries it.
impl Handle<Order, Confirmation, (), (), Repository> for Confirm {
async fn handle(
&self,
order: &Order,
_outs: &(),
ctx: &mut Context<'_, (), Repository>,
) -> Result<Confirmation, HandlerOutcome> {
let repo = ctx.state();
tracing::debug!(
order = order.id,
customer = %order.customer,
item = %order.item,
"confirming order"
);
match repo.record_order(order.id).await {
Ok(()) => Ok(Confirmation {
order_id: order.id,
accepted: order.quantity > 0,
}),
Err(e) if e.is_transient() => {
tracing::warn!(order = order.id, "store busy, asking for redelivery");
Err(HandlerOutcome::retry())
}
Err(e) => {
tracing::error!(order = order.id, error = %e, "dropping order");
Err(HandlerOutcome::drop())
}
}
}
}
/// The mount, and the whole declaration the attribute's clauses carried: the broker's own
/// descriptor as the source, `.to(..)` for the reply channel, and `.describe(..)` for the sentence
/// the attribute lifts off the handler's doc comment. Who publishes the reply is wiring rather
/// than declaration, so it lives on the mount chain on both paths: `.out_reply(Publish)` names
/// the position the returned value leaves through and the policy that carries it, which pairs with
/// the connected broker at startup and encodes with the default codec. The definition says what it
/// replies with and where; the chain says who sends it.
fn confirm_route() -> impl RouterDef<MemoryBroker, Repository> {
Router::<MemoryBroker>::new()
.include(
subscriber(MemorySource::new("orders"), Confirm)
.reply()
.to("confirmations")
.describe("Confirms an order and replies on `confirmations`.")
.build(),
)
.out_reply(Publish)
.build()
}
用于测试¶
MemoryBroker 上的应用,你用 TestApp 套件来测试:构建应用,交给
TestApp::start,发布消息,然后断言处理器收到了什么、发布了什么。完整用法参见
测试。
在一次测试运行期间,套件会记录服务发布的每一条消息,因此无论应用建立在哪一种形态的 Broker 之上,
published::<T>(..) 断言读到的都是同一份列表。在套件之外,通过 TestableBroker::published 读回
日志,看到的就是那个 Broker 保留的内容:保留日志的那个给出上限之内的全部消息,默认的那个什么都
没有。