-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Decide on message API - allow multiple message types per actor? #11
Comments
I haven't yet formed opinion on this, but some considerations:
|
This has actually been one of the biggest pain points for me in the current actor API. Let's say I have two actors, A and B. A and B used to not communicate at all with each other, but suddenly a new requirement arises and I want to start sending messages from A to B. If B's message type used to just be a simple struct, I now I have to refactor that into an enum and add a variant so A can send the type it desires to B. I then have to go through all instances in the code where other actors sent messages to B, and change them to use the new enum message. Contrast this with the trait approach, where I simply
My intuition says "yes" here, especially if we wanted priority channels. We're also not tied to crossbeam for channels, there are other implementations like flume which are worth investigating. We could possibly use something like the A procedural macro is another possibility but I'd leave that as more of a "last resort" solution. |
Yeah, I agree this situation of making it easier to receive multiple message types is one that needs to be solved cleanly in a new design, so either adding a struct Hello;
struct Goodbye;
enum MyMessage {
Hello(Hello),
Goodbye(Goodbye)
}
impl From<Hello> { ... }
impl From<Goodbye> { ... }
impl Actor for MyActor {
type Message = MyMessage;
...
} so that you can eventually do something like: self.my_actor.send(Goodbye)?; The downside of the above approach that we've been using is that any struct that wants to hold a pub struct OtherActor<M: From<Goodbye>> {
next: Recipient<M>,
} which is kind of gross looking a lot of the time, especially when you are holding multiple recipients in the same struct. If there's some type magic that makes that pattern cleaner, I'd say it's also a possible solution that would make it more ok to keep the enum approach. |
Some research into how Actix does things:
|
Some very quick research into
|
I.e before this change, Input, Damper actors assumed they are fed into Mixer, but Delay assumed it is fed into something that expects naked `Chunk`. This made no sense, and made actors dependent on how they are wired in the system, which is an antipattern. This is an artificial change that demonstrates problems outlined in #11 (comment)
...within bounds that that M: Into<N> for Recipient<M> for Actor<Message = N>. Fixes #11 (in its narrow sense) without creating multiple channels per actor and without boxing the messages transferred. Has been made possible by removal of the ability to retrieve back the failed-to-send message in one of the earlier commits. The whole trick is in defining a `Sender<M>` trait to abstract crossbeam::Sender, implementing `<M: Into<N>> Sender<M>` for `crossbeam::Sender<N>`, and boxing *that* implementation in Addr::recipient(). All generic parameters in the echo example have disappeared without losing any flexibility, yay! Speaking strict semver, this is an API-breaking change, but e.g. the media_pipeline compiles and works unchanged. There was a lot of design choices with subtle differences (like making Recipient a trait), this change preferred ones that meant a smaller change.
I.e before this change, Input, Damper actors assumed they are fed into Mixer, but Delay assumed it is fed into something that expects naked `Chunk`. This made no sense, and made actors dependent on how they are wired in the system, which is an antipattern. This is an artificial change that demonstrates problems outlined in #11 (comment)
...within bounds that that M: Into<N> for Recipient<M> for Actor<Message = N>. Fixes #11 (in its narrow sense) without creating multiple channels per actor and without boxing the messages transferred. Has been made possible by removal of the ability to retrieve back the failed-to-send message in one of the earlier commits. The whole trick is in defining a `Sender<M>` trait to abstract crossbeam::Sender, implementing `<M: Into<N>> Sender<M>` for `crossbeam::Sender<N>`, and boxing *that* implementation in Addr::recipient(). All generic parameters in the echo example have disappeared without losing any flexibility, yay! Speaking strict semver, this is an API-breaking change, but e.g. the media_pipeline compiles and works unchanged. There was a lot of design choices with subtle differences (like making Recipient a trait), this change preferred ones that meant a smaller change.
I.e before this change, Input, Damper actors assumed they are fed into Mixer, but Delay assumed it is fed into something that expects naked `Chunk`. This made no sense, and made actors dependent on how they are wired in the system, which is an antipattern. This is an artificial change that demonstrates problems outlined in #11 (comment)
...within bounds that that M: Into<N> for Recipient<M> for Actor<Message = N>. Fixes #11 (in its narrow sense) without creating multiple channels per actor and without boxing the messages transferred. Has been made possible by removal of the ability to retrieve back the failed-to-send message in one of the earlier commits. The whole trick is to box crossbeam `Sender<M>` into `Arc<dyn SenderTrait<M>>` in `Receiver`, and implementing `SenderTrait<M>` for crossbeam `Sender` and for boxed version of itself (!), second time with ability to convert `M`. All generic parameters in the echo example have disappeared without losing any flexibility, yay! Speaking strict semver, this is an API-breaking change, but e.g. the media_pipeline compiles and works unchanged. v2: make the change smaller and simpler by not messing up with `GenericReceiver`. Enables nicer API and keeping `SenderTrait` private, at the small expense of one extra boxing when creating `Addr`, and one extra pointer indirection when calling send() family of functions. All that thanks to @skywhale's clever question.
I.e before this change, Input, Damper actors assumed they are fed into Mixer, but Delay assumed it is fed into something that expects naked `Chunk`. This made no sense, and made actors dependent on how they are wired in the system, which is an antipattern. This is an artificial change that demonstrates problems outlined in #11 (comment)
...within bounds that that M: Into<N> for Recipient<M> for Actor<Message = N>. Fixes #11 (in its narrow sense) without creating multiple channels per actor and without boxing the messages transferred. Has been made possible by removal of the ability to retrieve back the failed-to-send message in one of the earlier commits. The whole trick is to box crossbeam `Sender<M>` into `Arc<dyn SenderTrait<M>>` in `Receiver`, and implementing `SenderTrait<M>` for crossbeam `Sender` and for boxed version of itself (!), second time with ability to convert `M`. All generic parameters in the echo example have disappeared without losing any flexibility, yay! Speaking strict semver, this is an API-breaking change, but e.g. the media_pipeline compiles and works unchanged. v2: make the change smaller and simpler by not messing up with `GenericReceiver`. Enables nicer API and keeping `SenderTrait` private, at the small expense of one extra boxing when creating `Addr`, and one extra pointer indirection when calling send() family of functions. All that thanks to @skywhale's clever question.
I.e before this change, Input, Damper actors assumed they are fed into Mixer, but Delay assumed it is fed into something that expects naked `Chunk`. This made no sense, and made actors dependent on how they are wired in the system, which is an antipattern. This is an artificial change that demonstrates problems outlined in #11 (comment)
...within bounds that that M: Into<N> for Recipient<M> for Actor<Message = N>. Fixes #11 (in its narrow sense) without creating multiple channels per actor and without boxing the messages transferred. Has been made possible by removal of the ability to retrieve back the failed-to-send message in one of the earlier commits. The whole trick is to box crossbeam `Sender<M>` into `Arc<dyn SenderTrait<M>>` in `Receiver`, and implementing `SenderTrait<M>` for crossbeam `Sender` and for boxed version of itself (!), second time with ability to convert `M`. All generic parameters in the echo example have disappeared without losing any flexibility, yay! Speaking strict semver, this is an API-breaking change, but e.g. the media_pipeline compiles and works unchanged. v2: make the change smaller and simpler by not messing up with `GenericReceiver`. Enables nicer API and keeping `SenderTrait` private, at the small expense of one extra boxing when creating `Addr`, and one extra pointer indirection when calling send() family of functions. All that thanks to @skywhale's clever question.
The original actor system has an API such that an actor accepts a single message type, causing us to often use enums to allow different types of messages to be sent a la:
Actix and other systems use something like:
Would be good to work out the pros/cons.
The text was updated successfully, but these errors were encountered: