| Crates.io | cineyma |
| lib.rs | cineyma |
| version | 0.1.0 |
| created_at | 2026-01-13 08:54:08.806686+00 |
| updated_at | 2026-01-13 08:54:08.806686+00 |
| description | A lightweight actor framework for Rust with typed messages, OTP-style supervision, and distributed clustering |
| homepage | https://cineyma.vercel.app |
| repository | https://github.com/pixperk/cineyma |
| max_upload_size | |
| id | 2039629 |
| size | 7,393,733 |
A lightweight actor model framework for Rust, inspired by Erlang/OTP, Akka, and actix.
run_later and run_interval schedulingcineyma prioritizes:
If you want HTTP-first or macro-heavy ergonomics, use actix. If you want OTP-style fault tolerance in Rust, use cineyma.
Note on panics: cineyma treats panics inside actors as failures, similar to Erlang process crashes. Panics are caught at actor boundaries and never crash the runtime.
use cineyma::{Actor, Handler, Message, ActorSystem, Context};
// define a message
struct Greet(String);
impl Message for Greet {
type Result = String;
}
// define an actor
struct Greeter;
impl Actor for Greeter {}
impl Handler<Greet> for Greeter {
fn handle(&mut self, msg: Greet, _ctx: &mut Context<Self>) -> String {
format!("Hello, {}!", msg.0)
}
}
#[tokio::main]
async fn main() {
let system = ActorSystem::new();
let addr = system.spawn(Greeter);
// fire and forget (async, applies backpressure if mailbox full)
addr.do_send(Greet("World".into())).await.unwrap();
// request-response
let response = addr.send(Greet("cineyma".into())).await.unwrap();
println!("{}", response); // "Hello, cineyma!"
}
cineyma uses bounded mailboxes (default capacity: 256 messages) to prevent out-of-memory issues from slow consumers.
// default capacity (256)
let addr = system.spawn(MyActor);
// custom capacity for high-throughput actors
let addr = system.spawn_with_capacity(MyActor, 10000);
// child actors with custom capacity
ctx.spawn_child_with_capacity(ChildActor, 1000);
// async send with backpressure (blocks if mailbox full)
addr.do_send(msg).await?;
// non-blocking try_send (returns MailboxFull error if full)
addr.try_send(msg)?;
// request-response (always async)
let response = addr.send(msg).await?;
When to use which:
- Use
do_send().awaitin async contexts when you want backpressure- Use
try_send()in sync contexts (handlers, lifecycle hooks) or when you want immediate failure- Benchmarks and high-throughput scenarios should use
try_send()or increase mailbox capacity
Context<Self> is the actor's handle to the runtime:
| Method | Description |
|---|---|
spawn_child(actor) |
Spawn supervised child (default capacity: 256) |
spawn_child_with_capacity(actor, capacity) |
Spawn child with custom mailbox capacity |
spawn_child_with_strategy(factory, strategy) |
Spawn with restart policy |
spawn_child_with_strategy_and_capacity(...) |
Spawn with restart policy and custom capacity |
stop() |
Stop this actor |
address() |
Get own Addr<Self> |
run_later(duration, msg) |
Delayed self-message |
run_interval(duration, msg) |
Periodic self-message |
add_stream(stream) |
Attach async stream |
watch(addr) |
Get notified when actor dies |
use cineyma::{Actor, Context, SupervisorStrategy};
use std::time::Duration;
struct Parent;
struct Child;
impl Actor for Parent {
fn started(&mut self, ctx: &mut Context<Self>) {
// restart child up to 3 times within 10 seconds
ctx.spawn_child_with_strategy(
|| Child,
SupervisorStrategy::restart(3, Duration::from_secs(10)),
);
}
}
impl Actor for Child {}
Strategies:
Stop - Let actor die (default)Restart { max_restarts, within } - Restart on panic, up to N times within durationEscalate - Propagate failure to parent (OTP-style)use cineyma::{Actor, StreamHandler, Context};
use tokio_stream::wrappers::UnboundedReceiverStream;
struct MyActor {
stream: Option<UnboundedReceiverStream<i32>>,
}
impl Actor for MyActor {
fn started(&mut self, ctx: &mut Context<Self>) {
if let Some(stream) = self.stream.take() {
ctx.add_stream(stream);
}
}
}
impl StreamHandler<i32> for MyActor {
fn handle(&mut self, item: i32, _ctx: &mut Context<Self>) {
println!("Received: {}", item);
}
fn finished(&mut self, _ctx: &mut Context<Self>) {
println!("Stream completed");
}
}
let system = ActorSystem::new();
let addr = system.spawn(MyActor);
// register with auto-unregister on actor death
system.register("my_actor", addr);
// lookup
if let Some(addr) = system.lookup::<MyActor>("my_actor") {
addr.do_send(SomeMessage).await.unwrap();
}
Failure semantics: Registry entries are automatically removed when actors stop. During restarts, the same
Addrremains valid - senders don't need to re-lookup.
cineyma supports sending messages to actors on other nodes over TCP with Protocol Buffers serialization.
Define messages (protobuf serializable):
use cineyma::{Message, remote::RemoteMessage};
use prost::Message as ProstMessage;
// request message
#[derive(Clone, ProstMessage)]
struct Add {
#[prost(int32, tag = "1")]
n: i32,
}
impl Message for Add {
type Result = AddResult;
}
impl RemoteMessage for Add {}
// response message
#[derive(Clone, ProstMessage)]
struct AddResult {
#[prost(int32, tag = "1")]
value: i32,
}
impl Message for AddResult {
type Result = ();
}
impl RemoteMessage for AddResult {}
Server side:
use cineyma::{Actor, Handler, ActorSystem, Context};
use cineyma::remote::{LocalNode, RemoteServer};
struct Calculator { value: i32 }
impl Actor for Calculator {}
impl Handler<Add> for Calculator {
fn handle(&mut self, msg: Add, _ctx: &mut Context<Self>) -> AddResult {
self.value += msg.n;
AddResult { value: self.value }
}
}
#[tokio::main]
async fn main() {
let system = ActorSystem::new();
let calc = system.spawn(Calculator { value: 0 });
let node = LocalNode::new("calc-server");
let handler = node.handler::<Calculator, Add>(calc);
let server = RemoteServer::bind("0.0.0.0:8080", handler).await.unwrap();
server.run().await;
}
Client side:
use cineyma::remote::{RemoteClient, TcpTransport, Transport};
#[tokio::main]
async fn main() {
let transport = TcpTransport;
let conn = transport.connect("127.0.0.1:8080").await.unwrap();
let client = RemoteClient::new(conn);
let remote = client.remote_addr::<Calculator>("calc-server", "calculator");
let response = remote.send(Add { n: 5 }).await.unwrap();
let result = AddResult::decode(response.payload.as_slice()).unwrap();
println!("Result: {}", result.value);
}
Handle multiple message types:
use cineyma::remote::MessageRouter;
let handler = MessageRouter::new()
.route::<Add>(node.handler::<Calculator, Add>(calc.clone()))
.route::<Subtract>(node.handler::<Calculator, Subtract>(calc.clone()))
.route::<GetValue>(node.handler::<Calculator, GetValue>(calc))
.build();
let server = RemoteServer::bind("0.0.0.0:8080", handler).await.unwrap();
cineyma provides a gossip-based cluster with:
Nodes exchange membership information to achieve eventual consistency:
use cineyma::remote::cluster::{ClusterNode, Node, NodeStatus};
use std::sync::Arc;
use std::time::Duration;
#[tokio::main]
async fn main() {
// create cluster node
let node1 = Arc::new(ClusterNode::new(
"node-1".to_string(),
"127.0.0.1:7001".to_string(),
));
// start gossip server
tokio::spawn(node1.clone().start_gossip_server(7001));
// add peer and start periodic gossip
node1.add_member(Node {
id: "node-2".to_string(),
addr: "127.0.0.1:7002".to_string(),
status: NodeStatus::Up,
}).await;
// gossip every 100ms, mark suspect after 5s
node1.clone().start_periodic_gossip(
Duration::from_millis(100),
Duration::from_secs(5),
);
// after multiple rounds, all nodes converge to same membership view
}
Nodes track heartbeat timestamps and mark unresponsive nodes:
suspect_timeoutsuspect_timeout * 2// node becomes SUSPECT if no gossip for 5s
// node becomes DOWN if no gossip for 10s
node.start_periodic_gossip(
Duration::from_millis(100),
Duration::from_secs(5),
);
Actors register on their local node, and their location spreads via gossip:
// node2 registers an actor
node2.register_actor("user-store".to_string(), "UserStore".to_string()).await;
// after gossip propagates, node1 can lookup the actor
let location = node1.lookup_actor("user-store").await;
// Some(("node-2", "UserStore"))
Actor cleanup: When a node goes DOWN, all its actors are removed from the registry.
ClusterClient combines cluster discovery with remote messaging:
Server setup:
use cineyma::{Actor, Handler, ActorSystem, Context};
use cineyma::remote::{LocalNode, MessageRouter};
use cineyma::remote::cluster::ClusterNode;
use std::sync::Arc;
// actor
struct PingPong;
impl Actor for PingPong {}
#[derive(Clone, prost::Message)]
struct Ping { #[prost(string, tag = "1")] msg: String }
impl Message for Ping { type Result = Pong; }
impl RemoteMessage for Ping {}
#[derive(Clone, prost::Message)]
struct Pong { #[prost(string, tag = "1")] reply: String }
impl Message for Pong { type Result = (); }
impl RemoteMessage for Pong {}
impl Handler<Ping> for PingPong {
fn handle(&mut self, msg: Ping, _ctx: &mut Context<Self>) -> Pong {
Pong { reply: format!("pong: {}", msg.msg) }
}
}
#[tokio::main]
async fn main() {
let system = ActorSystem::new();
let actor = system.spawn(PingPong);
// create cluster node
let node = Arc::new(ClusterNode::new(
"node-2".to_string(),
"127.0.0.1:9002".to_string(),
));
// register actor in cluster
node.register_actor("pingpong".to_string(), "PingPong".to_string()).await;
// create handler
let local_node = LocalNode::new("node-2");
let handler = MessageRouter::new()
.route::<Ping>(local_node.handler::<PingPong, Ping>(actor))
.build();
// start unified server (gossip + actor messages)
tokio::spawn(node.clone().start_server(9002, Some(handler)));
// start periodic gossip
node.clone().start_periodic_gossip(
Duration::from_millis(100),
Duration::from_secs(10),
);
}
Client usage:
use cineyma::remote::{ClusterClient, ClusterRemoteAddr};
// create cluster client
let client = ClusterClient::new(node1.clone());
// create typed remote address (no manual node lookup needed!)
let remote: ClusterRemoteAddr<PingPong> = client.remote_addr("pingpong");
// option 1: low-level send (returns envelope)
let envelope = remote.send(Ping { msg: "hello" }).await?;
let pong = Pong::decode(envelope.payload.as_slice())?;
// option 2: high-level call (auto-decodes response) - recommended
let pong: Pong = remote.call(Ping { msg: "hello" }).await?;
println!("{}", pong.reply); // "pong: hello"
// option 3: fire-and-forget
remote.do_send(Ping { msg: "notify" }).await?;
How it works:
remote.call(Ping { ... }) looks up "pingpong" in cluster registry → finds node-2PongFeatures:
A simple in-memory user store - no locks needed!
use cineyma::{Actor, Handler, Message, ActorSystem, Context};
use std::collections::HashMap;
// messages
struct CreateUser { id: u64, name: String }
struct GetUser { id: u64 }
struct UpdateUser { id: u64, name: String }
struct DeleteUser { id: u64 }
impl Message for CreateUser { type Result = (); }
impl Message for GetUser { type Result = Option<String>; }
impl Message for UpdateUser { type Result = bool; }
impl Message for DeleteUser { type Result = bool; }
// actor
struct UserStore {
users: HashMap<u64, String>,
}
impl Actor for UserStore {}
impl Handler<CreateUser> for UserStore {
fn handle(&mut self, msg: CreateUser, _ctx: &mut Context<Self>) {
self.users.insert(msg.id, msg.name);
}
}
impl Handler<GetUser> for UserStore {
fn handle(&mut self, msg: GetUser, _ctx: &mut Context<Self>) -> Option<String> {
self.users.get(&msg.id).cloned()
}
}
impl Handler<UpdateUser> for UserStore {
fn handle(&mut self, msg: UpdateUser, _ctx: &mut Context<Self>) -> bool {
if self.users.contains_key(&msg.id) {
self.users.insert(msg.id, msg.name);
true
} else {
false
}
}
}
impl Handler<DeleteUser> for UserStore {
fn handle(&mut self, msg: DeleteUser, _ctx: &mut Context<Self>) -> bool {
self.users.remove(&msg.id).is_some()
}
}
Run the chat server example:
cargo run -p chat
Connect with netcat:
nc localhost 8080
See examples/chat/README.md for details.
demonstrates cluster capabilities: gossip membership, actor discovery, location-transparent messaging, and failure detection.
# start first node
cargo run -p distributed-kv -- node-1
# start second node (joins via first)
cargo run -p distributed-kv -- node-2 127.0.0.1:7001
# start third node
cargo run -p distributed-kv -- node-3 127.0.0.1:7001
interactive cli commands:
> set user:1 alice
OK
> get user:1
alice
> members
cluster members:
node-1 @ 127.0.0.1:7001 [Up]
node-2 @ 127.0.0.1:7002 [Up]
node-3 @ 127.0.0.1:7003 [Up]
> actors
registered actors:
kv-store -> node-1 (KVStore)
See examples/distributed-kv/README.md for details.
cineyma is designed for high throughput and low latency. All benchmarks run on a single machine using Criterion.
| Operation | Time | Throughput |
|---|---|---|
| Spawn single actor | 2.2 µs | 454k actors/sec |
| Spawn 10 actors | 22.9 µs | 2.3 µs/actor |
| Spawn 100 actors | 228.8 µs | 2.3 µs/actor |
| Spawn 1000 actors | 1.5 ms | 1.5 µs/actor |
Analysis: Perfect linear scaling. Minor improvement at 1000 actors due to better CPU cache utilization. Each actor gets its own mailbox (bounded channel, default capacity 256) and spawns a tokio task.
| Scenario | Time | Throughput |
|---|---|---|
| 100 msgs (single actor) | 11.2 ms | ~9k msgs/sec |
| 1k msgs (single actor) | 11.6 ms | ~86k msgs/sec |
| 10k msgs (single actor) | 12.9 ms | ~775k msgs/sec |
| 100k msgs (100 actors × 1k each) | 66.5 ms | ~1.5M msgs/sec |
Analysis: The 10ms sleep in the benchmark dominates. Actual message dispatch overhead is negligible - bounded channels provide excellent throughput with minimal overhead. Parallel throughput shows excellent scaling with multiple actors.
| Operation | Latency | Notes |
|---|---|---|
| Sync handler | 17.8 µs | Includes oneshot channel overhead |
| Async handler | 1.28 ms | Includes 10µs simulated async work |
| Batched 10 requests | 25.4 µs | 2.5 µs/req (7x faster) |
| Batched 100 requests | 63.2 µs | 0.6 µs/req (28x faster!) |
Analysis:
join_all shows massive improvements - 100 concurrent requests achieve 28× better per-request latency| Operation | Time | Scaling |
|---|---|---|
| Create gossip (10 nodes) | 8.8 µs | - |
| Create gossip (50 nodes) | 40.6 µs | 0.81 µs/node |
| Create gossip (100 nodes) | 76.0 µs | 0.76 µs/node |
| Merge gossip (50 nodes) | 29.8 µs | Fast even with RwLock writes |
| Convergence (7-node chain) | 112 ms | Includes TCP + serialization |
Analysis:
| Payload Size | Encode | Decode | Round-trip |
|---|---|---|---|
| 64 B | 48 ns | 59 ns | 113 ns |
| 1 KB | 87 ns | 91 ns | 191 ns |
| 16 KB | 5.0 µs | 9.5 µs | 9.6 µs |
| 256 KB | 5.9 µs | 145 µs | 87 µs |
Analysis:
| Operation | Time | Scaling |
|---|---|---|
| Detect suspect (3 nodes) | 309 ms | Within suspect_timeout + gossip_interval |
| Heartbeat check (10 nodes) | 4.8 µs | - |
| Heartbeat check (50 nodes) | 22.6 µs | 0.45 µs/node |
| Heartbeat check (100 nodes) | 45.3 µs | 0.45 µs/node |
Analysis:
# run all benchmarks (~10 min)
make bench
# run specific benchmark
make bench-actor # actor spawn
make bench-msg # message throughput
make bench-rr # request-response
make bench-gossip # cluster gossip
make bench-serial # serialization
make bench-fail # failure detection
# quick smoke test
make bench-quick
# view HTML reports
open target/criterion/report/index.html
graph TB
subgraph ActorSystem
AS[ActorSystem]
REG[Registry]
AS --> REG
end
subgraph Actors
A1[Actor A]
A2[Actor B]
A3[Actor C]
end
AS -->|spawn| A1
AS -->|spawn| A2
A1 -->|spawn_child| A3
subgraph "Message Flow"
M1[Message]
MB1[Mailbox]
H1[Handler]
end
M1 -->|do_send/send| MB1
MB1 -->|deliver| H1
graph TB
subgraph "Client Node"
RA[RemoteAddr]
RC[RemoteClient]
RC -->|creates| RA
end
subgraph "Network"
TCP[TCP Connection]
ENV[Envelope<br/>protobuf bytes]
end
subgraph "Server Node"
LN2[LocalNode]
RS[RemoteServer]
MR[MessageRouter]
H[Handler]
ADDR[Addr]
ACT[Actor]
LN2 -->|creates| H
RS -->|dispatches to| MR
MR -->|routes to| H
H -->|calls| ADDR
ADDR -->|sends to| ACT
end
RC -->|sends| TCP
TCP -->|delivers| RS
ACT -->|returns| H
H -->|responds via| TCP
TCP -->|receives| RC
style RC fill:#4a9eff
style LN2 fill:#4a9eff
style ACT fill:#22c55e
graph TB
subgraph "Node 1"
CN1[ClusterNode]
CC1[ClusterClient]
A1[Actor A]
CN1 -->|provides registry| CC1
CC1 -->|sends to| A1
end
subgraph "Node 2"
CN2[ClusterNode]
A2[Actor B]
A3[Actor C]
CN2 -->|hosts| A2
CN2 -->|hosts| A3
end
subgraph "Node 3"
CN3[ClusterNode]
A4[Actor D]
CN3 -->|hosts| A4
end
CN1 <-->|gossip| CN2
CN2 <-->|gossip| CN3
CN1 <-->|gossip| CN3
CC1 -.->|"1. lookup 'ActorB'"| CN1
CN1 -.->|"2. node-2"| CC1
CC1 ==>|"3. send msg"| A2
style CN1 fill:#4a9eff
style CN2 fill:#4a9eff
style CN3 fill:#4a9eff
style CC1 fill:#22c55e
Multiplexed Protocol:
graph LR
subgraph "ClusterMessage (oneof)"
G[Gossip]
E[Envelope<br/>Actor Message]
end
subgraph "Single Port"
S[Server]
end
G -->|route to| S
E -->|route to| S
S -->|gossip handler| M[Membership Merge]
S -->|actor handler| A[Actor Dispatch]
style G fill:#fbbf24
style E fill:#22c55e
Coming Soon:
MIT