Implementing a distributed algorithm
- In order to implement a distributed algorithm, the class must fulfill these requirements:
It must be a subclass of
NodeAlgorithmclass.It should have a class attribute
NodeAlgorithm.Statuswhich is an enumeration of the possible states of the node. This enumeration must subclassStatusValues.Action implementations must be methods of the class, they must be called as the action itself and must be decorated with a member of the Status enumeration. One such implementation should look like this:
@Status.IDLE
def receive(self, node: NodeAccess, message: Message):
if is_my_favorite_neighbor(message.source):
# Send message
self.send(
node,
data='Hi! Wanna play?',
destination=message.source,
header="PLAY INVITATION",
)
# Remind myself to send the message again in 10 simulation seconds
self.set_alarm(
node,
time=10,
)
# Change my status to WAITING
self.status = self.Status.WAITING
else:
# Ignore the message, I don't like this guy
node.memory['SPAM_COUNT'] += 1
Here, self is the instance of the algorithm class, node is the node that is executing the action and message is the
message that triggered the action. You would use the node to access the node’s status and memory, the
message to access the message’s data, header and source; and self to access the algorithm’s interfaces for
sending messages and setting alarms.
Note
To help the programmer, there is a special action Actions.default which will be called if the action was
not implemented for a given status.
Warning
Actions without an implementation will only log a warning message, so for no-ops, it is not necessary to implement anything.
Node’s own id and neighbor labels
The framework is designed to prevent the programmer from accessing information that should not be available to the nodes. For this reason, we provide two classes that act as proxies for the node and its neighbors, respectively.
NodeAccess class
A NodeAccess instance represents a node’s own view, encapsulating the knowledge gathered so far (inside the
memory attribute) and inherent of the node (such as who are it’s neighbors).
By default, this class allows read-only access to the node’s clock attribute and read-and-write access to the
memory and status attributes.
In addition to this attributes, each NodeAccess instance has an integer id attribute, by default it’s random and
won’t be unique in the network.
These are the two main ways a programmer comes across a NodeAccess instance:
@Status.IDLE
def receiving(self, node: NodeAccess, message: Message):
↑
def initializer(self):
for node in self.nwm.nodes():
↑
node.memory['SPAM_COUNT'] = 0
for node in self.network.nodes():
↑ NOT A NodeAccess INSTANCE
NeighborLabel class
A NeighborLabel instance represents the knowledge of the neighbor respect to current processing node.
Similar to the NodeAccess class, the NeighborLabel instances have an integer id attribute, by default it’s random
and won’t match any other id in the network. It’s only used to identify that neighbor among the other neighbors.
These are the two main ways a programmer comes across a NeighborLabel instance:
@Status.IDLE
def receiving(self, node: NodeAccess, message: Message):
source = message.source # The node that sent the message
↑
for neighbor in node.neighbors():
↑
“The Santoro’s interface”
Send a message
The NodeAlgorithm.send_msg() method receives a source node and a message. The message will be sent to the destinations
specified in the message itself. The destinations must be a list of nodes or a single node. The message will be sent to
all the destinations.
The NodeAlgorithm.send() method receives a source node, a message content a list of destinations and, optionally,
a header. Is equivalent to the NodeAlgorithm.send_msg() method, but the message is created automatically.
Set an alarm
The NodeAlgorithm.set_alarm() method receives a node and a time. An empty message will be sent to the node after
the time has passed. The time is in algorithm steps, which behavior varies according to the algorithm and the simulationparameters.
Check the extended interface methods for advanced use of the alarm mechanism.
Open and close edges/links
The NodeAlgorithm.close() and NodeAlgorithm.open() methods implement the closing and opening of edges,
respectively. The arguments are the blocking node and the blocked node. From the call of NodeAlgorithm.close()
until the call of NodeAlgorithm.open(), the blocking node will not be able to receive any messages from the
blocked node. The blocked node will not be notified of the blocking. The blocked node can still send
messages to the blocking node, and once the edge is opened, the blocking node will receive all the messages sent during
the blocking period. The blocking node can still send messages to the blocked node.
Helper functions and the extended interface
Block and unblock messages - Keep in queue and receive later
With the help of some functional programming, the NodeAlgorithm.block_inbox() and
NodeAlgorithm.unblock_inbox() methods allow a node to block the reception of messages for any given condition.
The blocked messages are kept in a queue and will be delivered to the node when the blockade is lifted. The condition
is a function that receives a message and returns a boolean. If the function returns False, the message will be
blocked.
- The usage is this simple:
Call
NodeAlgorithm.block_inbox()with the condition function and store the returned object in the node’s memory.Call
NodeAlgorithm.unblock_inbox()with the object returned by theNodeAlgorithm.block_inbox()method.
Tip
The condition function can be a lambda function, a function defined in the algorithm class or a function defined outside the algorithm class. One such function could look like this:
# Normal function
def ignore_message(message: Message):
return message.header != 'IGNORE_ME'
# Lambda function
ignore_message = lambda message: message.header != 'IGNORE_ME'
Alarm management
The NodeAlgorithm.set_alarm() method receives an extra, optional, parameter, the message. This message will be
sent to the node when the alarm goes off. The message must be an instance of Message. This, however, is not
meant to be used to send messages to other nodes, but to the node itself.
Note
To schedule a message to be sent to another node, we recommend using the NodeAlgorithm.send() method inside
the callback of the alarm.
The NodeAlgorithm.set_alarm() method returns an object that represents the alarm. This object can be used to
disable the alarm, update the time, check the time left for the alarm to go off and check if the alarm is active.
To disable alarms, the following methods are available:
NodeAlgorithm.disable_all_node_alarms()which disables all alarms of the node.
NodeAlgorithm.disable_alarm()which disables a specific alarm. The argument is the alarm object itself, which is returned by theNodeAlgorithm.set_alarm()method.
And finally, to add or subtract time from a pending alarm, NodeAlgorithm.update_alarm_time() can be used. The
arguments are the alarm object and the time to be added or subtracted. If the time subtracted is greater than the time
left for the alarm to go off, the alarm will be triggered the next “alarm triggering event” (usually the next step of
the algorithm).