You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
While functional and generally applicable, our current actor model is mostly designed for actors that are:
Meant to live forever
Always able to receive and process inbound messages
Use a channel as their primary inbound stream
As evidenced by the Gathering actor, this is not always the case. As such, we should look into expanding the design space for actors.
Design Considerations:
I believe there are two primary design considerations to consider: "lifetime" and "role".
Here, "lifetime" is not a Rust lifetime but rather if the actor might ever terminate. An actor with a finite lifetime is one that we expect to terminate at some point. Conservely, an actor with an infinite lifetime should never terminate, save for an expected panic. We want to distinguish between these two because that would help improve the ergonomics and stability of all actors. When sending a message to an infinite actor, we want to unwrap the Result that sending a message produces since an error means that the infinite actor is dead, so we ought to "bubble up" that error (in the same way that you bubble up errors from poisoned mutexes).
The idea of an actor's "role" is a bit more complicated, but it can be roughly modeled by the Stream and Sink traits. All actors that we've worked with up until now do some combination of consuming, responding to, and forwarding messages. Such actors act like sinks. The GatheringHall, for example, does all of these. However, we have not seen an actor that only/primarily emits messages. These actors would act like streams.
The motivating example for a stream-like actor is WebSockets. The management of WS connections (in both the client and backend) is a bit messy. This is due to a lack of separation of concerns. For example, a Gathering is responsible for:
Processing inbound sync requests
Forwarding successful syncs
Managing the sync and forwarding message chains for all clients
Managing persistence of the tournament
Additionally, it also needs to "know" about changes to a client's session and when to ignore a WS message because of a closed connection or invalid session. A better separation of concerns can be achieved by having an actor manage the WS connection. However, the client for this actor would transmit the messages originating from the actor, rather than sending a message to the actor. The key difference between these actor types has far more to do with the infrastructure around the actors than the actors themselves. After all, all actors share the same baseline functionality of processing messages from some inbound stream.
Possible Solutions:
To me, the most promising solutions for these two characteristics are very different.
To mark a trait as (in)finite, we can use an associate constant/type on the actor trait. This would allow us to distinguish between them in a mutually exclusive manner. This is important for knowing when to unwrap send errors in the client and when to allow the scheduler to offer the terminate method.
To mark a trait as a sink, stream, or both, we can use extension traits, something like StreamActor and SinkActor. These traits would help the builder determine what kinds of clients and methods. They would also help govern the behavior of the Schedule as needed.
Challenges:
The largest issue we have to contend with is the clients. Because we want to ascribe semantic meaning to the lifetime of an actor and whether or not the actor is a sink and/or stream, the client needs to be aware of this as well. This means that we might need up to 6 different client types. This will require some forethought to avoid code duplication and too much cognitive load.
The text was updated successfully, but these errors were encountered:
Thanks for the detailed description of the problems. Here are my initial thoughts and questions
Calling the persistence of the actors lifetime might get confusing in the long run. Should we consider an alternative? I propose transient and permanent actors and permanence of actors.
For the API, could we allow the users to define what kind of an actor they want while building the actor (as a method on ActorBuilder)? The following seems like a good, intuitive syntax that might reduce cognitive load.
ActorBuilder::new().transient().launch();ActorBuilder::new().persistent().launch();// or we could get rid of `new` constructor in these casesActorBuilder::persistent().launch();ActorBuilder::transient().launch();ActorBuilder::new().launch();// defaults to `persistent`ActorBuilder::new().sink().launch();ActorBuilder::new().stream().launch();// as before, briefer constructorsActorBuilder::stream().launch();ActorBuilder::sink().launch();// we could combine:ActorBuilder::transient().sink().launch();
I haven't heard of extension traits before. I will check into them.
I like the phrasing of "transient" and "permanent" actors a lot. That's a great idea.
As for API, the actor state is the part that "knows" if the state will ever terminate or not. This will allow us to restrict the scheduler accordingly. But more importantly, you should not be able to create a client that thinks it's talking to a permanent actor when it's talking to a transient actor (or, somewhat less importantly, visa versa). This is certainly possible. My larger concern is code duplication between different client types.
As for extension traits, see the Itertools trait in the itertools crate or the FutureEtx, StreamEtx, and SinkExt traits in the futures crate.
Unmet Need:
While functional and generally applicable, our current actor model is mostly designed for actors that are:
As evidenced by the
Gathering
actor, this is not always the case. As such, we should look into expanding the design space for actors.Design Considerations:
I believe there are two primary design considerations to consider: "lifetime" and "role".
Here, "lifetime" is not a Rust lifetime but rather if the actor might ever terminate. An actor with a finite lifetime is one that we expect to terminate at some point. Conservely, an actor with an infinite lifetime should never terminate, save for an expected panic. We want to distinguish between these two because that would help improve the ergonomics and stability of all actors. When sending a message to an infinite actor, we want to unwrap the
Result
that sending a message produces since an error means that the infinite actor is dead, so we ought to "bubble up" that error (in the same way that you bubble up errors from poisoned mutexes).The idea of an actor's "role" is a bit more complicated, but it can be roughly modeled by the
Stream
andSink
traits. All actors that we've worked with up until now do some combination of consuming, responding to, and forwarding messages. Such actors act like sinks. TheGatheringHall
, for example, does all of these. However, we have not seen an actor that only/primarily emits messages. These actors would act like streams.The motivating example for a stream-like actor is WebSockets. The management of WS connections (in both the client and backend) is a bit messy. This is due to a lack of separation of concerns. For example, a
Gathering
is responsible for:Additionally, it also needs to "know" about changes to a client's session and when to ignore a WS message because of a closed connection or invalid session. A better separation of concerns can be achieved by having an actor manage the WS connection. However, the client for this actor would transmit the messages originating from the actor, rather than sending a message to the actor. The key difference between these actor types has far more to do with the infrastructure around the actors than the actors themselves. After all, all actors share the same baseline functionality of processing messages from some inbound stream.
Possible Solutions:
To me, the most promising solutions for these two characteristics are very different.
To mark a trait as (in)finite, we can use an associate constant/type on the actor trait. This would allow us to distinguish between them in a mutually exclusive manner. This is important for knowing when to unwrap send errors in the client and when to allow the scheduler to offer the
terminate
method.To mark a trait as a sink, stream, or both, we can use extension traits, something like
StreamActor
andSinkActor
. These traits would help the builder determine what kinds of clients and methods. They would also help govern the behavior of the Schedule as needed.Challenges:
The largest issue we have to contend with is the clients. Because we want to ascribe semantic meaning to the lifetime of an actor and whether or not the actor is a sink and/or stream, the client needs to be aware of this as well. This means that we might need up to 6 different client types. This will require some forethought to avoid code duplication and too much cognitive load.
The text was updated successfully, but these errors were encountered: