Rust中的事件溯源 - ariseyhun


Rust 是一种与事件溯源艺术相结合的令人难以置信的语言。
这篇博文的目的是介绍我一直在全职工作的一个名为Thalo的项目。
它是一个 Rust 库,提供构建事件源系统所需的一切。
目前,Thalo 提供:

  • 具有基本聚合、事件、事件存储和事件流特征的核心板条箱,以及一些派生宏
  • 测试库(Given-When-Then)
  • Postgres、内存和文件事件存储
  • 卡夫卡事件流

 
您可以通过克隆项目并在单独的终端选项卡中运行客户端和服务器来运行示例。
$ git clone git@github.com:thalo-rs/thalo.git && cd thalo
$ cargo run -p example-protobuf --bin server
# In separate terminal tab
$ cargo run -p example-protobuf --bin client

由于Thalo提供的派生宏,编写聚合体并不费力,但我仍然觉得缺少一些东西。我已经研究了CloudEvents和AsyncAPI作为定义聚合模式的选择,但它们似乎并不擅长这样做。
 
ESDL
事件源模式定义语言是一种模式语言,用于定义集合体、命令、事件和类型,可用于在Rust中生成代码,大大简化了集合体的模板。网址



examples > bank-account > B bank-account.esdl
   1 aggregate BankAccount { open_account(initial_balance: Float!): OpenedAccount!
   3     deposit_funds(amount: Float!): DepositedFunds!
   4     withdraw_funds(amount: Float!): WithdrewFunds!
   5   }
   6
   7     event OpenedAccount {
   8     initial_balance: Float!
   9   }
 10
 11      event DepositedFunds {
 12      amount: Float!
 13    }
 14
 15      event WithdrewFunds {
 16      I amount: Float!
 ¹⁷      }
 18

如果你熟悉GraphQL,它是不言自明的,但让我们一行一行地看下去。

  • aggregate  BankAccount {
    一个esdl文件总是必须精确地定义一个聚合体,在我们的例子中名为BankAccount。
  • open_account(initial_balance: Float!): OpenedAccount!
    我们定义了一个名为open_account的命令,它需要一个初始余额的浮动值,并产生一个OpenedAccount事件。
    注意!这意味着类型是必须的,不能是未定义/空/无。
  • deposited_funds(amount: Float!): DepositedFunds!
    withdrew_funds(amount: Float!): WithdrewFunds!: WithdrewFunds!
    如上所述,这些也是接受一个金额并返回一个事件的命令。
  • event  OpenedAccount {
    这里我们定义了OpenedAccount事件和它的可用字段。
  • initial_balance: Float!
    开户事件有一个初始余额,要求有一个浮动值。

......其余的就很好解释了。
在这一点上,我们有一个用于事件源的Rust库,和一个用于定义聚合的模式语言。如果我们把这两者结合起来,我们就可以用esdl文件来实现模式,而用Rust来通过代码生成来实现。
为了从这个esdl文件中生成Rust代码,我们可以使用build.rs文件并使用esdl crate进行编译。
// build.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
    esdl::configure()
        .add_schema_file(
"./bank-account.esdl")?
        .compile()?;
    Ok(())
}

而在我们的Rust代码中,我们可以使用Thalo提供的宏来导入它。

thalo::include_aggregate!("BankAccount");

这将包括生成的代码,其中包括:
  • trait BankAccountCommand
  • enum BankAccountEvent
  • struct OpenedAccountEvent
  • struct DepositedFundsEvent
  • struct WithdrewFundsEvent

由此,我们可以开始执行命令:
use thalo::aggregate::{Aggregate, TypeId};
#[derive(Aggregate, Clone, Debug, Default, PartialEq, TypeId)]
pub struct BankAccount {
    id: String,
    opened: bool,
    balance: f64,
}
impl BankAccountCommand for BankAccount {
    type Error = ();
    fn open_account(&self, initial_balance: f64)
        -> Result<OpenedAccountEvent, Self::Error> { ... }
    fn deposit_funds(&self, amount: f64)
        -> Result<DepositedFundsEvent, Self::Error> { ... }
    fn withdraw_funds(&self, amount: f64)
        -> Result<WithdrewFundsEvent, Self::Error> { ... }
}

然后用apply函数来更新聚合状态:
fn apply(bank_account: &mut BankAccount, event: BankAccountEvent) {
    use BankAccountEvent::*;
    match event {
        OpenedAccount(OpenedAccountEvent { initial_balance }) => {},
        DepositedFunds(DepositedFundsEvent { amount }) => {},
        WithdrewFunds(WithdrewFundsEvent { amount }) => {},
    }
}

ThaloESDL将让您在为事件源系统编写干净且一致的聚合方面领先一步。
我正在积极地全职开发 Thalo,并希望在未来几个月内发布一些实质性的东西,敬请期待!