Package net.sodacan.core.message
A message flows from a source Actor to a destination Actor. If an actor never receives a message, it will never be realized. The receipt of a message is the only way that an Actor will perform any action.
Overview
A message usually originates in an Actor and is consumed by another Actor. In general, a message will, by its type, convey some Action expected by the receiving Actor. However, messages can also represent more complex flows and information exchange.
Sodacan Messages can be used for two different designs: point to point and business flows. Either or both can be used as desired. The point to point approach simply means each interaction between two Actors is represented by a specific message type. For example, in an IOT system, a Lamp Actor could receive one of two messages: TurnLightsOn and TurnLightsOff. On the other hand, a "flow" design might route a single message from Actor to Actor building a payload that is ready for some behavior by the last Actor in the flow. The bulk of the rest of this specification covers complex flows while the point-to-point design is simply a degenerate case of more complex flows.
Messages are generally composed of the following:
- A subclass of Message. While a message can just be a message, it is often useful to use subclasses of Message to convey some specific meaning. At a minimum, a subclass can describe the structure of a message if not the meaning.
- MessageId serves two purposes. It provides a unique identifier for the message. This Id is particularly during complex flows where it acts as a correlationId.
- Routing is where the sequence of actions is maintained as a message makes its way from Actor to Actor.
- Payload is a collection of zero or more records. The payload is predefined in the message but its contents can change as the message makes its way along a flow.
The following goes into more detail about Messages.
MessageId
The MessageId component of a message uniquely identifies the Message. If a single message flows to several different Actors, it retains its MessageId. This ID is particularly in complex message flows such as a fan-out, fan-in situation. The Id allows the various copies of the message to find each other when it's time to merge them back together.
MessageIds are created by a factory method in the Sodacan configuration. The default factory is:
Config config = new DefaultConfig.Builder() .messageId((c) -> new DefaultMessageId(c) ...
The nature of Sodacan depends on a fast and unique message id. To accomplish this, the Id has two components: a UTC timestamp and a random number. The timestame does suggest that messages can be sorted, however, Sodacan does not do so in normal message processing.
Routing
Routing in a message can be very simple or it can be very complex. To begin with, imagine the routing in a message as being a stack of locations. If a single ActorId is pushed onto this stack, then the message will make its way to that Actor and that will end it's journey.
Message message = new SomeMessage().push(someActorId);
Let's say that a message should also report back to the sending Actor. In this case, the ActorId of the sender is pushed onto the stack followed by the target ActorId. When the message arrives at the target, the top of stack is popped and the "Stage.next()" function will route the same message back to the sender.
Message message = new SomeMessage().push(this.getActorId()).push(someActorId);
Of course the return trip could include some added data added to the message, if desired.
In a slightly more complex scenario, let's say the original target only partially answered the request message. Another ActorId could be pushed onto the stack (without the originator knowing) so that that other Actor could complete the request, and finally, the message makes its way back to the originating actorId at the bottom of the stack. Of course, this stack-behaviour can be more complex as well.
Message message = new SomeMessage().push(this.getActorId()).push(someActorId); ... // In SomeActor... Stage processMessage(Message message) { ... message.push(myHelperActor); stage.add(message);
Now, consider a subscription service provided by some Actor. That actor will want to send an Update containing some updated data to every subscribing Actor. The subscription request is straightforward and not described here. But when it comes time to report the updated data to interested Actors, we would prefer that a message be sent to all of those Actors in parallel. We do this easy enough by using a broadcast function.
Message message = new SomeMessage().broadcast(listOfActorIds); ...
The broadcast function directs a separate copy of the message to each Actor in the list. A broadcast usually doesn't have a reply-to associated with it. The stack is just one-level deep, but it might be very "wide".
Now, using the same approach, we can build a solicitation. We just push a bottom-of-the-stack reply-to ActorId which means each of the broadcast recipients will, at the conclusion of processing the message, forward the message (back) to the originator or to a different actor. This approach is a way implement a simple solicitation flow.
Message message = new SomeMessage().push(anotherActorId).broadcast(listOfActorIds); ...
Route Types - Verb
As described earlier, the subclass of a message can convey the purpose of the message. However, we prefer to use the message type as a way to convey message structure. Instead, we use subclasses of Route to convey purpose or action to the target Actor.
Complex routing
Sodacan can also provide routing that includes fan-out and fan-in services. This type of routing describes a graph of routes which can be dynamically enhanced as needed. This may seem similar to a relational database query involving one or more joins. In fact. the problem can be described in the same way as a database query but is implemented differently. Sodacan also allows a decomposition of complex data manipulation functions unlike a relational query.
We will start by describing a fan-in operation. The point-to-point, request-response, and fan-out (broadcast) operations have already been described above. But fan-in involves a special use of Actors. This is necessary because Actors can receive messages but Messages cannot. So, we create an actor to perform the fan-in work. Imagine a web page that displays a list of patients seen by a physical therapist. On this list we need the patient's name and phone number. For the moment, assume that the ActorId of the therapist is supplied in the REST request from the browser.
One approach would be to maintain the list of patient's names and phone numbers in the therapist's Actor. This would result in a very fast query (the whole list is ready to go in a single Actor). And we could use a subscription to keep the details on our list of patients up to date. Instead, we may prefer to only keep the ActorIds of the therapist's patients. So, in this case, we don't subscribe to updates of patient info but rather start the query at the therapist Actor, send messages to each of the patients to get their name and phone number, and finally collect the results into the message which is returned in the response to the browser.
The HTTP request should have no say in how the list is constructed. This task is performed by the therapist Actor. But the HTTP request does know what it wants for a response to the browser. So, it creates a mostly empty patient-list message and sends it to the therapist asking for the list of patients to be filled in. When complete, a single message should be returned as the response to the browser. At this point we know that the last route in the message will be the HTTP request. And we know that the first route will be to the therapist. What happens in between these two steps is a fan-out fan-in operation instigated by the therapist Actor.
Upon receipt of the message, the route to the therapist has been popped off the route stack leaving just the final reply-to. Two things happen at this point: A special actor is created to collect and assemble the results from each patient actor and to send the message to each of the patient Actors. Each patient supplies their name and phone number in the message they receive and then put the message back into the stage where it is then routed to the special collector Actor where it is added to the rest of the results. When the special actor has received all of the patient data, it can be forwarded to the next and final route; The HTTP request.
Payload
*During message delivery, the payload is not considered. The header of a message can vary in structure but the critical element is the current target (destination) ActorId. ActorId contains the ActorGroup number. This number is important in determining how the message will be delivered.
Each Actor has an inbound message queue, sometimes called a mailbox. Sodacan tries to minimize the number of steps, especially intermediate queues between the source and destination Actors.
While an Actor is processing a message, outgoing message are accumulated in a *stage*. Once the Actor "commits", the messages in the stage can be sent. As much work as possible is done in the Actor's thread.
If the destination of a message is within the same ActorGroup, the message can be delivered directly to the destination Actor's input queue.
Message delivery between ActorGroups is more formal; Even if the two ActorGroups are on the same Host. This is necessary because ActorGroups can move from one Host to another.
While an Actor *could* send messages directly to other ActorGroups, even on other Hosts, this has the potential of affecting the liveliness of the system. So, instead, when an Actor needs to send a message to another ActorGroup, it just directs the message to a special Actor, ActorGroupSender, that operates within the same ActorGroup, where it is sent to its destination. The ActorGroup has an ActorGroupSender for each destination ActorGroup. This approach allows the source Actor to continue processing more messages. It hands over message delivery to another Actor that specializes in message delivery. It also allows the ActorGroupSender Actor to consolidate messages from source Actors into larger message containers for more efficient delivery.
The ActorGroupSender Actors are sent periodic messages from their Host telling the senders which Host to send a message to for that particular target ActorGroup.
The rest of the message flow exploits the fact that Sodacan Hosts support an HTTP server. This server can handle messages from outside and inside the Sodacan environment. So, when an ActorGroupSender is ready to send a message, it uses an HTTP client to send the message to the correct Host. From there the message is directly routed through to the destination Actor's message queue. (The message is briefly handled by the ActorGroup, but is not queued by the ActorGroup.)
The whole message path between ActorGroups is dependent on the coordinator Host/ActorGroup configuration. The ActorGroupSenders receive updates to this configuration when it changes.
StateMessages follow a different path: Among the outgoing messages from an Actor is one or more StateMessages for Actors that have a state and that state changed during processing. This type of message is sent to a special Actor called the StateRouter. The StateRouter determines destinations for a StateMessage. First, it send the state message to the JournalWriter actor for the ActorGroup of the Actor on the local Host and also copies it to any other replica ActorGroup JournalWriters. This flow is described in more detail in the journal package. Non-state messages can also be routed to the JournalWriter when appropriate.
- See Also:
-
ClassesClassDescriptionAll Messages have a messageId and a routing stack.MessageId is normally a Java Instant plus a random integer.