Hacker News new | past | comments | ask | show | jobs | submit login
Ractor – a Rust Actor Framework (slawlor.github.io)
149 points by todsacerdoti 2 days ago | hide | past | favorite | 74 comments





I'm always most curious with these frameworks how they're considering supervision. That's the real superpower of OTP, especially over Rust.

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's the real superpower of OTP, especially over Rust.

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.



It's clear that there's some beginning of this in place. They reference it as initial in the docs there and it's missing quite a bit. I'm not a huge fan of what I'm seeing here where each actor is implicitly itself and a supervisor (i.e., ActorRef makes reference to the actor's children). I think I saw that first in Akka and while it makes sense in theory, I don't like the practice.

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.


At $dayjob I've taken to just implementing my own actor-like model when I need interior mutability across tasks. Something like:

    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.


I’m not saying that you can’t use another project’s name, but there already exists an actor framework named Ractor, but this one written in Ruby.

https://docs.ruby-lang.org/en/master/Ractor.html


I was about to mention this, Ractor is already a name used to describe the Ruby Actor class. But I guess this is only known within the Ruby Community? It would’ve taken one search to find out about this. Either way, since both names are bounded to their languages, the context should make it clear what is referred to. And to be fair, the name Ractor makes sense for both languages.

That looks interesting! What's the distributed story of Ractor? Would you need a central store like Redis to serve as Actor registry?

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


Seems to be supported

> 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!


Elixir/erlang/gleam also have the advantage that many libraries are written as servers and so automatically gain the benefits and resilience. Something all these actor frameworks can’t give you.

With gleam, how do you call an actor on another machine when you have a cluster? Do you need something like redis?

The Erlang runtime and EPMD takes care of this for you. Every node in the cluster gets a name, so you can address it directly, and, every PID is unique across nodes on a cluster, so you can send messages to processes (actors) no matter where they are in the cluster.

OTP is super powerful out of the box :)



I personally find actors in rust to be too much of a head ache. You might as well use Erlang (or elixir) and do your heaving lifting in rust with somethign like https://github.com/rusterlium/rustler

Related from one month ago: Kameo - https://github.com/tqwewe/kameo

Show HN: Kameo – Fault-tolerant async actors built on Tokio - https://news.ycombinator.com/item?id=41723569 - October 2024 (58 comments)


I personnaly don't like the single enum model for messages. I prefer the generic Handler trait model.

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.


> all of them use mutable self

As demonstrated in the linked tutorial, Ractor passes its handlers `&self` with `&mut State`.


It looks like the message type can be anything:

    type Msg = MyFirstActorMessage;

Actors in Rust still kind of suck because there are no first-class resume arguments. In order to provide a `&mut State` you have to either:

- 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.


What is a first-class resume argument? How does it enable &mut State and non-sequential messaging within Rust's current futures design?

Right now the only arguments to Future::poll are self and the task scheduler context. Any borrows of mutable state have to be captured by self, which means only one future with access to the state can be inflight at a time. If `&mut State` was an argument on resume, then the actor could schedule many futures simultaneously, which each getting temporary access to the state whenever it makes progress.

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.


Ractor is cool, but I've been wondering why it uses the async `async_trait` rather than native async traits. Is it just because it came out before async traits were stabilized, and now code relies on it and it would be a breaking change to migrate?

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.


> I've been wondering why it uses the async `async_trait` rather than native async traits

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.

[0]: https://docs.rs/ractor/0.13.0/ractor/index.html


Sorry for being so negative but why do people keep building these Actor frameworks for Rust? None of them get any usage.

This seems to be used at Meta: "Ractor had a session at RustConf’24 about how it’s used for distributed overload protection in Rust Thrift servers at Meta."

Their presentation [1] contains a few slides with the motivation for this framework (they have existing Thrift services in C++ and Python, and want to start using Rust too) and why actors (answer: they scale better than a naive solution?). Also a slide "why a new framework" when so many already exist for Rust. (Answer: many are dead/unsupported, are too far from Erlang principles, not flexible enough, or use custom runtimes while ractor builds on Tokio.)

[1] https://slawlor.github.io/ractor/assets/rustconf2024_present...


Tiny bit of context I'd also call out is that one of the primary authors comes from WhatsApp IIRC, with an extensive Erlang background.

If you've ever used an ML descendant you'll miss sum types wherever you go. If you've ever used Erlang you'll miss the OTP actor implementation wherever you go.

For reasonable reasons in my view.

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.


> That said, some people just really like actors as a mental model

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.


> Sorry for being so negative but why do people keep building these Actor frameworks for Rust? None of them get any usage.

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


I think it's an (so-far) endless cycle of:

- 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


I guess it's a bit hard to change the way of thinking and structuring programs/applications. We mostly do either blocking procedural things, or web servers. Maybe actors can help remove the network from web servers where they add unnecessary complexity/overhead.

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.


I've used something in the general shape of actors (though mostly handrolled) when implementing websocket APIs. If I had to wire up lots of or several layers of those then using a library for that would start to make sense.

