Target-Initiator Example
This example provides a request-response interaction. In this scenario, an Initiator reactor sends a request to a Target reactor by invoking one of its methods. The Target then processes the request and sends a response back to the Initiator. This pattern is often referred to as a Remote Procedure Call (RPC), and it is essential for building systems where components need to query each other for information or trigger actions.
The Target Data Model
The Target reactor's data model serves as its public API, defining the methods that other reactors can invoke. In this case, it exposes a single asynchronous method called answer, which accepts a string message as an argument and is expected to return a string.
name: "target"
description: "Target"
root:
!!FolderNode
name: "Methods"
children:
- !!AsyncMethodNode
name: "answer"
description: "Answer to the message"
parameters:
- !!StringVariableNode
name: "message"
description: "message to answer"
default_value: ""
returns:
- !!StringVariableNode
name: "return"
description: "the answer"
The Initiator Reactor
The Initiator reactor is the client in this interaction. Its role is to initiate the communication by sending a request to the Target. At startup, it builds a message to invoke the answer method on the Target, passing a question as the argument. After sending the request, it waits for a response. When the response arrives, it logs the answer provided by the Target.
reactor Initiator extends FrostBase {
input channel_in
output channel_out
reaction(startup) -> channel_out {=
// Send a message to invoke the "answer" method on the target
message = FrostMessage(
sender=self.name,
target="target",
// ... message header for method invocation ...
payload=MethodPayload(
node="Methods/answer",
args=["What is your name?"],
kwargs={},
),
)
self._set_channel_out_port(message, channel_out)
=}
reaction(channel_in) {=
// Receive the response from the target
message = channel_in.value[0]
if message.header.type == MsgType.RESPONSE and message.header.namespace == MsgNamespace.METHOD:
self.logger.info(f"{message.sender}: {message.payload.ret['response']}")
=}
}
The Target Reactor
The Target reactor is the server in this scenario. It is responsible for implementing the logic of the answer method. At startup, it retrieves the answer method node from its data model and assigns a callback function to it. When the Initiator (or any other reactor) invokes this method, the assigned callback is executed. In this simple example, the callback ignores the input message and returns a fixed string, "Bob".
reactor Target extends FrostMachine {
state method_node
state target_name = "Bob"
reaction(startup) {=
method_node = self.data_model.get_node("Methods/answer")
method_node.callback = self.answer
=}
method answer(message) {=
// This method is called when the initiator invokes "Methods/answer"
return self.target_name
=}
}
The main Reactor
The main reactor is responsible for assembling the entire simulation. It instantiates the Initiator, Target, and a FrostBus. The FrostBus acts as a message broker, facilitating communication between the other reactors. The main reactor connects the Initiator and Target to the bus, which allows the Initiator's method invocation to be routed to the Target, and the Target's response to be sent back.
main reactor {
frost_bus = new FrostBus(name = "frost_bus", width = 2)
initiator = new Initiator(name = "initiator")
target = new Target(name = "target")
frost_bus.channel_out -> initiator.channel_in, target.channel_in after 10 msec
initiator.channel_out, target.channel_out -> frost_bus.channel_in after 10 msec
}