27个版本

0.10.3 2024年5月24日
0.9.8 2024年5月2日
0.9.7 2024年3月16日
0.9.3 2023年10月13日
0.7.5 2023年3月24日

#60 in #actor-framework

Download history 23/week @ 2024-05-04 10/week @ 2024-05-11 589/week @ 2024-05-18 119/week @ 2024-05-25 44/week @ 2024-06-01 14/week @ 2024-06-08 81/week @ 2024-06-15 64/week @ 2024-06-22 46/week @ 2024-06-29 105/week @ 2024-07-06 115/week @ 2024-07-13 41/week @ 2024-07-20 60/week @ 2024-07-27 22/week @ 2024-08-03 24/week @ 2024-08-10 118/week @ 2024-08-17

226 monthly downloads
ractor_cluster中使用

MIT许可证

36KB
344

ractor

发音为 ract-er

一个纯Rust actor框架。受Erlang的gen_server启发,兼具Rust的速度和性能!

  • github
  • crates.io
  • docs.rs
  • docs.rs
  • CI/main
  • codecov
  • ractor: ractor 下载
  • ractor_cluster: ractor_cluster 下载

网站 Ractor有一个配套网站,提供更详细的入门指南和一些最佳实践,并定期更新。API文档仍可在docs.rs找到,但这将是ractor的补充网站。试试看!https://slawlor.github.io/ractor/

关于

ractor试图解决在Rust中构建和维护类似Erlang的actor框架的问题。它提供了一套通用原语,并帮助我们自动化监督树和actor的管理,以及传统的actor消息处理逻辑。它最初是为了使用tokio运行时而设计的,但现在也支持async-std运行时。

ractor是一个100%用Rust编写的现代actor框架。

此外,ractor还有一个配套库,即ractor_cluster,它对于在分布式(类似集群)场景中部署ractor是必需的。ractor_cluster不应被视为生产就绪,但它相对稳定,我们非常欢迎您的反馈!

为什么选择ractor?

除了Rust语言编写的其他actor框架(如Actixriker,或者Tokio中的actor)之外,还有一些类似的框架列在这个Reddit帖子上。

Ractor试图通过更多地向纯Erlang的gen_server模型靠拢来实现差异化。这意味着每个actor也可以简单地成为其他actor的supervisor,而无需额外的成本(只需将它们链接在一起即可!)此外,我们还在努力保持与Erlang模式紧密的逻辑,因为它们在实际应用中表现良好并且得到充分利用。

此外,我们编写了ractor,没有在需要spawn的某种“运行时”或“系统”上构建。actor可以独立运行,与其他基本的tokio运行时结合使用,而无需额外的开销。

目前我们完全支持以下功能

  1. 单线程消息处理
  2. actor监督树
  3. rpc模块中对actor的远程过程调用
  4. time模块中的计时器
  5. 命名actor注册表(registry模块),来自Erlang的Registered processes
  6. 进程组(ractor::pg模块),来自Erlang的pg模块

我们的路线图包括添加更多Erlang功能,包括可能的一个分布式actor集群。

性能

ractor中的actor通常非常轻量级,并有一些基准测试,您可以在自己的主机系统上运行

cargo bench -p ractor

安装

通过将以下内容添加到Cargo.toml依赖中安装ractor

[dependencies]
ractor = "0.10"

ractor的最低支持Rust版本(MSRV)是1.64。但是,为了利用特性中的本地async fn支持,而不依赖于async-trait crate的desugaring功能,您需要使用Rust版本>= 1.75。特性中的async fn最近已经添加

功能

ractor公开以下功能

  1. cluster,它公开了设置和管理actor网络链接集群所需的各项功能。这是一个正在进行中的项目,在#16中跟踪。
  2. async-std,它启用了使用async-std的异步运行时,而不是tokio运行时。但是,带有sync功能的tokio仍然是一个依赖项,因为我们利用了来自tokio的消息同步原语,而不管运行时如何,因为它们并不特定于tokio运行时。这项工作在#173中跟踪。您可以删除默认功能以“最小化”tokio依赖性,仅限于同步原语。

与actor一起工作

ractor 中的演员非常轻量,可以被视为线程安全的。每个演员一次只会调用其处理函数中的一个,并且它们永远不会并行执行。遵循演员模型可以导致具有良好定义的状态和处理逻辑的微服务。

以下是一个示例 ping-pong 演员可能如下所示

