troupe.cr

Troupe Virtual Actor framework for Crystal — architecture, roadmap, and scaffold

Troupe.cr

面向 Crystal 的 Virtual Actor 框架,语义对标 Orleans,沿用 Troupe 的剧团术语。

当前状态:M1 单机核心 + M2 持久化/语义硬化/可观测/基准 + M3 PostgreSQL Provider(dev)。 已实现 ActorId / Role manifest / Memory 与 SQLite / PostgreSQL PropStore(CAS+fencing,PG 为 cross_node_safe? = true 的生产 Provider)/ Stage / Agent / ActorSlot(single-flight)/ 有界 CallBoard / Intermission / Rehearsal / 幂等记录 / 协作取消 / metrics 与 W3C trace 钩子 / 基线基准。TCP transport、集群路由、持久 reminder 与 Show 尚未实现;多进程 kill -9 级故障注入矩阵尚未执行,不要据此宣称 production-ready 或跨语言互操作。

文档

验证

要求 Crystal >= 1.22。

crystal spec
crystal tool format --check src spec
crystal build src/troupe.cr --no-codegen

--no-codegen 只验证库源码可编译,不生成可执行程序。各阶段真实命令、环境与结果记录在 docs/verification.md。

最小用法(单机 Stage)

require "troupe"

class CartProps
  include JSON::Serializable
  property items : Array(String) = [] of String

  def initialize
  end
end

class CartActor < Troupe::Actor(CartProps)
  def add(item : String) : Int32
    props.items << item
    save
    props.items.size
  end
end

stage = Troupe::Stage.new
stage.register(Troupe::Manifest(CartProps).define("cart", CartActor) do |m|
  m.schema_version 1
  m.initial { CartProps.new }
  m.turn("add", CartActor) do |actor, ctx, args|
    JSON::Any.new(actor.add(args["item"].as_s).to_i64)
  end
end)

id = Troupe::ActorId.new("app", "cart", "cart-1")
result = Troupe::Agent.new(stage).call!(id, "add", JSON.parse(%({"item": "pear"})))

Turn 闭包走 JSON 参数/结果边界;save 以 revision CAS + epoch fencing 显式提交。测试请使用 Troupe::Rehearsal(固定 namespace、Memory Provider、虚拟时钟、pump 确定性调度)。

类型安全 Agent(宏 manifest)

class CartActor < Troupe::Actor(CartProps)
  troupe_manifest "cart", schema_version: 1 do
    turn add_item, {item: String}, returns: Int32
  end

  def add_item(item : String) : Int32
    props.items << item
    save
    props.items.size
  end
end

stage.register(CartActor.troupe_manifest)          # 宏生成 manifest(含参数 schema)
cart = CartActor::Agent.new(stage)                 # 类型安全代理
cart.add_item(id, "pear")                          # 编译期类型安全;缺参/类型不符排队前拒绝

完整示例见 examples/cart.cr(crystal run examples/cart.cr)。

本地持久化(M2,开发用)

store = Troupe::SQLitePropStore.open("./dev-troupe.db") # WAL 模式
stage = Troupe::Stage.new(store: store, ownership: store.ownership)

SQLite Provider 的 CAS 与 fencing 均为单条 SQL 原子语句;进程重启后从文件恢复。 注意:仅进程/单机持久(store.cross_node_safe? == false),不承诺节点丢失或跨机恢复。

PostgreSQL Provider(M3,生产)

store = Troupe::PostgreSQLPropStore.open("postgres://troupe:troupe@127.0.0.1:5432/troupe",
  owner_instance: "silo-1", lease_ms: 30_000)
stage = Troupe::Stage.new(store: store, ownership: store.ownership)

所有权带租期(数据库服务器时钟判定):跨实例有效租期内 acquire 显式拒绝(可重试), 过期后可取得且 epoch 单调递增;旧 epoch 写一律被 fencing 拒绝。多节点接管语义 (cross_node_safe? == true)。测试需设置 TROUPE_PG_URL(CI 已内置 postgres service)。

可观测(M2)

stage.on_event { |event| ... }                      # 结构化生命周期事件
stage.on_trace { |trace| ... }                      # Turn 边界 TraceEvent(W3C traceparent)
stage.metrics.counter("turns_completed")            # lifecycle/call 计数器

future = agent.call(id, "charge", args, nil, "00-<32hex>-<16hex>-01")   # 传入调用方 W3C context
ctx.traceparent                                     # Turn 内互调时显式作为父传递

核心零 OTel 依赖:on_trace 即桥接真实 OTel SDK 的钩子(对齐 troupejs onSpan 模式)。

基线基准(M2)

crystal run --release bench/run.cr

五场景:激活吞吐 / KB per Actor / 热点 vs 分散调用 / CPU 阻塞调度延迟 / 队列饱和。 口径声明与方法见 docs/benchmarks.md——本仓自基线,不与其他语言异口径比较。

实施阶段

按 PLAN.md 已完成 M1 单机核心与 M2 全部五项(SQLite 本地 Provider、语义硬化、 可观测、类型安全 Agent/宏 manifest、基线基准)。下一阶段 M3:PostgreSQL Provider、 TCP transport、持久 reminder 与生产容错验收。

许可

MIT,见 LICENSE。

Repository

troupe.cr

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 2
  • about 1 hour ago
  • November 14, 2026
License

MIT License

Links
Synced at

Sat, 14 Nov 2026 16:03:28 GMT

Languages