-
Notifications
You must be signed in to change notification settings - Fork 91
Actors
A new actor can be developed easily by inheriting from the Actor
class, which holds
similarity to CAL actors, if one is familiar with them.
The following is an example of an actor. In this case, the only function is to pass any token recived on the inport to the outport, optionally printing it to standard out.
from calvin.actor.actor import Actor, ActionResult, manage, condition
class Identity(Actor):
"""
forward a token unchanged
Inputs:
token : a token
Outputs:
token : the same token
"""
@manage(['dump'])
def init(self, dump = False):
self.dump = dump
def log(self, data) :
print "%s<%s>: %s" % (self.__class__.__name__, self.id, data)
@condition(['token'], ['token'])
def donothing(self, input):
if self.dump : self.log(input)
return ActionResult(tokens_consumed=1, tokens_produced=1, production=( input, ))
action_priority = ( donothing, )
The first lines import the necessary parts of the Actor module.
from calvin.actor.actor import Actor, ActionResult, manage, condition
Here, Actor
is the base class of all actors and should always be included.
ActionResult
is how an action reports its result to the runtime, including
how many tokens where consumed, many where produced, and of course what those
produced were.
To simplify development of actors, there are three decorators that can be imported from the calvin.actor.actor
module: @manage
, @condition
, and @guard
. Use of @manage
and @condition
is in fact mandatory, while use of @guard
is optional.
The @manage
decorator is used to inform the runtime which attributes of the
actor should be automatically managed when migrating, and the @condition
decorator specifies how many tokens are needed on in-ports and how much space
is required on out-ports before an action can fire. The condition
decorator also
serves the dual purpose of specifying that the decorated function actually is an action.
Finally, the optional @guard
decorator, which may only appear after the @condition
decorator,
can inspect the data available for the action and decide wether or not to allow the action to run.
@manage
This decorator should be used with the init
method of the actor to inform the system of which actor attributes (i.e. actor state) should be serialized on migration.
Usage:
@manage() # Manage every instance variable known upon completion of init
@manage(include = []) # Manage nothing
@manage(include = [foo, bar]) # Manage self.foo and self.bar only. Equivalent to @manage([foo, bar])
@manage(exclude = [foo, bar]) # Manage everything except self.foo and self.bar
@manage(exclude = []) # Same as @manage()
@manage(<list>) # Same as @manage(include = <list>)
N.B. If include and exclude are both present, exclude will be disregarded.
@condition
The @condition
decorator specifies the required input data and necessary output space. Unless both input and output conditions are fulfilled the action cannot be fired.
Usage:
@condition(action_input=[], action_output=[])
Both action_input
and action_output
parameters are lists of tuples: ("portname", #tokens)
Optionally, the port specification can be a "portname" only instead of a tuple, meaning #tokens is 1.
@guard
The decorator guard refines the criteria for picking an action to run by stating a function with the same signature as the guarded action, returning a boolean (True if action allowed). If the specified function is unbound or is a lambda expression, you must account for 'self', e.g.
@condition(['select', 'false'], ['data'])
@guard(lambda self, select, data : select == 0)
def false_action(self, select, data):
...
An actor should always inherit from the Actor
base class.
class Identity(Actor):
The docstring of the actor defines the ports and their names. Note: This is
required as it is the only way of defining ports. In this example there is
one in-port, named token
, and one out-port, also named token
. The Inputs:
and Outputs:
headings are optional if no ports listed under heading. For
aesthetical reasons the plural 's' in the headings is optional, as is the
capitalization of the words. Also optionally document the port after the port
string. At least a whitespace is required after the portname, but the
convention is to separate port name and port documentation by " : ", i.e.
space colon space
. It is a convention to use lowercase letters for ports,
unless there is good reason not to, e.g. a port taking a URL as input.
"""
forward a token unchanged
Inputs:
token : a token
Outputs:
token : the same token
"""
The manage
decorator, here used in its simplest form - giving a list of
attributes to manage. Optionally, it could be written @manage( include = ['dump'] )
. If there are a multitude of attributes, all of which should be
included, an empty argument list, i.e. @manage()
will ensure all of them are
included. It is also possible to only specify which attributes should be
excluded. See the documentation for Actor.actor.manage
for further details.
@manage(['dump'])
The init
method serves the same purpose for actors as __init__
does for
python classes, to initialize attributes. This is the first method called
after an actor is created and has access to all actor specific methods.
Any attribute which exists after init
returns can be managed (see above)
def init(self, dump = False):
self.dump = dump
It is of course possible to define and call any number of methods in an actor.
def log(self, data) :
print "%s<%s>: %s" % (self.__class__.__name__, self.id, data)
An action is defined by the @condition
decorator. In this case, it expects
one token on the in-port token
, and one available slot on the out-port,
also named token
. The argument list here is actually a shortened version
of @condition( action_input = [ ('token', 1) ], action_output = [ ('token', 1) ]
meaning the action has as input one token from in-port 'token' and will produce one
token on out-port 'token'. See documentation for Actor.actor.condition
.
@condition(['token'], ['token'])
Omitted in this action is the guard
decorator. Whereas condition
only does a superficial
examination of the ports (number of tokens available on in-ports and space left on out-ports, mainly)
the guard
has full access to the tokens and can examine them more thoroughly. For example, it would
be possible to handle input differently based on type by including a guard
@guard(lambda self, a : isinstance(a, str))
on an action, and it would only be applied when the token on the in-port is a string. The guard will always have the same signature as the action.
def donothing(self, input):
if self.dump : self.log(input)
The ActionResult
returns the result of the action to the runtime. Here, one token was consumed,
one was produced, and the tokens produced is given as a sequence.
return ActionResult(tokens_consumed=1, tokens_produced=1, production=( input, ))
Finally, action_priority
determines the priority of the actions. This is the
order in which conditions
(and guards
) will be evaluated to see which
actions fire. Whenever an action fires, this can cause actions of higher
priority to be ready to fire, and thus the sequence will be iterated,
restarting whenever an action fires, until no action has fired for a full
iteration.
action_priority = ( donothing, )