#可靠性 #Erlang #异步 #监督器 #监督

async-backplane

针对 Rust Futures 的简单、受 Erlang 启发的可靠性框架

3 个不稳定版本

0.1.1 2021 年 7 月 7 日
0.1.0 2020 年 8 月 1 日
0.0.0 2020 年 7 月 22 日

#5#监督

MPL-2.0 许可证

43KB
539

async-backplane

License Package Documentation

简单、受 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
  • 调用了 Devicedisconnect() 方法。

一旦 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>>,其中 TC 是异步块返回的 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执行器。

完成时,这些将会

性能

我没有花太多时间开发基准测试,如果你真的很关心,你应该自己进行测试。

以下是我的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