3 个不稳定版本
0.1.1 | 2021 年 7 月 7 日 |
---|---|
0.1.0 | 2020 年 8 月 1 日 |
0.0.0 | 2020 年 7 月 22 日 |
#5 在 #监督
43KB
539 行
async-backplane
简单、受 Erlang 启发的 Rust Futures 容错框架。
特性
- Erlang 传奇可靠性的秘密。
- 惯用的 Rust API,具有低级控制。
- 简单。易于学习和使用。
- 与现有的 Futures 生态系统兼容
- 不使用不稳定特性或不安全代码。
- 高性能和(相对)低内存
- 轻量级:约 600 行代码,6 个依赖项,新鲜构建只需几秒。
- 没有
Box<dyn Any>
,LOL。
状态。
我们相信一切都能正常工作,但我们仍然太新,不能确定。API 在 1.0 之前可能会略有变化,但不会有太大变化,我希望。
请注意,这是一个比大多数声称受 Erlang 启发的大多数库更具通用性和更低级别的工具。计划其他库将提供更高级别的体验。我正在做一些工作,这些工作将很快推出
指南
简介
Backplane(这是“主板”的时髦说法)是一张动态的设备网格——表示背板存在的所有者对象。当删除一个设备或调用其 disconnect()
方法时,其他已选择了解其情况的设备将会收到通知。
所有以 Erlang 风格实现的可靠性都源于这一能力,即通知您依赖项的故障。这是构建更高级概念(如著名的监督器)的基础级工具。
创建一个 Device
很简单
use async_backplane::Device;
fn device() -> Device { Device::new() }
Device
是什么?在背板中拥有存在意味着什么?
- 我们维护一个要通知的
Devices
列表。 - 当我们
disconnect
时,我们将通知这些设备。
断开连接有两个触发器
- 删除了
Device
。 - 调用了
Device
的disconnect()
方法。
一旦 Device
断开连接,就无法再使用它。不再链接,不再发送消息,结束了。
Device
是一个 futures Stream
,并可以轮询 Message
。消息是以下两种事物之一
- 请求关闭,带有请求者的
DeviceID
。 - 一条通知,表明另一台
设备
已断开连接。这包含断开连接设备的DeviceID
以及一个描述断开连接原因的选项<故障>
。
以下是在异步函数中轮询它的一个示例
use async_backplane::{Device, Message};
use futures_lite::StreamExt; // for `.next()` on Stream
async fn next_message(device: &mut Device) -> Option<Message> {
device.next().await
}
如果有要监听的内容,这将非常有用,这正是链接的作用所在!
链接
链接是我们配置设备在断开连接(断开或在其上调用 .disconnect()
)时相互通知的方式。有三种链接模式(LinkMode
)
Monitor
- 当其他设备断开连接时被通知。Notify
- 当此设备断开连接时通知其他设备。Peer
- 当它们断开连接时相互通知。
如果您有一对设备(例如,当您正在创建新设备时),链接就非常简单了
use async_backplane::Device;
// `l` will be notified when `r` disconnects
fn monitor(l: &Device, r: &Device) { l.link(r, LinkMode::Monitor); }
// `r` will be notified when `l` disconnects
fn notify(l: &Device, r: &Device) { l.link(r, LinkMode::Notify); }
// `l` will be notified when `r` disconnects
// `r` will be notified when `l` disconnects
fn peer(l: &Device, r: &Device) { l.link(r, LinkMode::Peer); }
现在我们有了要监听的内容,让我们持续重启一个失败的任务,永远如此
use async_backplane::*;
use futures_lite::StreamExt; // for `.next()` on Stream
use smol::Task; // just a small and simple futures executor
async fn never_stop<F: Fn(Device)>(mut device: Device, spawn: F) {
loop { /// We want to go forever
let d = Device::new();
device.link(&d, LinkMode::Monitor);
spawn(d);
while let Some(message) = device.next().await {
match message {
Message::Shutdown(id) => (), // ignore!
Message::Disconnected(_id, _fault) => { break; } // restart!
}
}
}
}
/// This is quite obviously not going to succeed. Maybe yours should!
fn failing_task(device: Device) {
smol::Task::spawn(async {
device.disconnect(Some(Fault::Error(())))
}).detach();
}
fn main() {
never_stop(failing_task)
}
从某种意义上说,我们刚刚编写了我们的第一个监视器!一个名为 async-supervisor 的新 crate 即将推出,其中包含类似 Erlang 风格的监视器。
受管理设备
尽管所有这些对如何响应退出的低级控制令人兴奋,但如果我们认真对待 Erlang 模型,我们通常将其留给监视器,而我们的大部分任务都不是监视器。
非监视器任务只想继续自己的工作。这意味着如果它们监视的任何设备因 故障
而断开连接,它们也会想以 故障
断开连接。在这种情况下,链接是 设备
之间的 依赖关系图(这些设备是使用这些 设备
的计算的代理)。
我们称这种非常常见的场景为 管理模式。可以通过 Device.manage()
方法访问它
use async_backplane::*;
use smol::Task;
fn example() {
let device = Device::new();
Task::spawn(async move {
device.manage(async { Ok(()) }); // Succeed!
}).detach();
}
这里有三个逻辑步骤
- 创建设备(
Device::new()
)。 - 在执行器上创建一个 Future(
Task::spawn(...).detach()
)。 - 在创建的 Future 中,使用异步块执行设备的管理模式(
device.manage(async { Ok(()) })
)。
提供给 Device.manage()
的异步块应该返回某种类型的 Result
。如果您返回 Ok
,则设备将被认为无故障完成。如果您返回 Err
,则设备将被认为出现了故障。
受管理设备将一直运行,直到以下第一个发生
- 提供的 future/async 块返回结果。
- 提供的 future/async 块发生 panic。
- 设备向我们发送消息
- 收到关闭请求时成功完成。
- 收到一个致命的断开连接通知时,发生故障。
通过调用 .manage()
,您将永久放弃对设备的所有权。当上述情况之一发生时,任何正在监视我们的设备都将被通知。
manage()
方法返回一个 Result<T, Crash<C>>
,其中 T
和 C
是异步块返回的 Result<T,C>
的成功和错误类型。Crash
只是一个包含各种失败类型的枚举。它包含关于出了什么问题的详细信息,而任何关于我们断开连接的 通知 只包含基本信息。
我仍在努力弄清楚如何处理崩溃。我不想这个库太有意见或使依赖关系树过于庞大。也许我会创建一个有意见的库来使用这个库,或者也许你只需在每个项目中创建自己的 manage_panic()
函数并使用它?欢迎提出建议!
动态链路拓扑
通常,我们希望使用 Device.manage()
来获取自动管理行为,但我们还希望将新设备链接到这项工作中。但是 manage()
永久地拥有 Device
,所以我们该怎么办?
Line
是对 Device
的一个可克隆引用,类似于 Arc
(实际上包含一个)。问题是由于 Line
不是拥有者,您尝试使用它时,它所引用的 Device
可能已经断开连接,因此链接可能会失败
use async_backplane::*;
fn example() {
let a = Device::new();
let b = Device::new();
let line = b.line();
a.link_line(line, LinkMode::Monitor) // suspiciously like `.link()`...
.unwrap(); // b clearly did not disconnect yet
// ... spawn both ...
}
注意,link_line()
消耗了 Line
。这是因为内部,可通知的 Device
列表实际上是一个 Line
列表,因此我们避免了您不再需要 Line
时的克隆。
您还可以直接在 Lines
之间进行链接,因为 Line
也具有 link_line()
方法
use async_backplane::*;
fn demo() {
let a = Device::new();
let b = Device::new();
let c = Device::new();
let c2 = c.line();
let d = Device::new();
let d2 = d.line();
a.link(&b, LinkMode::Peer); // Device-Device link
b.link_line(c2, LinkMode::Peer).unwrap(); // Device-Line link
c2.link_line(d2, LinkMode::Peer).unwrap(); // Line-Line link
// ... now go spawn them all ...
}
在您使用 Device.manage()
动态链接时,您应该首先创建一个 Line
。
关于动态拓扑的注意事项
一旦您通过 Line
与某物建立了链接,您应仅通过 Line
断开链接。设备到设备的链接非常快,因为它避免了处理此情况所需的工作。通常,您只有在知道您之前没有与相应的 Line
链接时,才应使用 Device
进行链接或断开链接。
Erlang/OTP 的区别
虽然我深受 Erlang 和 OTP 原则的启发,但 Rust 和 Erlang 之间有一些阻抗不匹配,特别是在所有权与垃圾回收方面。因此,backplane 是对 Rust “感觉正确”的原则的适应。
经过几个月的研发,最终变成了一个级别较低的工具,该工具力求不过度推销和带有主观意见,并且非常小巧。
以下是一些显著的差异
设备和逻辑的分离
在Erlang中,当你想要创建一个进程时,你需要提供一个0参数的函数。默认情况下,它的工作方式基本上与Device.manage()
相同,但不需要转让所有权。
在backplane中,我不想强迫你选择执行器或执行策略,因此创建一个Device
完全独立于将要使用它的Future的创建,这是出于必要的。
这意味着虽然大多数代码会调用Device.manage()
,但你完全自由地实现任何你想要的逻辑,并将Device
存储在你想要的位置。
设备和邮箱的分离
在Erlang中,发送给进程的所有消息都通过同一个渠道(邮箱)。在某种程度上,设备确实有一个邮箱,但它只具有严格有限的效用。与Device
无关的Device
不处理任何消息,而Erlang的消息可以是任何内容。为了与使用Device
的任务交换通用消息,你需要例如打开一个async-channel
通道。
常见问题解答
为什么是Erlang?
作者过去几年一直是一名Elixir程序员,并深刻理解Erlang的可靠性原则,Elixir就是基于这个原则。最重要的是,我珍视简洁性。整个系统足够简单,以至于可以大规模地进行推理。
其他人没有尝试过这个吗?为什么要重新发明轮子?
这很大程度上是品味问题。我认为任何现有的解决方案都没有真正捕捉到Erlang可靠性的本质,或者感受到其本质的美。人们似乎太过于纠缠于演员和监督,而忽略了基本的东西。
现有的解决方案也往往是大而复杂的,难以学习和推理,并引入了大量依赖项。对我来说,Erlang的整个点在于它使得并发和依赖变得如此简单,以至于可以大规模地进行推理。但恐怕我们又在讨论品味了。
我还应要求在Reddit上提供了一个与bastion的具体比较。这只是我的观点,其他人也有。
库配对建议
这些与async-backplane
配合使用效果很好
- async-oneshot - 一个快速、小巧、功能齐全、与no-std兼容的单次发送通道库。
- async-channel - 优秀的通用异步感知MPMC通道。
- smol - 小型、高性能的多线程Futures执行器。
完成时,这些将会
- async-supervisor - 为async-backplane提供的Erlang风格的监督器。
- async-oneshot-local -
async-oneshot
的单线程伴侣。
性能
我没有花太多时间开发基准测试,如果你真的很关心,你应该自己进行测试。
以下是我的Ryzen 3900X上的数据
Running target/release/deps/device-90347ed9496e0aaa
running 11 tests
test create_destroy ... bench: 231 ns/iter (+/- 3)
test device_monitor_drop ... bench: 526 ns/iter (+/- 11)
test device_monitor_drop_notify ... bench: 604 ns/iter (+/- 12)
test device_monitor_error_notify ... bench: 632 ns/iter (+/- 9)
test device_peer_drop_notify ... bench: 659 ns/iter (+/- 10)
test device_peer_error_notify ... bench: 671 ns/iter (+/- 10)
test line_monitor_drop ... bench: 634 ns/iter (+/- 11)
test line_monitor_drop_notify ... bench: 687 ns/iter (+/- 11)
test line_monitor_error_notify ... bench: 717 ns/iter (+/- 8)
test line_peer_drop_notify ... bench: 764 ns/iter (+/- 14)
test line_peer_error_notify ... bench: 778 ns/iter (+/- 9)
test result: ok. 0 passed; 0 failed; 0 ignored; 11 measured; 0 filtered out
Running target/release/deps/line-750db620e6752c99
running 6 tests
test create_destroy ... bench: 8 ns/iter (+/- 0)
test line_monitor_drop ... bench: 637 ns/iter (+/- 8)
test line_monitor_drop_notify ... bench: 670 ns/iter (+/- 11)
test line_monitor_error_notify ... bench: 698 ns/iter (+/- 8)
test line_peer_drop_notify ... bench: 843 ns/iter (+/- 7)
test line_peer_error_notify ... bench: 917 ns/iter (+/- 11)
它仍然在我的旧2015年MacBook Pro上表现良好
Running target/release/deps/device-8add01b9803770b5
running 11 tests
test create_destroy ... bench: 212 ns/iter (+/- 9)
test device_monitor_drop ... bench: 585 ns/iter (+/- 64)
test device_monitor_drop_notify ... bench: 771 ns/iter (+/- 39)
test device_monitor_error_notify ... bench: 798 ns/iter (+/- 39)
test device_peer_drop_notify ... bench: 964 ns/iter (+/- 40)
test device_peer_error_notify ... bench: 941 ns/iter (+/- 304)
test line_monitor_drop ... bench: 805 ns/iter (+/- 48)
test line_monitor_drop_notify ... bench: 975 ns/iter (+/- 48)
test line_monitor_error_notify ... bench: 993 ns/iter (+/- 55)
test line_peer_drop_notify ... bench: 1,090 ns/iter (+/- 62)
test line_peer_error_notify ... bench: 1,181 ns/iter (+/- 65)
test result: ok. 0 passed; 0 failed; 0 ignored; 11 measured; 0 filtered out
Running target/release/deps/line-c87021ef05fddd66
running 6 tests
test create_destroy ... bench: 13 ns/iter (+/- 4)
test line_monitor_drop ... bench: 793 ns/iter (+/- 51)
test line_monitor_drop_notify ... bench: 968 ns/iter (+/- 357)
test line_monitor_error_notify ... bench: 1,018 ns/iter (+/- 54)
test line_peer_drop_notify ... bench: 1,343 ns/iter (+/- 70)
test line_peer_error_notify ... bench: 1,370 ns/iter (+/- 77)
请注意,在链接时,使用Device比使用Line更便宜,也就是说
device.link()
是最快的。device.link_line()
的成本略高。line.link_line()
的成本更高一些。
如果性能真的非常重要,请始终将设备与设备相连。还应该花一些时间优化这个库,因为我们还没有。
即将到来的工作
- no_std 支持。
- 演员。也许。
变更日志
v0.1.1
- 修复了
Crash.is_completed
我还修复了 clippy 检查的 lint 并稍微调整了一些代码。
版权和许可证
版权所有(c)2020 James Laver,async-backplane 贡献者
此源代码形式受 Mozilla 公共许可证第 2.0 版的条款约束。如果未与此文件一起分发 Mozilla 公共许可证(MPL)副本,您可以在 http://mozilla.org/MPL/2.0/ 获取一个副本。
依赖关系
~780KB
~13K SLoC