use ractor::{cast, Actor, ActorProcessingErr, ActorRef};

/// [PingPong] is a basic actor that will print
/// ping..pong.. repeatedly until some exit
/// condition is met (a counter hits 10). Then
/// it will exit
pub struct PingPong;

/// This is the types of message [PingPong] supports
#[derive(Debug, Clone)]
pub enum Message {
    Ping,
    Pong,
}

impl Message {
    // retrieve the next message in the sequence
    fn next(&self) -> Self {
        match self {
            Self::Ping => Self::Pong,
            Self::Pong => Self::Ping,
        }
    }
    // print out this message
    fn print(&self) {
        match self {
            Self::Ping => print!("ping.."),
            Self::Pong => print!("pong.."),
        }
    }
}

// the implementation of our actor's "logic"
impl Actor for PingPong {
    // An actor has a message type
    type Msg = Message;
    // and (optionally) internal state
    type State = u8;
    // Startup initialization args
    type Arguments = ();

    // Initially we need to create our state, and potentially
    // start some internal processing (by posting a message for
    // example)
    async fn pre_start(
        &self,
        myself: ActorRef<Self::Msg>,
        _: (),
    ) -> Result<Self::State, ActorProcessingErr> {
        // startup the event processing
        cast!(myself, Message::Ping)?;
        // create the initial state
        Ok(0u8)
    }

    // This is our main message handler
    async fn handle(
        &self,
        myself: ActorRef<Self::Msg>,
        message: Self::Msg,
        state: &mut Self::State,
    ) -> Result<(), ActorProcessingErr> {
        if *state < 10u8 {
            message.print();
            cast!(myself, message.next())?;
            *state += 1;
        } else {
            println!();
            myself.stop(None);
            // don't send another message, rather stop the agent after 10 iterations
        }
        Ok(())
    }
}

#[tokio::main]
async fn main() {
    let (_actor, handle) = Actor::spawn(None, PingPong, ())
        .await
        .expect("Failed to start ping-pong actor");
    handle
        .await
        .expect("Ping-pong actor failed to exit properly");
}

它将输出

$ cargo run
ping..pong..ping..pong..ping..pong..ping..pong..ping..pong..
$ 

消息传递演员

演员之间的通信方式是它们相互传递消息。开发者可以定义任何消息类型,只要它是 Send + 'static,它就会得到 ractor 的支持。有 4 种并发消息类型,它们按优先级监听。它们是

  1. 信号:信号是所有中的最高优先级,并将中断演员当前正在处理的地方(这包括异步工作的终止)。目前只有 1 个信号,即 Signal::Kill,它将立即终止所有工作。这包括消息处理或监督事件处理。
  2. 停止:还有一个预定义的停止信号。如果您想,您可以为它提供一个“停止原因”,但这不是必需的。停止是一个优雅的退出,这意味着正在执行的异步工作将完成,在下一个消息处理迭代中,停止将优先于未来的监督事件或常规消息。它将 不会 终止当前正在执行的工作,无论提供的理由如何。
  3. 监督事件:监督事件是子演员在启动、死亡和/或未处理的恐慌事件时发送给其监督者(父演员)的消息。监督事件是监督者(父演员)或同伴如何通知其子/同伴的事件,并为他们处理生存周期事件的方式。如果您在 Cargo.toml 中设置 panic = 'abort',则恐慌将 导致程序终止,并且不会在监督流中被捕获。
  4. 消息:常规的、用户定义的消息是传递给演员的最后一个通信渠道。它们是 4 种消息类型中优先级最低的,表示一般演员工作。前 3 种消息类型(信号、停止、监督)在演员的生命周期事件之外通常很安静,但这个渠道是“工作”渠道,执行演员想要执行的操作!

Ractor 在分布式集群中

Ractor 演员也可以用来构建分布式演员池,类似于 Erlang 的 EPMD,它管理节点之间的连接 + 节点命名。在我们的实现中,我们有 ractor_cluster,以方便分布式 ractor 演员。

ractor_cluster 中有一个主要类型,即 NodeServer,它代表一个 node() 进程的主机。它还包含一些宏和过程宏,以帮助开发者提高构建分布式演员的效率。 NodeServer 负责以下内容

  1. 管理所有传入和传出的 NodeSession 演员,这些演员代表连接到该主机的远程节点。
  2. 管理 TcpListener,它为主机服务器套接字接受传入会话请求。

