One of the promises of Elixir/Erlang is that you can call a process/Actor on different machine just like you can one on same once you put together a bunch of machines in a cluster
> Additionally ractor has a companion library, ractor_cluster which is needed for ractor to be deployed in a distributed (cluster-like) scenario. ractor_cluster shouldn’t be considered production ready, but it is relatively stable and we’d love your feedback!
OTP is super powerful out of the box :)
And
https://github.com/wmealing/gleam-otp-design-principals/blob...
Most people most of the time are just better hooking up one of the many, many other event busses that both Rust and Erlang can speak to and working across that.
The closest you could get to this would be:
1. assuming a running process pA on nodeA, registered in a cluster-level process registry by name N;
2. send the process pA's state to a function on node B, which will use it to start a process pB on node B that is a clone of pA;
3. update the cluster-level process registry to point N at pB.
4. if you want to be really clever — tail-call (or code-upgrade) pA out of its existing loop, into code that makes pA act as a proxy for pB, so that anything sending a message to pA ends up talking to pB through pA.
But this isn't "sending a process"; pA itself remains "stuck" on nodeA (node IDs are actually embedded in PIDs!), and there's especially no native mechanism that would do things like rewrite the links/monitors on other processes that currently point to pA, to instead point to pB (so if pA was a supervisor, you couldn't "re-parent" the children of pA over to pB.)
True, but find me one language that someone, somewhere didn't over- something. Either over-engineer or over-simplify, or over-use.
People get excited about technology and try to push the envelope on its usage. That's true of any widely used tech.
Hitting the golden middle is notoriously hard, especially when it is not the same middle for everyone.
> the whole ecosystem
What precisely do you mean? Sure Spring is notorious for this, but not the wider ecosystem.
Sure some libraries might not be using SemVer, but Maven itself predates it as well.
Nobody can blame them for choosing the right tool for the job, but many held them in contempt for license violation.
The actor model is imo a great way of doing concurrency in the absence of the data race guarantee that safe rust provides at compile time. If you know you have no data races, I don't think actors give you that much.
That said, some people just really like actors as a mental model and/or they want to interoperate with actor-based systems written in other languages or provide an actor substrate written in rust that will be embedded into another language perhaps? It's definitely a niche usage.
This is me, for sure. "little box does one thing, processes one thing at a time, maintains the state of one thing, talks to different pieces of the system through this predefined method", etc, that actors give me is very easy for me to reason about and work with, so I use it and love it.
Github stats show some uptake so clearly someone finds them useful
Also, didn't what I said about usage better map to number of dependent crates or downloads by cargo? Both are listed on https://crates.io/crates/ractor
[1] https://slawlor.github.io/ractor/assets/rustconf2024_present...
Homogenous tasks should use an approach that is aware of and takes advantage of the homogeneity, e.g., the sort of specific optimizations a framework might make to orchestrate data flows to keep everything busy with parallel tasks.
You can use actors for orchestration, but you're really just using them because they're there, not because they bring any special advantages to the task. Any other solution that works is fine and there would never be a particular reason to switch to actors if you already had a working alternative.
There's also the entire category of "data lakes" and other nouns that have "data" applied as an adjective that includes various orchestration techniques because just being storage isn't enough, that is its own entire market segment.
At the same time Web servers seem a bit better as I don't really want to be figuring out how many actors of each "class of actor" to spawn and to maybe babysit each of them, and for debugging the web stack has more tooling and is better understood than any particular actor system. The advantages seem mostly out of experience and a lot of time. Maybe some iteration of an Actor framework will render Web servers too quirky/unsafe/slow/complex.
There are good answers already on the actual "why", but I'd like to point out that "none of them get any usage" is a much better reason to build a new one than "there's already a library dominating the ecosystem". Clearly none of the existing libraries, for one reason or another, have been deemed interesting enough, let's see someone else try
- One decides that they want to use actors
- Look for frameworks
- find those that either abandoned or don't vibe with one
- start your own
- passion runs out for project that needed actors
For context, the `async_trait` crate makes futures from trait methods that are wrapped in `Pin<Box<dyn Future<...>>>`, which means that every async call to a trait method must make a heap allocation. This is currently a necessary thing to do if async trait methods were invoked with dynamic dispatch (through `dyn Actor`), but the `Actor` trait has associated methods, so that is already not generally possible.
I realize that methods on the `Actor` trait return futures that are `Send`, but specifically for an actor framework that feels like a very specific design choice that isn't universally good or necessary. Another design would give let the spawned task that executes the actor's messages exclusive access to the actor (so `handle()` could take `&mut self`).
I've ended up implementing a simple alternative design in my own project (it's not fundamentally very hard, but doesn't have all the features, like supervision) because the per-message heap allocations and internal locking became wasteful for my use case.
From their crate documentation[0]:
> The minimum supported Rust version (MSRV) is 1.64. However if you disable the `async-trait` feature, then you need Rust >= 1.75 due to the native use of `async fn` in traits.
https://discord.com/channels/734893811884621927/127043967262...
Actix-web remains healthy in this regard, it was specifically just about the actix crate.
Show HN: Kameo – Fault-tolerant async actors built on Tokio - https://news.ycombinator.com/item?id=41723569 - October 2024 (58 comments)
Also another square that I have not circled with async actor other than actix is that all of them use mutable self and they dont have a great way to have long running tasks. Sometimes you would want to call a remote server without blocking the actor, in actix this is easy to do and you unamed child actors to do that. All those newer frameworks don't have the primitives.
type Msg = MyFirstActorMessage;
As demonstrated in the linked tutorial, Ractor passes its handlers `&self` with `&mut State`.
struct State { counter: usize, control: mpsc::Receiver<Msg> }
struct StateActor { addr: mpsc::Sender<Msg> }
enum Msg {
Increment { reply: oneshot::Sender<()> }
}
impl StateActor {
pub async fn increment(&self) {
let (tx, rx) = oneshot::channel();
let msg = Msg::Increment { reply: tx };
self.addr.send(msg).await.unwrap();
rx.await.unwrap();
}
}
impl State {
fn start(self) {
tokio::spawn(async move {
/* ... tokio::select from self.control in a loop, handle messages, self is mutable */
/* e.g. self.counter +1 1; msg.reply.send(()) */
})
}
}
// in main
some_state_actor.increment().await // doesn't return until the message is processed
A StateActor can be cheaply cloned and used in multiple threads at once, and methods on it are sent as messages to the actual State object which loops waiting for messages. Shutdown can be sent as an in-band message or via a separate channel, etc.To me it's simpler than bringing in an entire actor framework, and it's especially useful if you already have control loops in your program (say, for periodic work), and want an easy system for sending messages to/from them. That is to say, if I used an existing actor framework, it solves the message sending/isolation part, but if I want to do my own explicit work inside the tokio::select loop that's not strictly actor message processing, I already have a natural place to do it.
To me, Rust has adequate concurrency tooling to make ad hoc actor designs roughly on par with more developed ones for many tasks, but supervision is both highly valuable and non-trivial. Briefly, I'd say Rust is top-notch for lower-level concurrency primitives but lacks architectural guidance. Supervisor trees are a great choice here for many applications.
I've tried implementing supervision a few times and the design is both subtle and easy to get wrong. Even emulating OTP, if you go that route, requires exploring lots of quiet corner cases that they handle. Reifying this all into a typed language is an additional challenge.
I've found myself tending toward one_for_all strategies. A reusable Fn that takes some kind of supervisor context and builds a set of children, potentially recursively building the supervision tree beneath, tends to be the best design for typed channels. It forces one_for_all however as it's a monolithic restart function. You can achieve limited (batch-y) rest_for_one by having the last thing you boot in your one_for_all be another one_for_all supervisor, but this feels a little hacky and painful and pushes back against more granular rest_for_one designs.
You then probably want a specialized supervisor for one_for_one, similar to Elixir's DynamicSupervisor.
That, and preemptive scheduling. And being able to inspect / debug / modify a live system. Man, these actor frameworks just make me appreciate how cool Erlang is.
To me, your supervision tree should be dedicated to that purpose and forms a superstructure relating entirely to spawning, resource provisioning, restarting, shutdown. Part of what makes it nice in Erlang is that it's consistent and thoughtless, just part of designing your system instead of being behavior you have to write or worry about much.
Here with Ractor they've built a special monitoring channel and callbacks into Actor (`handle_supervisor_evt`). This implies at some point one might write a nice supervisor in their framework that hopefully has some of those properties.
> When designing ractor, we made the explicit decision to make a separate state type for an actor, rather than passing around a mutable self reference. The reason for this is that if we were to use a &mut self reference, creation + instantiation of the Self struct would be outside of the actor's specification (i.e. not in pre_start) and the safety it gives would be potentially lost, causing potential crashes in the caller when it maybe shouldn't.
> Lastly is that we would need to change some of the ownership properties that ractor is currently based on to pass an owned self in each call, returning a Self reference which seems clunky in this context.
- handle messages sequentially per-actor (like Ractor, and like Bastion in practice)
- create your own future trait without async/await (like Actix)
I keep hoping the language team will get coroutines off the ground, but it hasn't happened yet.
Actix does exactly that, but it needs a bespoke ActorFuture trait with extra poll arguments. But until https://lang-team.rust-lang.org/design_notes/general_corouti... gets worked through, there is no way for actix to support async/await.
If I'm understanding, then I don't yet follow the use case. You want to have multiple actors reading from the same message queue? Or different concurrent chunks inside of the actor?
This feels to me to strain against the actor design pattern a lot. I feel this often, that futures are somewhat in tension with actors. There are too many options.