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
}