Repository: opensistemas-hub/osbrain
Pull request #53 (2017-03-02 12:20:20)

Changes to prepare osBrain for asynchronous requests
- Unique identifiers for agents.
- Reimplementation of AgentAddressKind as string type.


Patch:
diff --git a/osbrain/address.py b/osbrain/address.py
index ebb0ef0..cf7af4d 100644
--- a/osbrain/address.py
+++ b/osbrain/address.py
@@ -95,23 +95,22 @@ class AgentAddressRole(str):
         return self.__class__('server')
 
 
-class AgentAddressKind(int):
+class AgentAddressKind(str):
     """
-    Agent's address kind class. It can be any ZMQ type ('REP', 'PUB'...).
+    Agent's address kind class.
 
-    Inherits from `int` to be compatible with ZMQ definitions, however,
-    it is represented in its string form. The equivalence can also be
-    evaluated against its string form.
+    This kind represents the communication pattern being used by the agent
+    address: REP, PULL, PUB...
     """
-    ZMQ_KIND_TWIN = {
-        zmq.REQ: zmq.REP,
-        zmq.REP: zmq.REQ,
-        zmq.PUSH: zmq.PULL,
-        zmq.PULL: zmq.PUSH,
-        zmq.PUB: zmq.SUB,
-        zmq.SUB: zmq.PUB,
+    TWIN = {
+        'REQ': 'REP',
+        'REP': 'REQ',
+        'PUSH': 'PULL',
+        'PULL': 'PUSH',
+        'PUB': 'SUB',
+        'SUB': 'PUB',
     }
-    ZMQ_STR_CONVERSION = {
+    ZMQ_KIND_CONVERSION = {
         'REQ': zmq.REQ,
         'REP': zmq.REP,
         'PUSH': zmq.PUSH,
@@ -119,32 +118,21 @@ class AgentAddressKind(int):
         'PUB': zmq.PUB,
         'SUB': zmq.SUB
     }
-    keys = list(ZMQ_STR_CONVERSION.keys())
-    for key in keys:
-        ZMQ_STR_CONVERSION[ZMQ_STR_CONVERSION[key]] = key
+    REQUIRE_HANDLER = ('REP', 'PULL', 'SUB')
 
     def __new__(cls, kind):
-        if kind not in cls.ZMQ_STR_CONVERSION:
+        if kind not in cls.TWIN.keys():
             raise ValueError('Invalid address kind "%s"!' % kind)
-        if isinstance(kind, str):
-            kind = cls.ZMQ_STR_CONVERSION[kind]
         return super().__new__(cls, kind)
 
-    def __eq__(self, other):
-        if isinstance(other, int):
-            return int(self) == other
-        if isinstance(other, str):
-            return str(self) == other
-        return False
-
-    def __str__(self):
-        return self.ZMQ_STR_CONVERSION[self]
-
-    def __repr__(self):
-        return str(self)
-
-    def __hash__(self):
-        return hash(int(self))
+    def zmq(self):
+        """
+        Returns
+        -------
+        int
+            The equivalent ZeroMQ socket kind.
+        """
+        return self.ZMQ_KIND_CONVERSION[self]
 
     def requires_handler(self):
         """
@@ -155,9 +143,7 @@ class AgentAddressKind(int):
             A socket which processes incoming messages would require a
             handler (i.e. 'REP', 'PULL', 'SUB'...).
         """
-        if self.ZMQ_STR_CONVERSION[self] in ('REQ', 'PUSH', 'PUB'):
-            return False
-        return True
+        return self in self.REQUIRE_HANDLER
 
     def twin(self):
         """
@@ -168,7 +154,7 @@ class AgentAddressKind(int):
             of `REP` and viceversa, `PUB` would be the twin of `SUB` and
             viceversa, etc.
         """
-        return self.__class__(self.ZMQ_KIND_TWIN[self])
+        return self.__class__(self.TWIN[self])
 
 
 class AgentAddressSerializer(str):
diff --git a/osbrain/agent.py b/osbrain/agent.py
index 2befc56..81e20df 100644
--- a/osbrain/agent.py
+++ b/osbrain/agent.py
@@ -162,6 +162,7 @@ class Agent():
         Set to `True` if the agent is running (executing the main loop).
     """
     def __init__(self, name=None, host=None, serializer=None):
+        self.uuid = uuid4().hex
         self.name = name
         self.host = host
         if not self.host:
@@ -387,7 +388,7 @@ class Agent():
         level = LogLevel(level)
         message = '[%s] (%s): %s' % (datetime.utcnow(), self.name, message)
         if self.registered(logger):
-            logger_kind = AgentAddressKind(self.socket[logger].socket_type)
+            logger_kind = AgentAddressKind(self.address[logger].kind)
             assert logger_kind == 'PUB', \
                 'Logger must use publisher-subscriber pattern!'
             self.send(logger, message, topic=level)
@@ -480,6 +481,7 @@ class Agent():
         self.socket[alias] = socket
         self.socket[address] = socket
         self.address[alias] = address
+        self.address[socket] = address
         self.address[address] = address
         if handler is not None:
             self.poller.register(socket, zmq.POLLIN)
@@ -548,7 +550,7 @@ class Agent():
         kind = AgentAddressKind(kind)
         assert not kind.requires_handler() or handler is not None, \
             'This socket requires a handler!'
-        socket = self.context.socket(kind)
+        socket = self.context.socket(kind.zmq())
         transport = transport or os.environ.get('OSBRAIN_DEFAULT_TRANSPORT')
         serializer = serializer \
             or self.serializer \
@@ -629,7 +631,7 @@ class Agent():
         return client_address
 
     def _connect_new(self, client_address, alias=None, handler=None):
-        socket = self.context.socket(client_address.kind)
+        socket = self.context.socket(client_address.kind.zmq())
         socket.connect('%s://%s' % (client_address.transport,
                                     client_address.address))
         self.register(socket, client_address, alias, handler)
@@ -780,9 +782,9 @@ class Agent():
         for socket in events:
             if events[socket] != zmq.POLLIN:
                 continue
-            self._process_socket_event(socket)
+            self._process_single_event(socket)
 
-    def _process_socket_event(self, socket):
+    def _process_single_event(self, socket):
         """
         Process a socket's event.
 
@@ -792,7 +794,7 @@ class Agent():
             Socket that generated the event.
         """
         serialized = socket.recv()
-        socket_kind = AgentAddressKind(socket.socket_type)
+        socket_kind = AgentAddressKind(self.address[socket].kind)
         if socket_kind == 'SUB':
             self._process_sub_event(socket, serialized)
         else:


Test patch:
diff --git a/osbrain/tests/test_address.py b/osbrain/tests/test_address.py
index 6d81986..ed24376 100644
--- a/osbrain/tests/test_address.py
+++ b/osbrain/tests/test_address.py
@@ -59,31 +59,35 @@ def test_transport():
         AgentAddressTransport('foo')
 
 
-def test_kind():
+@pytest.mark.parametrize('string,strtwin,zmqint,zmqtwin,handler', [
+    ('REQ', 'REP', zmq.REQ, zmq.REP, False),
+    ('REP', 'REQ', zmq.REP, zmq.REQ, True),
+    ('PUSH', 'PULL', zmq.PUSH, zmq.PULL, False),
+    ('PULL', 'PUSH', zmq.PULL, zmq.PUSH, True),
+    ('PUB', 'SUB', zmq.PUB, zmq.SUB, False),
+    ('SUB', 'PUB', zmq.SUB, zmq.PUB, True),
+])
+def test_kind(string, strtwin, zmqint, zmqtwin, handler):
     """
     This test aims to cover basic AgentAddressKind operations: initialization,
     equivalence and basic methods.
     """
-    strings = ['REQ', 'REP', 'PUSH', 'PULL', 'PUB', 'SUB']
-    zmqints = [zmq.REQ, zmq.REP, zmq.PUSH, zmq.PULL, zmq.PUB, zmq.SUB]
-    handlers = [False, True, False, True, False, True]
-    strtwins = twin_list(strings)
-    zmqtwins = twin_list(zmqints)
-    configurations = zip(strings, strtwins, zmqints, zmqtwins, handlers)
-    # Make sure there are no missing values
-    assert len(list(configurations)) == len(strings)
+    # Initialization and equivalence
+    kind = AgentAddressKind(string)
+    assert kind == string
+    assert kind == AgentAddressKind(kind)
+    # Basic methods
+    assert kind.zmq() == zmqint
+    assert isinstance(kind.twin(), AgentAddressKind)
+    assert kind.twin() == strtwin
+    assert kind.requires_handler() == handler
 
-    for string, strtwin, zmqint, zmqtwin, handler in configurations:
-        # Initialization and equivalence
-        kind = AgentAddressKind(string)
-        assert kind == zmqint
-        assert kind == string
-        assert kind == AgentAddressKind(zmqint)
-        assert kind == AgentAddressKind(kind)
-        # Basic methods
-        assert isinstance(kind.twin(), AgentAddressKind)
-        assert kind.twin() == strtwin
-        assert kind.requires_handler() == handler
+
+def test_kind_value_error():
+    """
+    Creating an AgentAddressKind with a wrong value should result in an
+    exception being raised.
+    """
     # Value error exceptions
     with pytest.raises(ValueError):
         AgentAddressKind('FOO')
diff --git a/osbrain/tests/test_agent.py b/osbrain/tests/test_agent.py
index 72d0f8b..18e9236 100644
--- a/osbrain/tests/test_agent.py
+++ b/osbrain/tests/test_agent.py
@@ -47,6 +47,16 @@ def logger_received(logger, log_name, message, timeout=1.):
     return True
 
 
+def test_agent_uuid():
+    """
+    All agent identifiers should be unique strings.
+    """
+    N = 1000
+    bunch = set(Agent().uuid for i in range(N))
+    assert len(bunch) == N
+    assert all(isinstance(identifier, str) for identifier in bunch)
+
+
 def test_early_agent_proxy(nsaddr):
     """
     It must be possible to create a Proxy when the registration of the new
@@ -164,51 +174,30 @@ def test_socket_creation(nsaddr):
     Test ZMQ socket creation.
     """
     a0 = run_agent('a0')
-    a0.bind('REQ', 'alias0')
-    a0.bind('PUB', 'alias1')
-    a0.bind('PUSH', 'alias2')
-    addresses = a0.get_attr('address')
-    assert 'alias0' in addresses
-    assert 'alias1' in addresses
-    assert 'alias2' in addresses
-
-
-def test_correct_serialization(nsaddr):
+    addr0 = a0.bind('REQ', 'alias0')
+    addr1 = a0.bind('PUB', 'alias1')
+    addr2 = a0.bind('PUSH', 'alias2')
+    assert a0.addr('alias0') == addr0
+    assert a0.addr('alias1') == addr1
+    assert a0.addr('alias2') == addr2
+
+
+@pytest.mark.parametrize('agent_serial,socket_serial,result', [
+    (None, None, os.getenv('OSBRAIN_DEFAULT_SERIALIZER')),
+    ('raw', None, 'raw'),
+    ('pickle', None, 'pickle'),
+    (None, 'raw', 'raw'),
+    (None, 'json', 'json'),
+    ('pickle', 'json', 'json'),
+])
+def test_correct_serialization(nsaddr, agent_serial, socket_serial, result):
     """
     Test that the right serializer is being used when using the different
     initialization options.
     """
-    # Without specifying a serializer
-    a0 = run_agent('a0')
-    addr0 = a0.bind('PUB', 'alias0')
-    _address = a0.get_attr('address')
-    assert _address[addr0].serializer \
-        == os.getenv('OSBRAIN_DEFAULT_SERIALIZER')
-
-    # Specifying a serializer at Agent level
-    # Test is twice with different serializer to avoid collision with the
-    # environment variable
-    a1 = run_agent('a1', serializer='raw')
-    addr1 = a1.bind('PUB', 'alias1')
-    _address = a1.get_attr('address')
-    assert _address[addr1].serializer == 'raw'
-
-    a2 = run_agent('a2', serializer='pickle')
-    addr2 = a2.bind('PUB', 'alias2')
-    _address = a2.get_attr('address')
-    assert _address[addr2].serializer == 'pickle'
-
-    # Specifying a serializer at Socket level
-    # Test is twice with different serializer to avoid collision with the
-    # environment variable
-    a3 = run_agent('a3')
-    addr3 = a3.bind('PUB', 'alias3', serializer='raw')
-    _address = a3.get_attr('address')
-    assert _address[addr3].serializer == 'raw'
-    a4 = run_agent('a4')
-    addr4 = a4.bind('PUB', 'alias4', serializer='json')
-    _address = a4.get_attr('address')
-    assert _address[addr4].serializer == 'json'
+    agent = run_agent('a0', serializer=agent_serial)
+    addr = agent.bind('PUB', serializer=socket_serial)
+    assert addr.serializer == result
 
 
 def test_reqrep(nsaddr):