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