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
226 monthly downloads
在ractor_cluster中使用
36KB
344 行
ractor
发音为 ract-er
一个纯Rust actor框架。受Erlang的gen_server
启发,兼具Rust的速度和性能!
网站 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框架(如Actix、riker,或者Tokio中的actor)之外,还有一些类似的框架列在这个Reddit帖子上。
Ractor试图通过更多地向纯Erlang的gen_server
模型靠拢来实现差异化。这意味着每个actor也可以简单地成为其他actor的supervisor,而无需额外的成本(只需将它们链接在一起即可!)此外,我们还在努力保持与Erlang模式紧密的逻辑,因为它们在实际应用中表现良好并且得到充分利用。
此外,我们编写了ractor
,没有在需要spawn的某种“运行时”或“系统”上构建。actor可以独立运行,与其他基本的tokio
运行时结合使用,而无需额外的开销。
目前我们完全支持以下功能
- 单线程消息处理
- actor监督树
- 在
rpc
模块中对actor的远程过程调用 - 在
time
模块中的计时器 - 命名actor注册表(
registry
模块),来自Erlang的Registered processes
- 进程组(
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
公开以下功能
cluster
,它公开了设置和管理actor网络链接集群所需的各项功能。这是一个正在进行中的项目,在#16中跟踪。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 个信号,即
Signal::Kill
,它将立即终止所有工作。这包括消息处理或监督事件处理。 - 停止:还有一个预定义的停止信号。如果您想,您可以为它提供一个“停止原因”,但这不是必需的。停止是一个优雅的退出,这意味着正在执行的异步工作将完成,在下一个消息处理迭代中,停止将优先于未来的监督事件或常规消息。它将 不会 终止当前正在执行的工作,无论提供的理由如何。
- 监督事件:监督事件是子演员在启动、死亡和/或未处理的恐慌事件时发送给其监督者(父演员)的消息。监督事件是监督者(父演员)或同伴如何通知其子/同伴的事件,并为他们处理生存周期事件的方式。如果您在
Cargo.toml
中设置panic = 'abort'
,则恐慌将 会 导致程序终止,并且不会在监督流中被捕获。 - 消息:常规的、用户定义的消息是传递给演员的最后一个通信渠道。它们是 4 种消息类型中优先级最低的,表示一般演员工作。前 3 种消息类型(信号、停止、监督)在演员的生命周期事件之外通常很安静,但这个渠道是“工作”渠道,执行演员想要执行的操作!
Ractor 在分布式集群中
Ractor 演员也可以用来构建分布式演员池,类似于 Erlang 的 EPMD,它管理节点之间的连接 + 节点命名。在我们的实现中,我们有 ractor_cluster
,以方便分布式 ractor
演员。
ractor_cluster
中有一个主要类型,即 NodeServer
,它代表一个 node()
进程的主机。它还包含一些宏和过程宏,以帮助开发者提高构建分布式演员的效率。 NodeServer
负责以下内容
- 管理所有传入和传出的
NodeSession
演员,这些演员代表连接到该主机的远程节点。 - 管理
TcpListener
,它为主机服务器套接字接受传入会话请求。
然而,节点之间连接的大部分逻辑都包含在 NodeSession
中,它管理
- 底层的 TCP 连接,管理流中的读写。
- 节点和连接到同伴的连接之间的身份验证
- 管理在远程系统上创建的演员的生命周期
- 在节点之间传输所有演员之间的消息
- 管理 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
例程是用来初始化和设置这个状态的。您可以想象做些事情,比如
- 打开网络套接字 + 将
TcpListener
存储在状态中 - 建立数据库连接 + 认证到 DB
- 初始化基本状态变量(计数器、统计数据等)
由于这一点以及一些操作可能失败的可能性,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