Restioson/xtra

Remove `ActorManager` in favor of executor specific modules / functions

thomaseizinger opened this issue · 13 comments

Currently, we have Actor::create and the ActorManager struct which allows us to conveniently spawn actors in a single line onto a particular runtime:

let address = MyActor.create(None).spawn_global();

I am proposing to remove the create function and ActorManager in favor of executor-specific modules that will fulfill the same job. These APIs are of similar vein as #133.

let address = xtra::tokio::spawn_unbounded(MyActor);
let address = xtra::tokio::spawn_bounded(MyActor, 5);
let address = xtra::wasm_bindgen::spawn_bounded(MyActor, 5);

A slightly alternative API could be to make the user pass the Mailbox in which would reduce the cardinality a bit:

let address = xtra::tokio::spawn(MyActor, Mailbox::unbounded());
let address = xtra::tokio::spawn(MyActor, Mailbox::bounded(5));
let address = xtra::wasm_bindgen::spawn(MyActor, Mailbox::bounded(5));

Another variation could be to have more top-level functions instead of modules. Arguably, if there is only one function per module, it might not be worth it.

let address = xtra::spawn_tokio(MyActor, Mailbox::unbounded());
let address = xtra::spawn_tokio(MyActor, Mailbox::bounded(5));
let address = xtra::spawn_wasm_bindgen(MyActor, Mailbox::bounded(5));

Personally, I think I prefer the last one because it has a low cardinality, is expressive but also not too verbose.

Technically, all of these APIs could also go into an extension crate, one per executor.

let address = xtra_tokio::spawn(MyActor, Mailbox::unbounded());

Technically, all of these APIs could also go into an extension crate, one per executor.

let address = xtra_tokio::spawn(MyActor, Mailbox::unbounded());

This is a bit tedious though. With extension crates, I think it is nice to have a "meta" crate that re-exports the various sub-crates.

Assuming that xtra is going to be our meta crate, we would have to move all the core types into something like xtra_core (maybe even have an xtra_channel, not sure if that is worth it) and then have xtra depend on that and re-export it. An extension crate like xtra_tokio would also depend on xtra_core to get access to types like Mailbox and would also be pulled in by xtra under a feature-flag.

After this analysis, I think my personal preference is to for now not go for an extension crate but simply feature-flag this in the primary xtra crate. We would be able to go from single crate to meta-crate without any breaking changes so we can do it in a patch release.

This is nice, I think. In the olden days of xtra 0.1, ActorManager used to actually manage the actor 😄 Now, Context does it, so the name makes vastly less sense. It's essentially a tuple with some extension methods at this point, so this would be a nice solution, I think.

On the topic of executor-specific extension crates, I think that xtra::spawn_tokio(MyActor, Mailbox::unbounded()) probably has a minimal convenience advantage over tokio::spawn(xtra::run(MyActor, Mailbox::unbounded())). It is slightly shorter, but loses the main (IMO) advantage of spawn_global being a postfix rather than a prefix

On the topic of executor-specific extension crates, I think that xtra::spawn_tokio(MyActor, Mailbox::unbounded()) probably has a minimal convenience advantage over tokio::spawn(xtra::run(MyActor, Mailbox::unbounded())).

The main reason why tokio::spawn(xtra::run(MyActor, Mailbox::unbounded())) can't work it because you are not going to get ahold of the Address :)

At least for what I proposed in #133, run takes a Mailbox but Mailbox::unbounded() would return both, the Address and the Mailbox. xtra_tokio::spawn would be implemented like this:

fn spawn<A>(actor: A, tuple: (Address<A>, Mailbox<A>)) -> Address<A> {
	let (address, mailbox) = tuple;
	tokio::spawn(run(actor, mailbox));

	address
}

The main reason why tokio::spawn(xtra::run(MyActor, Mailbox::unbounded())) can't work it because you are not going to get ahold of the Address :)

That's true, it does at least cut down a line

It is slightly shorter, but loses the main (IMO) advantage of spawn_global being a postfix rather than a prefix

From my perspective, the most important thing is to provide a way of how people can spawn an actor in a single line of code. How we do it is not as important. I am personally not necessarily a fan of traits if you don't abstract over them. You need to import traits before you can use them, they add a line to the use statements and they aren't as discoverable in the documentation.

You need to import traits before you can use them, they add a line to the use statements and they aren't as discoverable in the documentation.

I would argue that this point is somewhat moot, given that a lot of the rust ecosystem makes extensive use of extension crates.

Regardless, if we had an extension crate, both could probably be supported

You need to import traits before you can use them, they add a line to the use statements and they aren't as discoverable in the documentation.

I would argue that this point is somewhat moot, given that a lot of the rust ecosystem makes extensive use of extension crates.

An extension crate does not necessarily mean that one has to use traits right?

Oops, typo! I meant extension traits. E.g StreamExt, SinkExt, FutureExt to name some from futures which is proximate to xtra

Oops, typo! I meant extension traits. E.g StreamExt, SinkExt, FutureExt to name some from futures which is proximate to xtra

Right, those are fairly popular yes. Postfix is useful when things need to be chained. That isn't necessarily the case here right?

That is true, though postfix also formats nicer sometimes. It's probably not a huge concern though

My goal would be to keep these so short that they will typically fit into a single line of code, unless the actor's name is really long. Without the actor name, the longest variant would be spawn_wasm_bindgen with the following line using 63 characters:

let address = xtra::spawn_wasm_bindgen(, Mailbox::unbounded());

That leaves 27 characters for the actor type + constructor, assuming they use ::new(), that still leaves 20 characters for the actual type.

The other thing is, I am hoping that users will - inspired by the free function design - actually write their own spawn function, specific to the choices they make in their application. For example:

/// Spawns a new actor.
///
/// The actor instance will be created via the `Default` trait
/// with a mailbox size of 10 and spawned into the tokio runtime.
pub fn spawn_default_actor<T>() -> Address<T> where T: Default {
	let actor = T::default();
	xtra::spawn_tokio(actor, xtra::Mailbox::bounded(10))
}

This function is for obvious reasons way too opinionated to live within xtra but for an application, it would totally make sense to standardize things like mailbox size, runtime and construction via Default.

We could also go the route of not providing any spawn function at all but I think that is a bit bare bones and makes getting started a lot more annoying. The free function design however lends itself naturally to be abstracted away and thus scales easily with bigger codebases.