然而,节点之间连接的大部分逻辑都包含在 NodeSession 中,它管理

  1. 底层的 TCP 连接,管理流中的读写。
  2. 节点和连接到同伴的连接之间的身份验证
  3. 管理在远程系统上创建的演员的生命周期
  4. 在节点之间传输所有演员之间的消息
  5. 管理 PG 群组同步

等等。

NodeSession 通过创建 RemoteActor 来使本地演员在远程系统上可用,RemoteActor 实质上是未类型化的演员,只处理序列化消息,将消息反序列化留给源系统处理。它还会跟踪待处理的 RPC 请求,以便在回复时匹配请求和响应。在 ractor 中有特殊的扩展点,这些扩展点被添加来专门支持不在标准

Actor::spawn(Some("name".to_string()), MyActor).await

模式中使用的 RemoteActor

设计支持远程的演员

注意并非所有演员都是平等的。演员需要支持通过网络链路发送其消息类型。这是通过覆盖所有消息都需要支持 ractor::Message 特质的具体方法来实现的。由于 Rust 中缺乏特化支持,如果您选择使用 ractor_cluster,则需要为您在包中的所有消息类型派生 ractor::Message 特质。然而,为了支持这一点,我们提供了一些过程宏,使这个过程更加痛苦。

为仅进程内演员派生基本消息特质

许多演员将是仅本地使用的,并且不需要通过网络链路发送消息。这是最基本的情况,在这种情况下,默认的 ractor::Message 特质实现就足够了。您可以使用以下方式快速派生它

use ractor_cluster::RactorMessage;
use ractor::RpcReplyPort;

#[derive(RactorMessage)]
enum MyBasicMessageType {
    Cast1(String, u64),
    Call1(u8, i64, RpcReplyPort<Vec<String>>),
}

这将为您实现默认的 ractor::Message 特质,而无需您手动编写它。

为远程演员派生网络可序列化消息特质

如果您希望您的演员 支持 远程操作,那么您应该使用不同的派生语句,即

use ractor_cluster::RactorClusterMessage;
use ractor::RpcReplyPort;

#[derive(RactorClusterMessage)]
enum MyBasicMessageType {
    Cast1(String, u64),
    #[rpc]
    Call1(u8, i64, RpcReplyPort<Vec<String>>),
}

这为实现添加了大量底层样板代码(使用 cargo expand 查看它)

ractor_cluster::derive_serialization_for_prost_type! {MyProtobufType}

除此之外,只需像平时一样编写您的演员即可。演员本身将存在于您定义的位置,并将能够接收来自其他集群通过网络链路发送的消息!

演员的 "状态" 和 self

演员可以(但不一定需要)拥有内部状态。为了方便这一点,ractor 允许实现 Actor 特质的实现者定义演员的状态类型。演员的 pre_start 例程是用来初始化和设置这个状态的。您可以想象做些事情,比如

  1. 打开网络套接字 + 将 TcpListener 存储在状态中
  2. 建立数据库连接 + 认证到 DB
  3. 初始化基本状态变量(计数器、统计数据等)

由于这一点以及一些操作可能失败的可能性,pre_start 会捕获初始化过程中方法中的 panic,并将其返回给 Actor::spawn 的调用者。

在设计和 ractor 时,我们明确决定为演员创建一个单独的状态类型,而不是传递一个可变的 self 引用。这样做的原因是,如果我们使用一个 &mut self 引用,Self 结构体的创建和实例化将超出演员的规范(即在 pre_start 之外),并且它所提供的安全性可能会丢失,导致调用者可能不应该发生的崩溃。

最后,我们需要更改 ractor 目前基于的一些所有权属性,以便在每次调用中传递一个拥有的 self,并返回一个 Self 引用,这在当前上下文中看起来有些笨拙。

在当前实现中,演员的 self 以只读引用的形式传递,理想情况下不应包含状态信息,但如果需要,可以包含配置/启动信息。然而,每个 Actor 都有 Arguments,允许将拥有的值传递给演员的状态。在理想的世界中,所有演员结构体都是空的,不包含任何存储值。

贡献者

ractor 的原始作者是 Sean Lawlor (@slawlor)。有关如何为 ractor 贡献的更多信息,请参阅 CONTRIBUTING.md

许可证

本项目受 MIT 许可证许可。

依赖关系

~245–680KB
~16K SLoC