#actor-system #actor #actor-framework #messages #system #message

acteur

一个安全且易于使用的类似actor框架。简单、健壮、快速、文档齐全。

31个版本 (10个破坏性更新)

0.12.2 2020年11月29日
0.12.0 2020年9月13日
0.10.3 2020年5月24日
0.3.1 2020年3月29日

#845 in 并发

Apache-2.0/MIT

130KB
2K SLoC

Acteur Actor系统

一个安全且具有意见的类似actor框架,用Rust编写,易于使用。简单、健壮、快速、文档齐全。

状态更新

更新1

因此,我花了一些时间来思考这个问题框架,并有意将其迁移到业务逻辑+分布式框架。想法是创建一个框架,允许你编写标识聚合体/模型/actor,而不必承受太多负担。

更新2

我正在尝试使用raft和sled来实现集群部分。你可以在文件playing_with_raft.rs中看到它。

动机

actor很酷。很多人都在写关于它们的内容,Actix在基准测试中统治。但使用actor编写多个服务器组成的后端系统并不容易。实际上,这会带来很多其他复杂性。但actor并不是一个坏抽象,而是并发的解决方案,而不是业务逻辑组织的解决方案。它们间接解决了某些问题,这是很好的,但引入了其他问题。因此,这个框架旨在实现一个框架,该框架实现了一些类似于actor的功能,但进行了许多调整和优化,以便编写业务逻辑。

话虽如此,以下情况下Acteur可能不是你想要的工具:

  • 你需要一个完全符合ACID的系统
  • 你希望完全遵循actor模型
  • 你需要扩展到大量的流量。在这种情况下,你将需要多个服务器。(我计划实现一些多服务器集群,但目前只有一台服务器)。

但如果你想要以下内容,它可能会帮到你:

  • 你需要一个数据库,但不想承担READ、APPLY、SAVE的成本,而想将对象实例保留在RAM中。
  • 你不想处理乐观并发,并且希望消息按每个ID逐一处理,但在ID之间并发处理。
  • 你想为在线视频游戏创建后端,该游戏有大量实体同时交互,但又不想完全采用ECS。

Acteur的主要功能

这个actor系统与其他框架略有不同。它基于以下前提

  • 高级:框架面向映射业务逻辑,而不是任务并发。
  • 简单:API应该小巧、简单、直观。没有惊喜。
  • 并发:系统应该快速,并使用所有可用的CPU核心。
  • 文档化:一切都必须有详尽的示例进行文档化。

关于实现

  • Acteur 是 异步的,并且底层使用 async_std
  • Acteurs 有一个 ID,其类型由开发者定义。
  • 消息被路由到 ActorID
  • Actor 生命周期由框架 自动管理
  • 同一 Actor 和 ID 的消息是 顺序的。其他所有操作都是 并发 执行的。
  • 提供其他并发形式的服务。
  • 服务 没有 ID,并且是并发的。
  • 服务可以 订阅 消息,任何人都可以 发布 消息。
  • Acteur 是 全局的,只能存在一个实例。

实现状态

我现在的主要工作重点是添加并发和改进用户体验。已实现的功能

  • ☑️ Actor / Service 在收到第一条消息时激活
  • ☑️ Actor 可以向其他 Actor / 服务发送消息
  • ☑️ 系统可以向任何 Actor / 服务发送消息
  • ☑️ Actors / 服务可以选择,对消息做出响应
  • ☑️ 服务:有状态或无状态,没有 ID(如真正的 Actor)并且是并发的。
  • ☑️ 自动释放未使用的 Actor(5 分钟内没有消息后)
  • ☑️ 服务可以订阅消息
  • □ Actor 释放配置(基于 RAM、Actor 数量或超时)
  • □ 集群:实现 Raft 以将每个 Actor 分配到不同的服务器

Acteur 结构

为了使用 Acteur,您只需实现正确的 trait,当消息路由到您的 Actor/Service 时,Acteur 将自动使用您的实现。

主要 trait 有

只需实现它们,您的 Actor/Service 就可以使用了。

对于 Actors,您有两个 trait 来处理消息

  • Receive:接收消息但不响应。处理消息的最有效方式。
  • Respond:接收消息并允许对其进行响应。强制发送者等待直到 Actor 响应。

对于服务,您还有另外两个 trait。

  • Listen:接收消息但不响应。处理消息的最有效方式。
  • Serve:接收消息并允许对其进行响应。强制发送者等待直到 Actor 响应。

为什么您使用 4 个不同的 trait 而不是 1 或 2 个?

我尝试合并 Traits,但没有找到如何实现它,因为

A) 处理方法签名中包含 ActorAssistant 和 ServiceAssistant 类型,它们具有不同的类型。B) 我不喜欢为每条消息创建一个响应通道,因为许多消息不需要响应。

这两个块都产生 4 种组合。Actor 的 Receive/Respond 和服务的 Listen/Serve。

我仍在尝试改进命名和用户体验。我认为概念将保持不变,但用户体验可能会略有变化。

Actors 与 Services

Acteur 提供两种并发方式:Actors 和 Services。

Actors

Actors 有一个 ID,并将顺序处理指向同一 Actor ID 的消息。这意味着如果您向 Actor User-32 发送 2 条消息,它们将顺序处理。另一方面,如果您向 Actor User-32 和 User-52 发送消息,它们将并发处理。

这意味着,Actors 实例为同一 ID 保持消息顺序,但不同 ID 之间没有顺序。

Services

