34 个主要重大版本发布
| 40.0.0 | 2024年7月18日 |
|---|---|
| 39.0.0 | 2024年7月12日 |
| 38.0.0 | 2024年6月24日 |
| 37.0.0 | 2024年5月23日 |
| 0.0.0 | 2022年12月8日 |
#1040 in 神奇豆
6,278 每月下载
在 77 个 crate 中使用(77 个直接使用)
3MB
53K SLoC
通用消息队列 pallet
为任意用例提供基于队列的消息队列和处理的通用功能。
设计目标
- 对
Message和MessageOrigin的假设最小。两者都应该是 MEL 约束的 blob。这确保了 pallet 的通用性和可重用性。 - 已知且严格限制的预调度 PoV 重量,尤其是对于消息执行。这对于 pallet 的成功至关重要,因为消息执行是在
on_initialize中完成的,该操作必须 永不 低估其 PoV 重量。它还需要一个节俭的 PoV 足迹,因为 PoV 很稀缺,这可能在每块中(可能)完成。在不可预测的消息大小分布的情况下,这也必须成立。 - 可作为 XCMP、DMP 和 UMP 消息/调度队列使用 - 可能通过适配器类型。
设计
该托盘具备入队、存储和处理消息的功能。这是通过拥有队列来实现的,这些队列用于存储入队消息,并且可以被服务来处理这些消息。队列通过其在BookStateFor中的起源来识别。每条消息都有一个起源,定义了它将被存储到哪个队列中。消息通过附加到一本书的最后一个Page来存储。每本书通过索引Pages来跟踪其页面。ReadyRing包含所有至少包含一条未处理消息的队列,因此它们是就绪的,可以提供服务。ServiceHead指示哪个就绪队列是下一个要服务的。该托盘实现了frame_support::traits::EnqueueMessage、frame_support::traits::ServiceQueues,并提供frame_support::traits::ProcessMessage和OnQueueChanged钩子来与外界通信。
注意:由于它们不是公开的,因此存储项目之间没有链接。
消息执行
执行消息的工作被卸载到Config::MessageProcessor,其中包含处理这些消息的实际逻辑,因为它们是二进制文件。在发生错误时,存储更改不会被回滚。
失败的消息可能会暂时或永久超重。该托盘将不断尝试执行暂时超重的消息。永久超重的消息将被跳过,必须手动执行。
重入性
此托盘有两个执行(可能递归的)逻辑的入口点:Pallet::service_queues和Pallet::execute_overweight。这两个入口点都由相同的互斥锁保护,以防止重入错误。唯一明确允许由消息处理器调用的函数是:Pallet::enqueue_message和Pallet::enqueue_messages。所有其他函数都是禁止的,并使用Error::RecursiveDisallowed错误。
分页
队列以分页方式存储,通过将消息分割成Page来实现。这种方式在实现组件时增加了许多复杂性,但为了实现第二个#设计目标,这是完全必要的。问题在于消息可能非常大,比如64KiB。这将导致至少64KiB的MEL,进而产生至少64KiB的PoV。现在我们假设大多数消息的长度都远小于其最大允许长度。这将导致大多数消息在预派发PoV大小上比其派发后PoV大小大得多,可能是千倍之多。忽视这一观察将削弱组件的处理能力,因为它无法在运行时解决这个问题。从概念上讲,实现方法是尽可能将多个消息打包到一个有界vec中,使其实际适应边界。这减少了浪费的PoV。
页面数据布局
一个页面包含一个堆,用于存储所有消息。堆是通过连接(ItemHeader, Message)对来构建的。其中ItemHeader包含消息的长度,这是检索消息所必需的。这种布局允许以恒定时间访问下一个消息,以及线性时间访问页面中的任何消息。头部必须尽可能小,以减少其PoV影响。
权重计量
组件使用sp_weights::WeightMeter来手动跟踪其消耗,以确保始终保持在所需限制内。这意味着消息处理器钩子可以在不执行消息的情况下计算其权重。这限制了可能的用例,但由于组件在on_initialize中运行,该操作具有硬权重限制,这是必要的。《权重计量器》以can_accrue和check_accrue始终用于在提交之前检查操作的剩余权重。由于权重不足而退出称为“弃权”。
场景:消息入队
通过frame_support::traits::EnqueueMessage::enqueue_message(m, o)将消息m入队到来源o的队列Q[o]。
首先,如果队列存在,则加载队列,否则以默认空值创建队列。然后将消息插入队列,通过将其附加到其最后一个Page,或者如果它不适应那里,则为m创建一个新的Page。在《Book》中的消息数量增加。
Q[o]现在就绪,这将最终导致m被处理。
场景:消息处理
货架在每个块中运行 on_initialize 或通过手动调用 frame_support::traits::ServiceQueues::service_queues 时运行。
首先,它尝试通过将 ServiceHead 前进到下一个 就绪 队列来“旋转” ReadyRing。然后,它开始服务此队列,尽可能多地服务它的页面。服务一个页面意味着尽可能多地执行其中的消息。如果 Config::MessageProcessor 返回 Ok,则每个执行的消息都被标记为 已处理。随后发出一个 Event::Processed 事件。货架的权重限制可能永远不允许执行特定的消息。在这种情况下,它将保持未处理状态并被跳过。如果队列中不再有消息,或者剩余的权重不足以服务此队列,则此过程停止。如果权重足够,它将尝试前进到下一个 就绪 队列并服务它。这个过程会一直持续到没有更多的队列可以取得进展或者权重不足以进行检查。
场景:过重执行
被消息处理跳过的永久过重消息永远不会通过 on_initialize 自动执行,也不会通过调用 frame_support::traits::ServiceQueues::service_queues 执行。
需要通过 frame_support::traits::ServiceQueues::execute_overweight 的形式进行人工干预。过重消息发出一个 Event::OverweightEnqueued 事件,该事件可以用于提取手动执行的参数。这只适用于永久过重消息。没有保证这会工作,因为消息可能是旧页面的部分,在执行开始之前就被回收了。
术语
Message:一个数据块,该模块没有内省权限,定义为 [BoundedSlice<u8, MaxMessageLenOf<T>>]。消息长度由MaxMessageLenOf限制,该值从Config::HeapSize和 [ItemHeader::max_encoded_len()] 计算得出。MessageOrigin:消息的泛型来源,定义为MessageOriginOf。对它的要求保持最小,以保持尽可能的泛型。该类型定义在frame_support::traits::ProcessMessage::Origin。Page:一系列Message,见Page。不能为空。Book:一系列Page,见BookState。可以为空。Queue:一个Book和一个MessageOrigin,这可以成为ReadyRing的一部分。可以为空。ReadyRing:一个双链表,包含所有就绪的Queue。它通过其ready_neighbours字段将队列链接起来。如果一个Queue包含至少一个可以处理的Message,则该队列是就绪的。可以为空。ServiceHead:指向ReadyRing中下一个要服务的Queue的指针。- (
un)processed:在由模块执行后,消息被标记为已处理。一个尚未执行或无法执行的消息保持为unprocessed,这是消息入队后的默认状态。 knitting/unknitting:将Queue添加到或从ReadyRing中移除的方法。MEL:类型的最大编码长度,见codec::MaxEncodedLen。Reentrance:在完成之前再次进入执行上下文。
属性
活动性 - 入队
总是可以入队任何MessageOrigin的消息。
活动性 - 处理
on_initialize始终尊重其有限的权重限制。
进展 - 入队
入队的消息立即变为未处理,因此有资格执行。
进展 - 处理
如果有的话,模块将在每个块中至少执行一条未处理的消息。确保这一属性需要仔细考虑具体的权重,因为on_initialize的权重限制可能永远不允许执行任何消息;如果限制设置为零,则显然如此。integrity_test可用于确保此属性。
公平性 - 入队
为特定的MessageOrigin入队消息不会影响入队同一MessageOrigin或任何其他MessageOrigin消息的能力;由活动性 - 入队保证。
公平性 - 处理
如果队列数量恒定,则每个队列的消息处理可用重量相同。因此,创建新队列可能从经济角度来说是昂贵的。目前,通过每个并行链/线程有一个队列来实现这一点,这保持了队列数量的 O(n),应该“足够好”。
依赖项
~17–32MB
~544K SLoC