Because usage isn't the only motivator? Because they hope they'll get usage (can yuo see the future that non will get any usage?)?

Can it be used for distributed computation for ML or data science tasks (eg., like what dask does for Python)?

Actors are better suited for highly heterogeneous task sets, where am actor or small set of them correspond to some task and you may have thousands or more.

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.


Are you aware of any framework or could use for such tasks (ML and alike)?

The problem you face is not so much a lack of such things existing but such a staggering multiplicity that it is hard to poke through them all, and despite the sheer quantity, you may still find that your particular problem doesn't fit any of them terribly well. You can find anything from OpenMP, which amounts to "let's try to treat a whole bunch of resources a lot like one big computer" https://en.wikipedia.org/wiki/OpenMP, through things like Kubernetes which can be used to deploy all kinds of "worker nodes" for all sorts of tasks even if it doesn't do the orchestration of the task itself, and so many combinations of anything in between, plus you have all sorts of clustering technologies, message bus technologies, on-demand VMs spinning up, so many primitives that even if they aren't designed for this can be relatively easily put together into whatever it is you actually need that it is all rather about bewildering abundance than shortage.

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.


> None of them get any usage.

Github stats show some uptake so clearly someone finds them useful


I don't think it's a good metric, does it get stars because it's cool, or because people are building things with it while taking advantage of the Actor model?

well it's more of a metric than you've provided

Why do you get so defensive?

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


It’s kind of like Java. It’s a way to feel like you’re doing work without actually doing anything

Well my phone, TV and blue ray player do lots of useful stuff.

Displaying ads?

Powered by Java, in an a market share Desktop Linux will never achieve.

Linux has a license that protects it from the class of no-holds-barred codebase abuse Java suffers from, for one.

Apparently OEMs aren't aware of such marvel.

They check in periodically, mostly for posterity: https://github.com/teslamotors/linux

Nobody can blame them for choosing the right tool for the job, but many held them in contempt for license violation.


Couldn’t have said it any better myself

Except it makes no sense. Java is a language that, while idiomatically a bit boilerplatey, is generally a workhorse for getting shit done.

No I mean in Java it’s common to write extensive boilerplate and abstractions just bc it feels constructive. Java is extremely useful, but business use of it is not so nice

It’s everything but that. It’s the instrument of over engineering. Maybe not the language itself (it was the Rust if its time, everyone wanted to use it for everything, even if it made no sense), but the whole ecosystem just prefers complexity and breaking changes. Maybe it’s a coping mechanism for devs stuck with it, or I don’t know.

> it's the instrument of over engineering.

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.


This is nice. Any way to also make it an Erlang complient node so it can be called from a erlang cluster?

Making something Erlang-compliant is actually fairly heavy duty. It's more than just "you've got actors, I've got actors, let's play together!"... you have to support the Erlang term format specifically, so on the Rust side you would need to be able to convert to and from all the state you actually want into Erlang terms, including PID references, you need to support Erlang's linking capability, you need to support Erlang's name server lookups, you need to support Erlang's specific messaging semantics for out-of-order message handling and mailboxes, binary sharing, probably some other things I'm not remembering. It's not an impossible amount of work but it's not in the "couple of weeks of spare time for a motivated developer" range either.

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.


Not only that, but you need a full BEAM-compatible VM, as you can send entire processes across nodes.

No you can't.

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.)


No, there are cnodes.

What's the benefit of having a State type, instead of using handle(&mut self), pre_start() -> Self, and putting the state inside the type that implements the actor?

From the README (https://github.com/slawlor/ractor?tab=readme-ov-file#differe...):

> 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.


There’s also actix: https://github.com/actix/actix

It's been informally discouraged for use in net-new projects via a chat message from one of the maintainers during this year's RustConf.

https://discord.com/channels/734893811884621927/127043967262...

Actix-web remains healthy in this regard, it was specifically just about the actix crate.


This link just goes to a blank discord page for me as if something is trying to load. I don't think these are shareable if you aren't in the server.

I use actix_web for all my web server stuff, its wonderful, super fast, really easy to use and not that easy to fuck up.

I know it seems unintuitive but actix-web is largely unrelated to actix nowadays: https://actix.rs/docs/whatis/

Well someone got inspired by Rubys Ractors hence the naming?

https://docs.ruby-lang.org/en/master/ractor_md.html


Unlikely, the contributors don't seem to have meaningful Ruby backgrounds. Isn't it a simpler supposition that the naming intent was merely (R)ust Actors?

well, it's "r" + "actor" - just a coincidence that both "ruby" and "rust" started with an R!

I have to wonder if Ractor was inspired from The Diamond Age.

Yeah bad choice of naming here. Oddly this project shows up first on DDG for “ractor” despite prevalent Ruby ractor documentation.

Seeing "channel" and "actors can inter-communicate, via remote procedure calls" do I understand this correctly this is not an actor model implementation?

how does it compare with bastion?



Consider applying for YC's W25 batch! Applications are open till Nov 12.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: