Skip to content

runtime: write Registry#261

Open
npry wants to merge 4 commits into
mainfrom
npry/rt.registry
Open

runtime: write Registry#261
npry wants to merge 4 commits into
mainfrom
npry/rt.registry

Conversation

@npry

@npry npry commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #253.

Write a Registry actor in roughly the Erlang model allowing actors to be registered and looked up by name. The Registry can also handle forwarding messages for registered actors, which can be useful for message-ordering purposes (the lookup and send are atomic from the perspective of the forwarding actor).

Notably, each name is registered in the scope of a single actor type. This is a consequence of kameo's design trying to enforce strong message typing: the ActorRef type is a pid augmented with the actor type A, meaning that a collection of ActorRefs simply can't be held generically as ActorRefs (which is the only type you can send a message to because the A can be used to prove that A can handle the message you're trying to send it). To erase the actor type, you need to hold Box<dyn Any> and downcast later. But it's not possible to recover the actor type A from a generic incoming message (which you might want to forward): the sender must specify the type of the actor they're trying to communicate with. Hence, names overlapping between actors doesn't make sense and Registry separates them into what are essentially namespaces by TypeId::of::<A>().

This is in the interest of supervision; as in Erlang, when an actor process dies and a supervisor restarts the process, its old pid (ActorRef) doesn't address it anymore. So you functionally need a naming service to resolve the semantic, symbolic name ("the control plane actor") to the actual pid (ActorRef) it currently has at the time you're trying to send a message.

kameo's built-in registry

Kameo has a built-in actor registry which provides ActorRef::register(name). We don't use this for a few reasons:

  • The default instance providing ActorRef::register is global, which would break the ability to run more than one runtime in the same process without adding namespacing logic, which would require the runtimes to be aware of each other somehow (probably through more global state). I wanted to avoid that.
  • You can make your own instance with kameo::Registry::new, but the implementation is just a wrapper around a hashmap. It doesn't run as an actor. That's not ideal for observability or message-ordering — you'd need to stick it behind a mutex to run it like this, which is what the global one does. This also makes it difficult to wait for another actor to register itself, which this implementation provides
  • It puts all actor types in the same namespace but kind of requires you to downcast twice. This felt awkward/unnecessary

@npry npry force-pushed the npry/rt.registry branch from f2b5d00 to e5b3bc4 Compare June 30, 2026 20:33
@npry npry marked this pull request as ready for review June 30, 2026 21:17
@npry npry force-pushed the npry/rt.registry branch from e5b3bc4 to d7cbfbd Compare June 30, 2026 23:22
@npry npry force-pushed the npry/bump-kameo branch from 2c92ba0 to ea5a246 Compare June 30, 2026 23:23
Base automatically changed from npry/bump-kameo to main July 1, 2026 00:47
@npry npry force-pushed the npry/rt.registry branch from d7cbfbd to c4b8e6e Compare July 1, 2026 00:50
npry added 4 commits June 30, 2026 21:09
Signed-off-by: Nathan Perry <[email protected]>
Change-Id: I1da3320594c3f42f0b7350f137e92b2d6a6a6964
Signed-off-by: Nathan Perry <[email protected]>
Change-Id: I3674e34c01786579d90d26acd040cec26a6a6964
Signed-off-by: Nathan Perry <[email protected]>
Change-Id: I3674e34c01786579d90d26acd040cec26a6a6964
Signed-off-by: Nathan Perry <[email protected]>
Change-Id: I0f032c94121fee97b846a4585d89fbee6a6a6964
@npry npry force-pushed the npry/rt.registry branch from c4b8e6e to 462794f Compare July 1, 2026 01:09

@nrc nrc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, only significant comments are a couple of possible simplifications.

Comment thread ts_runtime/src/env.rs
Ok(())
}

/// Register an actor in the registry as the canonical actor of its type.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If name is specified, is this comment still correct or should it only be unnamed actors which are type-canonical?

Comment thread ts_runtime/src/env.rs
where
A: kameo::Actor + Any,
{
let aref = self

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could this call lookup_opt to avoid the code dup?

Comment thread ts_runtime/src/env.rs
self.resolve_aref(aref)
}

/// Send an ask to an actor in the registry.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you document the meaning of the wait arg please? (Unless it is a Kameo convention)

#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
struct Id {
actor_ty: TypeId,
name: SmolStr,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth using a SmolStr here rather than a plain String?

/// names are permitted to overlap between actors of different types, as there can't be a collision
/// between them.
///
/// The conventional structure of names in this registry includes the idea of a "canonical" actor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if the added complexity of canonical actors is worth it. Presumably, users could always just a pick a name or use a conventional name? I suppose it's not a huge amount of complexity, but it does complicate the API a little.

where
A: kameo::Actor + Any,
{
/// Construct a register request for the canonical actor of type `A`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Construct a register request for the canonical actor of type `A`.
/// Construct a register request for a named or canonical actor.

{
pub fn new(name: Option<SmolStr>) -> Self {
Self {
id: if let Some(name) = name {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could use Id::new

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants