16 个版本

0.3.0 2024 年 4 月 27 日
0.2.3 2024 年 4 月 26 日
0.1.71 2024 年 4 月 4 日
0.1.69 2024 年 3 月 31 日
0.1.63 2024 年 2 月 27 日

#168异步

Download history 320/week @ 2024-04-23 24/week @ 2024-04-30 5/week @ 2024-05-21 22/week @ 2024-07-02 67/week @ 2024-07-23 31/week @ 2024-07-30

每月 98 次下载

无许可

215KB
3K SLoC

ππtρ/pptr

github crates.io docs.rs

Coverage

Puppeteer:Rust 中可组合系统的灵活的基于 Actor 的框架

Puppeteer (pptr) 是一个基于 Actor 的框架,旨在简化 Rust 中可组合和可维护的异步系统的开发。通过其类型驱动的 API 设计,Puppeteer 提供了一种安全且方便的方法来创建和管理通过消息传递进行通信的 Actor。

Puppeteer 考虑到可组合性、封装性和单一责任,而不是专注于构建大规模分布式系统。这反映在其只允许每个类型只有一个 Actor 实例的方法中,从而促进了一个模块化和可维护的架构。每个 Puppet 都封装了特定的责任,并通过消息传递与其他 Puppet 进行通信,从而形成一个更可组合的系统。

虽然这种设计决策可能看起来像是一种限制,但它擅长构建优先考虑这些原则的常规系统,为典型应用提供了高度的可伸缩性。Puppeteer 通过将系统构建为通过消息传递进行通信的独立、可重用组件集合的方法,受到了 Alan Kay 对面向对象编程的愿景的启发。

与 Erlang OTP 或 Akka 的另一个副本不同,Puppeteer 有不同的目标,旨在通过消息传递提供对 Actor 模型和面向对象编程交汇处的全新视角。无论您是构建响应式用户界面还是重视可组合性和可维护性的模块化系统,Puppeteer 都使您能够轻松地在 Rust 中编写高效且可维护的异步代码。

关键特性

1. 类型驱动开发

Puppeteer 致力于类型驱动开发,利用 Rust 强大的类型系统来确保编译时安全和运行时可靠性。该框架利用 Rust 的特性和类型推断,为定义 Actor 和其消息处理行为提供了一种无缝且富有表现力的 API。

使用 Puppeteer,您可以定义每个 Actor 的自定义消息类型,实现精确的通信和强大的类型安全性。该框架自动为您消息类型推导必要的特性,减少样板代码并使您能够轻松地在 Actor 之间发送消息。

Puppeteer 的类型驱动方法也扩展到消息处理。您可以定义多个处理程序来处理单个消息类型,提供灵活性和模块化,以便于消息处理。该框架使用 Rust 的类型系统确保只有有效的消息类型可以被发送到和由演员处理,在编译时捕获潜在错误。

借助 Rust 的类型系统,Puppeteer 有助于您编写更健壮和可维护的代码。该框架的类型驱动设计鼓励您仔细思考您的演员将发送和接收的消息,从而实现更清晰和更明确的信息传递模式。借助 Puppeteer,您可以在开发过程中早期依赖编译器捕获与类型相关的错误,节省调试和重构的时间和精力。

2. 人体工程学 API

Puppeteer 提供了一个干净、表达性强的 API,使演员创建和消息传递变得轻而易举。API 被设计为直观易用,使开发者能够专注于构建他们的应用程序,而不是与复杂的语法或样板代码搏斗。

创建演员很简单,Puppeteer 提供了一种简单的方法来定义演员行为和处理传入的消息。该 API 鼓励明确分离关注点,使得推理每个演员的责任以及它们如何相互作用变得容易。

Puppeteer 可以无缝集成到流行的 Rust 库和框架中,如 Tokio,使开发者能够利用异步编程的力量,轻松构建高度并发的应用程序。

API 还提供了有用的实用工具和抽象,简化了常见任务,例如管理演员生命周期、处理错误以及在必要时优雅地关闭演员系统。

借助 Puppeteer 的人体工程学 API,开发者可以快速开始构建健壮且可扩展的基于演员的系统,同时不牺牲性能或灵活性。

3. 无需努力即可实现异步编程

Puppeteer 简化了 Rust 中的异步编程。它使用 Tokio 运行时,并与 Rust 的 async/await 语法良好地协同工作。这使得您可以编写易于阅读和理解的异步代码,几乎就像同步代码一样。

借助 Puppeteer,您可以创建管理自己状态并通过发送消息相互通信的演员。您还可以选择让演员直接共享状态。Puppeteer 会为您处理同步,减少竞争条件的发生,使您的代码更安全。

