Every node has a daemon running with a series of agents on it. These agents are each executed in their own thread or process. They are provided with a defined interface with which to send and receive messages to other object in the experiment. The Magi daemon will route messages to the agent based on the routing information in the message. The daemon supports group-based, name-based, and “dock”-based routing. (A dock is like a port for a traditional daemon; an agent listens on a dock.) Once a message is delivered to an agent, the format of the message data is then up to the agent itself.
Most agents will not need to parse messages directly however as the Magi Agent Library supports a number of useful abstractions implemented in base classes from which Agent authors can derive. This are described in detail below.
There are two execution models supported by the daemon for Agents: thread-based and process-based. A thread-based agent is loaded and runs in the process space of the daemon. The daemon communicates with a thread-based agent directly. A process-based Agent is started as a separate process. The daemon communicates with it via standard interprocess communication techniques: a pipe or a socket.
Here is a list outlining the differences between the execution models.
Process (Pipe or Socket)
Threads
Agent authors need to write an IDL that matches the interface exported by thier agent. This IDL is used by Magi (and Montage) to validate the interface of the agent. (And in the future to generate GUIs for agent execution and configuration.) The IDL should specify: agent execution model (thread or process), any public agent variables and thier types, ranges, or enumerated values; any public methods and the method arguments and thier types; “help” strings for each method and agent varible which explain their purpose; and finally any Agent library from which they derive.
This may seem like a lot to specify, but the Agent Library supplies IDL for base Agents so in practice much of the IDL specification will be supplied to the Agent author.
The IDL format and keywords are given in a table below. GTL INSERT CROSS REFERENCE HERE
In this section we describe the Agent Library and give brief examples for usage. Classes are organized from the bottom up, that is, starting with the class from which the others derive.
Attention
The Agent Library is only currently supported in thread-based Agents.
Note
When using the Orchestrator to run your experiment, the Orchestrator will, by default, handle a return value of False from an agent method as a reason to unload all agents, break down communication groups and exit. Thus your agent can stop an experiment by returning False.
This class implements a setConfiguration method. If derived from the user can call setConfiguration to set any self variables in your class. AgentVariables also implements an empty confirmConfiguration method that is called once the self variables are set. You can implement your own confirmConfiguration if you make sure the user has set your internal variables to match any constraints you may want to impose. Returning False from this method will signal to the orchestrator that something is amiss and the orchestrator should handle this as an error. The default implementation of confirmConfiguration simply returns True. The method signatures for confirmConfiguration() is
def confirmConfiguration(self):
It takes no arguments. In your confirmConfiguration method, you should confirm that your agent internal variables are the correct type an in the expected range.
In this example imagine an Agent has a variable that is an integer and the range of the value must be between 1 and 10. An Agent can use the AgentVariables class to implement this as so:
from magi.util.agent import AgentVariables
class myAgent(AgentVariables):
def __init__(self):
self.value = None
def confirmConfiguration():
if not isinstance(self.value, int):
return False
if not 1 <= self.value <= 10:
return False
return True
If the variable self.value is not an integer or is not between 1 and 10, confirmConfiguration returns False. If running this agent with the Orchestrator, the False value will get returned to the Orchestrator which will unload all agents, destroy all group communications, then exit. Thus your Agent can cause the experiment to stop and be reset when it is not given the correct inputs.
(Note: in the future, this functionality of enforcing correct input will be handled outside of the Agent code. The IDL associated with the agent already specifies correct input and the Orchestrator (or other Montage/Magi front end tool) will enforce proper input.)
All classes in the AgentLibrary inherit from AgentVariables.
The AgentVariables documentation can seen here.
The DispatchAgent implements the simple remote procedure call (RPC) mechanism used by the orchestrator (and available through the Magi python API). This allows any method in a class derived from DispatchAgent to be invoked in an AAL (or by a MagiMessage if using the Magi python interface directly). You almost always want to derive your agent from DispatchAgent. The DispatchAgent code simply loops, parsing incoming messages, looking for an event message. When it finds one, it attempts to call the method specified in the message with the arguments given in the message. Thus implementing a basic RPC functionality in your Agent.
The argument to your RPC-enabled method is the message sent. The Agent Library exports a function decorator for DispatchAgent callable methods named agentmethod. It is not currently used for anything, but it is suggested that Agent developers use it anyway.
Here is a simple example:
from magi.util.agent import DispatchAgent, agentmethod
def myAgent(DispatchAgent):
def __init__(self):
DispatchAgent.__init__(self)
@agentmethod()
def doAction(self, msg):
pass
Given the Agent myAgent above and the AAL fragment below, the method doAction will be called on all test nodes associated with myAgentGroup.
eventstreams:
myStream:
- type: event
agent: myAgentGroup
method: doAction
args: { }
The DispatchAgent documentation can seen here.
You will note that the DispatchAgent only allows an outside source to send commands to the agent. There is no communication backwards. There is another base class that has a slightly different run loop. Rather than blocking foreever on incoming messages it will also call its own method, periodic, to allow other operations to occur.
The call to periodic will return the amount of time in seconds (as a float) that it will wait until calling periodic again. The periodic function therefore controls how often it is called. The first call will happen as soon as run is called.
The method signature of the periodic method is:
def periodic(self, now):
If periodic is not derived in the base class, an Exception is raised.
This example code writes the current time to a file once a second. Note the explicit use of the AgentVariables class to set the file name.
import os.path
from magi.util.agent import ReportingDispatchAgent, agentmethod
class myTimeTracker(ReportingDispatchAgent):
def __init__(self):
ReportingDispatchAgent.__init__(self)
self.filename = None
def confirmConfiguration(self):
if not os.path.exists(self.filename):
return False
def periodic(self, now):
with open(self.filename, 'a') as fd:
fd.write('%f\n' % now)
# call again one second from now
return 1.0
The ReportingDispatchAgent documentation can seen here.
TrafficClientAgent models an Agent that periodically generates traffic. It must implement the getCmd method, returning a string to execute on the command line to generate traffic. For example, the getCmd could return a curl or wget command to generate client-side HTML traffic. The signature of getCmd is:
def getCmd(self, destination)
Where destination is a server host name from which the Agent should request traffic. The base class contains a number of variables which control how often getCmd is called and which servers should be contacted: servers is a list of server hostnames and interval is a distribution variable.
The TrafficClientAgent class implementes the following event-callable methods: startClient() and stopClient(). Neither method takes any arguments. These methods may be invoked from an AAL and start and stop the client respectively.
Note
A distribution variable is any valid python expression that returns a float. It can be as simple as an integer, “1” or an actual distribution function. The Agent Library provides minmax, gamma, pareto, and expo in the distributions module. Thus a valid value for the TrafficClientAgent interval value could be “minmax(1,10)”, which returns a value between 1 and 10 inclusive. The signatures of these distributions are:
Below is a sample TrafficClientAgent which implements a simple HTTP client-side traffic agent. It assumes the destinations have been set correctly (via the AgentVaraibles setConfiguration method) and there are web servers already running there.
from magi.util.agent import TrafficClientAgent
class mySimpleHTTPClient(TrafficClientAgent):
def __init__(self):
TrafficClientAgent.__init__(self)
def getCmd(self, destination):
cmd = 'curl -s -o /dev/null http://%s/index.html' % destination
return cmd
When this agent is used with the following AAL clauses, the servers server_1 and server_2 are used as HTTP traffic generation servers and traffic is generated once an interval where the interval ranges randomly between 5 and 10 seconds, inclusive. The first event sets the Agent’s internal configuration. The second event starts the traffic generation.
eventstreams:
myStream:
- type: event
agent: myHTTPClients
method: setConfiguration
args:
interval: 'minmax(5, 10)'
servers: ['server_1', 'server_2']
- type: event
agent: myHTTPClients
method: startClient
args: { }
The TrafficClientAgent documentation can seen here.
TDB
Explain the load agent chain: AAL –> load agent –> IDL parse –> getAgent() –> __init__()
Explain the agent RPC chain: AAL –> method event message –> daemon invocation of agent method