另一方面,服务没有ID,它们是并发的。这意味着你可以选择服务的实例数量(Acteur提供了一个默认值)。服务可以有或没有状态,但如果它们有,则需要同步(即互斥锁)。

简而言之,服务更像是普通Actor(或者,你可以将其视为普通Web服务),但具有一些预设的并发因素。你可以拥有许多实例,并且在消费消息时没有任何类型的同步。将它们视为当你想要创建不适合此框架的Actor模型时使用的原语。

用例

选择Actor用于实体(用户、发票、玩家等,任何其实例都有标识的)。

选择服务用于业务逻辑、基础设施、适配器等(存储、数据库访问、HTTP服务、不属于任何Actor的某些计算等)以及订阅消息(发布/订阅)。

订阅或发布/订阅

有时我们不想知道谁应该接收消息,只想订阅一种类型并等待。Acteur使用服务模拟发布/订阅模式。Acteur中的Actor无法执行订阅,因为这要求框架知道所有可能的Actor实例的所有可能的ID,以便将消息定向到正确的(或所有)实例,并且这与未使用的Actor的释放不太兼容。

如果你想从订阅向某些Actor发送消息,你可以创建一个订阅消息并确定要将消息发送到哪些Actor ID的服务。例如,在数据库/服务中进行查询以获取需要接收某些消息的ID集合。

与向服务/Actor发送/调用不同,发布在编译时不知道谁需要接收消息。这就是为什么要求服务在运行时订阅任何它们想要接收的消息的原因。为了确保服务执行订阅,一个好的做法是在应用程序启动时为每个应执行任何订阅的服务运行acteur.preload_service<Service>();

简单示例

use acteur::{Actor, Receive, ActorAssistant, Acteur};
use async_trait::async_trait;

#[derive(Debug)]
struct Employee {
    salary: u32
}

#[async_trait]
impl Actor for Employee {
    type Id = u32;

    async fn activate(_: Self::Id, _: &ActorAssistant<Self>) -> Self {
        Employee {
            salary: 0 // Load from DB or set a default,
        }
    }
}

#[derive(Debug)]
struct SalaryChanged(u32);

#[async_trait]
impl Receive<SalaryChanged> for Employee {
    async fn handle(&mut self, message: SalaryChanged, _: &ActorAssistant<Employee>) {
        self.salary = message.0;
    }
}

fn main() {
    let sys = Acteur::new();

    sys.send_to_actor_sync::<Employee, SalaryChanged>(42, SalaryChanged(55000));

    sys.wait_until_stopped();
}

为什么还需要另一个Actor框架?

有些事情让我感到烦恼。

  1. Actor系统是一种并发级别,但我看到它们被用于业务逻辑。使用普通的HTTP框架+SQL比使用Actix更自然。
  2. 为了使用Actix,你需要了解它是如何工作的。你需要管理并发、地址等。
  3. 不安全。我不想使用不安全。我不会信任自己像在C++中那样做这样的操作,因此,我不想有不稳定代码。Rust为那些C/C++经验不足10年的人打开了以更安全的方式做这类项目的门。

在async_std 1.0发布和与一些朋友交谈后,我开始设想我想如何喜欢一个actor框架。不是Actix和其他框架是错误的,但在我看来,它们太低级了,不适合业务逻辑。我想找一个不需要泄露太多底层概念的框架运行。同时,我认为竞争最后一纳秒是不健康的。即使框架已经非常快,也是如此。

常见模式

本节将更新你可以在应用程序中使用的常见模式。如果你想添加一个,或者只是想知道如何做到某事,请通过GitHub Issue告诉我。

Web服务器

由于所有参与者都由框架管理,因此例如让Rocket或Tide获取新的HTTP调用并调用acteur.call_serviceacteur.call_actor然后等待响应变得非常容易。如果您使用的是同步代码,可以使用调用的同步版本。请记住,您可以克隆Acteur并将其发送到您需要的任意多个线程/结构。


use acteur::Acteur;

let acteur = Acteur::new();

// You can clone and send it to another thread/struct
let acteur2 = acteur.clone();

如果您需要参与者查询数据库,通常将数据库连接/池保存在服务中是一个好主意,在那里您可以处理连接错误、在出错时重新连接,并且可以控制并发。

错误处理

如果您有可能会出错的操作,最好将它们编码在服务中,并保留参与者用于无法失败的操作。例如,数据库连接、网络连接等。

从业务规则的角度来看,将失败编码在参与者中是完全可以接受的,例如,在视频游戏中,一个角色不能攻击另一个不可侵犯的角色。

因此,将可能因外部环境(网络、硬盘等)而失败的一切放在服务中,让参与者请求他们需要的任何服务。

如果您有一个应该停止应用程序启动的错误,例如数据库连接,请将其添加到服务构建中,并使用preload_service方法在应用程序启动时尝试启动服务,如果出错则允许应用程序崩溃。

安全Rust

此crate中没有直接使用不安全代码。您可以在lib.rs中检查#![deny(unsafe_code)]行。

贡献

首先,如果您决定检查框架并为其做出贡献,我将非常高兴!只需打开一个问题/拉取请求,我们可以检查您想实现的内容。有关贡献的更多信息,请参阅此处:https://github.com/DavidBM/acteur-rs/blob/master/CONTRIBUTING.md

许可证

根据您的选择,此代码可受Apache许可证第2版MIT许可证的许可。
除非您明确表示,否则您提交给此crate的任何有意包含的贡献,根据Apache-2.0许可证定义,应如上所述双重许可,没有任何额外的条款或条件。

依赖项

~6–16MB
~213K SLoC