4. 性能驱动

Puppeteer 在设计时考虑了性能。它使用 Tokio 运行时高效地并发和并行处理消息。该框架提供三种消息处理模式:

  1. 顺序:消息依次处理,确保严格的顺序。
  2. 并发:并发处理消息,允许更高的吞吐量。
  3. 专用并发:将 CPU 密集型任务分配给单独的执行器,防止它们阻塞其他任务。

这些模式为您提供灵活性,可以根据特定需求选择最佳方法。Puppeteer 还提供了一个 Executor 特性,允许您为专用工作负载创建自定义执行器。这使您能够完全控制演员任务如何执行。

5. 灵活监督

Puppeteer 提供了一个灵活的监督系统,使您能够以分层的方式监督和整理演员。演员可以独立操作或在监督者的指导下操作。监控不是强制性的,但可以根据需要实施。Puppeteer 提供了三种预定义的监督策略:一对一、一对多和一对一。此外,您还可以创建自己的自定义策略来处理错误并在最适合您特定需求的方式下维护系统稳定性。

6. 多样化的消息传递

Puppeteer提供了多种方法供演员之间发送消息。您可以根据应用程序的需求选择最佳方法。如果您想快速发送消息,Puppeteer提供了相应的选项。如果您需要确保消息可靠送达,Puppeteer也支持这一点。您还可以异步发送消息,这意味着发送者不必等待响应就可以继续其他任务。Puppeteer的消息传递功能设计得简单易懂,因此您可以专注于构建应用程序,而不必陷入复杂的细节。

7. 层次化演员结构

Puppeteer允许您使用“木偶”和“主人”来组织演员的层次化结构。这意味着一些演员(称为主人)可以控制和管理工作中的其他演员(称为木偶)。以这种方式组织演员可以更容易地构建和理解复杂的系统。

使用木偶和主人,您可以

  • 明确定义不同演员的角色和责任
  • 建立演员之间的关系,明确他们如何协作
  • 简化大量演员的管理

虽然Puppeteer支持层次化结构,但您也可以选择使用平面结构,其中所有演员处于同一级别,如果这更适合您的需求。

8. 强健的错误处理

在系统中与多个演员一起工作时,拥有一种处理错误的方法非常重要。Puppeteer包含内置功能来监控演员,并在出现问题的情况下自动重启它们。它还允许您根据具体需求定义不同的错误处理策略。

在无法由演员或其监督者自行修复的严重错误情况下,Puppeteer提供了一种单独的机制来报告和管理这些问题。这有助于确保即使在面对意外问题时,您的系统也能保持稳定和可靠。

9. 生命周期管理

Puppeteer提供了内置方法,帮助管理演员的生命周期。这些方法处理初始化、启动、关闭和重置演员状态等任务。通过使用这些预实现的周期管理功能,您可以节省时间和精力,无需为每个演员编写相同的代码。相反,您可以将注意力集中在演员逻辑的重要部分。

使用Puppeteer的周期管理功能可以更容易地在应用程序中创建和管理演员。您无需担心如何初始化或清理演员的资源,因为Puppeteer会为您处理这些。这有助于使您的代码更干净,更专注于演员需要执行的具体任务。

10. 资源管理

Puppeteer提供了一种管理演员之间可以共享的资源的方法。在某些情况下,例如使用外部库或多个演员需要访问相同的数据库句柄或用户界面时,这可能很有用。

由于Rust的所有权系统,在Rust中共享资源可能会很具有挑战性。然而,Puppeteer提供了一种解决方案,允许演员安全地访问和修改共享资源。

虽然不建议在演员之间共享状态,但Puppeteer的资源管理系统提供了一种实用的方法来处理在必要时变得必要的场景。它为您提供了在某些用例中需要的灵活性。

通过利用Puppeteer的资源管理功能,您可以有效地管理共享资源,同时保持基于演员的系统安全性和效率。

入门指南

要开始在Rust项目中使用Puppeteer,请在您的Cargo.toml文件中添加以下依赖项

[dependencies]
Puppeteer = "0.2.0"
use pptr::prelude::*;

#[derive(Default)]
struct PingActor;

#[async_trait]
impl Puppet for PingActor {
    // This actor uses the 'OneForAll' supervision strategy.
    // If any child actor fails, all child actors will be restarted.
    type Supervision = OneForAll;

    // The 'reset' method is called when the actor needs to reset its state.
    // In this example, we simply return a default instance of 'PingActor'.
    async fn reset(&self, _ctx: &Context) -> Result<Self, CriticalError> {
        Ok(Self::default())
    }
}

// We define a 'Ping' message that contains a counter value.
#[derive(Debug)]
struct Ping(u32);

