Considerations

Clients versus servers

When using Basic communication patterns we have a lot of flexibility:

  • We are allowed to connect multiple clients to a server.
  • The server can play any role (i.e.: does not need to be always REP, but can be REQ as well). Servers are only defined by the action of binding, not by the role they play in the communication pattern.

For example, if we bind using PUSH and we connect multiple clients to this server, then messages pushed will be distributed among the clients in a Round-robin fashion, which means the first message will be received by the first client, the second message will be received by the second client, and so on.

If we bind using PULL and we connect multiple clients to this server, then messages pushed will all be received by the single server, as expected.

For more information simply refer to the ØMQ guide.

Closing connections

For closing a specific connection from an agent we need to call the close() method, which takes the alias of the socket from the connection we want to close as a parameter.

agent.bind('PUB', alias='connection')
...
agent.close('connection')

There is also a close_all() method, which takes no parameters and will close all user-defined connections of the agent.

Remember that the linger value from the osBrain configuration will be used for the actual socket.close() calls in both methods. For more information, simply refer to the ØMQ guide.

Note

Closing a connection within an agent will have no effect on any possible agents at the other end of the connection. Remember to manually close them as well if the connection is not going to be reused.

Adding new methods

Note that proxies can not only be used to execute methods remotely in the agent, but they can also be used to add new methods or change already existing methods in the remote agent.

In the following example you can see how we can create a couple of functions that are then added to the remote agent as new methods.

In order to add new methods (or change current methods) we only need to call set_method() from the proxy.

from osbrain import run_agent
from osbrain import run_nameserver


def set_x(self, value):
    self.x = value


def set_y(self, value):
    self.y = value


def add_xy(self):
    return self.x + self.y


if __name__ == '__main__':

    # System deployment
    ns = run_nameserver()
    agent = run_agent('Example')

    # System configuration
    agent.set_method(set_x, set_y, add=add_xy)

    # Trying the new methods
    agent.set_x(1)
    agent.set_y(2)
    print(agent.add())

    ns.shutdown()

Note that set_method() accepts any number of parameters:

  • In case they are not named parameters, the function names will be used as the method names in the remote agent.
  • In case they are named parameters, then the method in the remote agent will be named after the parameter name.

Lambdas

osBrain uses cloudpickle when communicating with remote agents through a proxy. This means that almost anything can be serialized to an agent using a proxy.

In order to further simplify some tasks, lambda functions can be used to configure remote agents:

from osbrain import run_agent
from osbrain import run_nameserver

if __name__ == '__main__':

    ns = run_nameserver()
    alice = run_agent('Alice')
    bob = run_agent('Bob')

    addr = alice.bind('REP', handler=lambda agent, msg: 'Received ' + str(msg))
    bob.connect(addr, alias='main')

    for i in range(10):
        bob.send('main', i)
        reply = bob.recv('main')
        print(reply)

    ns.shutdown()

See the similarities between this example and the one showed in Request-Reply. In fact, the only difference is the binding from Alice, in which we are using a lambda function for the handler.

Shutting down

If we want to end the execution of a specific agent in our system, we can do it by calling the Agent.shutdown() method:

import time

from osbrain import run_agent
from osbrain import run_nameserver


def tick(agent):
    agent.log_info('tick')


if __name__ == '__main__':

    ns = run_nameserver()
    a0 = run_agent('Agent0')
    a1 = run_agent('Agent1')

    a0.each(1, tick)
    a1.each(1, tick)
    time.sleep(3)

    a0.shutdown()
    time.sleep(3)

    ns.shutdown()

Shutting down a name server will result in all agents registered in that name server being shut down as well. This allows us to easily shutdown groups of agents at the same time.

Note

We can establish connections between agents registered in different name servers.

OOP

Although the approach of using proxies for the whole configuration process is valid, sometimes the developer may prefer to use OOP to define the behavior of an agent.

This, of course, can be done with osBrain:

import time

from osbrain import Agent
from osbrain import run_agent
from osbrain import run_nameserver


class Greeter(Agent):
    def on_init(self):
        self.bind('PUSH', alias='main')

    def hello(self, name):
        self.send('main', 'Hello, %s!' % name)


class Bob(Agent):
    def custom_log(self, message):
        self.log_info('Received: %s' % message)


if __name__ == '__main__':

    # System deployment
    ns = run_nameserver()
    alice = run_agent('Alice', base=Greeter)
    bob = run_agent('Bob', base=Bob)

    # System configuration
    bob.connect(alice.addr('main'), handler='custom_log')

    # Send messages
    for _ in range(3):
        alice.hello('Bob')
        time.sleep(1)

    ns.shutdown()

Most of the code is similar to the one presented in the Push-Pull example, however you may notice some differences:

  1. When running Alice, a new parameter base is passed to the osbrain.run_agent() function. This means that, instead of running the default agent class, the user-defined agent class will be used instead. In this case, this class is named Greeter.
  2. The Greeter class implements two methods:
    1. on_init(): which is executed on initialization and will, in this case, simply bind a 'PUSH' communication channel.
    2. hello(): which simply logs a Hello message when it is executed.
  3. When connecting Bob to Alice, we need the address where Alice binded to. As the binding was executed on initialization, we need to use the addr() method, which will return the address associated to the alias passed as parameter (in the example above it is main).
  4. When setting a handler that is a method already defined in the agent we simply pass a string with the method name.

Setting initial attributes

Many times, after spawning an agent, we want to set some attributes, which may be used to configure the agent before it starts working with the rest of the multi-agent system:

a0 = run_agent('foo')
a0.set_attr(x=1, y=2)

It is such a common task that a parameter attributes can be used when running the agent for exactly that:

a0 = run_agent('foo', attributes=dict(x=1, y=2))

As you can see, this parameter accepts a dictionary in which the keys are the name of the attributes to be set in the agent and the values are the actual values that this attributes will take.

Note

If you find yourself setting a lot of attributes through a proxy then you might use OOP instead (set attributes on initialization or create a method for that purpose).

Creating proxies to existing name servers

Many times, specially if we are not working with distributed systems, we want to spawn a single name server and run all the agents from a single script. If that is the case, simply by executing the run_nameserver function we would obtain a proxy to the name server.

Sometimes, however, we may need to access name servers that are already running and of which we do not have a proxy available. To do so, we definitely need to know the address of the name server, so make sure you spawn it with a well-known address or save it somewhere to read it later.

You can create a proxy to an already-running name server using the NSProxy class:

from osbrain import NSProxy


ns = NSProxy(nsaddr='127.0.0.1:1234')

Note how we need to specify the name server address.

Note

This might be useful for attaching yourself to an already-running system for manual configuration/update, debugging… If you are just planning to launch and configure your architecture from multiple scripts, then think it twice, as normally you would not need to do so.