Kameo - an Async Actor library built on Tokio
I've built an actor library on top of tokio (along with the other dozens that exist in the Rust ecosystem). But I wanted to share to get feedback and in case it may be of any use to anyone.
Kameo came about after iterating on a previous project with heavy use of macros, and I thought it can be done much cleaner without any macros.
https://github.com/tqwewe/kameo
The readme has a great overview of the library, but it's essentially a wrapper for spawning tokio tasks, and communicating with them via messages and signals over a mpsc channel.
As per the readme, some key features include:
- Async: Built on tokio, actors run asyncronously in their own isolated spawned tasks.
- Supervision: Link actors, creating dependencies through child/parent/sibbling relationships.
- MPSC Unbounded Channels: Uses mpsc channels for messaging between actors.
- Concurrent Queries: Support concurrent processing of queries when mutable state isn't necessary.
- Panic Safe: Catches panics internally, allowing actors to be restarted.
It does come with a helper #[actor] macro for use on impl blocks to generate message structs and implement them from a simple method on your actor.
Here's an example of defining an actor, messages, and spawning it.
// Define the actor state
struct Counter {
count: i64,
}
impl Actor for Counter {}
// Define messages
struct Inc { amount: u32 }
impl Message<Counter> for Inc {
type Reply = i64;
async fn handle(self, state: &mut Counter) -> Self::Reply {
state.count += self.0 as i64;
state.count
}
}
// Spawn the actor
let counter_ref: ActorRef<Counter> = Counter { count: 0 }.spawn();
// Send a message to it, receiving the reply
let count = counter_ref.send(Inc(42)).await?;
println!("Count is {count}");
How is Kameo different from other libraries?
In contrast with Actix, I've always struggled to comfortably use actix, especially when it comes to running async code. Kameo's message handlers are always async, so no special apis are needed for async stuff. There's also a very unscientific benchmark done comparing Actix with Kameo on the readme, it came out to be 13x faster than Actix for sending a simple message.
I've used some other libs over time, and most of them provide some kind of context along with message handlers. Kameo uses tokio::task_local internally to set the actors ref, so you can send messages to itself with self.actor_ref().send_async(...)?.
Kameo also offers actor linking (inspired by erlang), where actors can be notified of eachother when one dies. Features like this have default behaviour impemented in the Actor trait, but can be overwritten per actor.
Finally, Kameo has a concept of "queries", which are messages that only have read-only access to the actors state. This means multiple queries can be processed concurrently.