// The 'Handler' trait defines how the actor should handle incoming messages.
// It is a generic trait, which allows defining message handling for specific message types,
// rather than using a single large enum for all possible messages.
// This provides better type safety and easier maintainability.
// By implementing the 'Handler' trait for a particular message type and actor,
// you can define the specific behavior for handling that message within the actor.
// Additionally, the 'Handler' trait can be implemented multiple times for the same message type,
// allowing different actors to handle the same message type in their own unique way.
// This flexibility enables better separation of concerns and modular design in the actor system.
#[async_trait]
impl Handler<Ping> for PingActor {

    // The 'Response' associated type specifies the type of the response returned by the handler.
    // In this case, the response type is '()', which means the handler doesn't return any meaningful value.
    // It is common to use '()' as the response type when the handler only performs side effects and doesn't need to return a specific value.
    type Response = ();

    // The 'Executor' associated type specifies the execution strategy for handling messages.
    // It determines how the actor processes incoming messages concurrently.
    // The 'SequentialExecutor' processes messages sequentially, one at a time, in the order they are received.
    // This ensures that the handler for each message is executed to completion before processing the next message.
    // The 'SequentialExecutor' is suitable when the order of message processing is important and the handler doesn't perform any blocking operations.
    type Executor = SequentialExecutor;

    // The 'handle_message' method is called when the actor receives a 'Ping' message.
    // It prints the received counter value and sends a 'Pong' message to 'PongActor'
    // with an incremented counter value, until the counter reaches 10.
    async fn handle_message(&mut self, msg: Ping, ctx: &Context) -> Result<Self::Response, PuppetError> {
        // The 'ctx' parameter is a reference to the 'Context' struct, which encapsulates
        // the actor's execution context and provides access to the same methods as the 'pptr' instance.
        // It allows the actor to send messages, spawn new actors, and perform other actions.
        // If an actor is spawned using 'ctx', it automatically assigns the spawning actor as its supervisor.
        // The 'ctx' parameter enables safe and consistent interaction with the actor system,
        // abstracting away the underlying complexity.

        println!("Ping received: {}", msg.0);
        if msg.0 < 10 {
            // By using 'ctx.send', the actor can send messages to other actors directly from the message handler,
            // ensuring proper error propagation and potential supervision.
            ctx.send::<PongActor, _>(Pong(msg.0 + 1)).await?;
        } else {
            println!("Ping-Pong finished!");
        }

        Ok(())
    }
}

#[derive(Clone, Default)]
struct PongActor;

// By default, similar to 'PingActor', the 'reset' method returns a default instance of 'PongActor'.
#[async_trait]
impl Puppet for PongActor {
    type Supervision = OneForAll;
}

// We define a 'Pong' message that contains a counter value.
#[derive(Debug)]
struct Pong(u32);

#[async_trait]
impl Handler<Pong> for PongActor {
    type Response = ();
    type Executor = SequentialExecutor;

    // The 'handle_message' method for 'PongActor' is similar to 'PingActor'.
    // It prints the received counter value and sends a 'Ping' message back to 'PingActor'
    // with an incremented counter value, until the counter reaches 10.
    async fn handle_message(&mut self, msg: Pong, ctx: &Context) -> Result<Self::Response, PuppetError> {
        println!("Pong received: {}", msg.0);

        if msg.0 < 10 {
            ctx.send::<PingActor, _>(Ping(msg.0 + 1)).await?;
        } else {
            println!("Ping-Pong finished!");
        }
        Ok(())
    }
}

#[tokio::main]
async fn main() -> Result<(), PuppetError> {

    // Create a new instance of the Puppeteer.
    let pptr = Puppeteer::new();

    // Spawn a 'PingActor' and specify 'PingActor' as its own supervisor.
    // This means that 'PingActor' will manage itself.
    pptr.spawn::<PingActor, PingActor>(PuppetBuilder::new(PingActor::default())).await?;

    // Spawn a 'PongActor' using the shorter 'spawn_self' method.
    // This is equivalent to specifying 'PongActor' as its own supervisor.
    pptr.spawn_self(PuppetBuilder::new(PongActor::default())).await?;

    // Send an initial 'Ping' message to 'PingActor' with a counter value of 0.
    // This starts the ping-pong game between 'PingActor' and 'PongActor'.
    pptr.send::<PingActor, _>(Ping(0)).await?;

    tokio::time::sleep(std::time::Duration::from_secs(1)).await;

    Ok(())
}

有关详细的使用示例和API文档,请参阅Puppeteer 文档

许可证

Puppeteer 是一个开源软件,遵循 MIT 许可协议

依赖项

~4–11MB
~106K SLoC