Commits

Nick V.  committed d4bf5dd

Implemented convenient Actor.response() method for call responses.

Make it easier for a regular Actor (non-Server) to respond to calls
without having to deconstruct the whole call message to extract
messageid and address. Instead just call:
self.receive(orig_message, response)

This method does the extraction of the address and message_id
behind the scenes and casts the response back.

Example:

def main(self):
pat,msg = self.receive(CALL_PATTERN)
result = dispatch[msg['method']](msg['message'])
self.respond(msg, result)

It is also possible to handle 2 types of errors in responses:
1. Invalid method :
self.respond_invalid_method(orig_message, method)
2. Exception:
self.respond_exception(orig_message, exception)

Also, made message optional for call and responses. By default
message and responses are None (JSON nulls).

Added self.respond* unittests.

  • Participants
  • Parent commits b801163

Comments (0)

Files changed (2)

File pyact/actor.py

 class RemoteException(ActorError):
     pass
 
+class InvalidCallMessage(ActorError):
+    """Message doesn't match call message shape.
+    """
+    pass
 
 def is_actor_type(obj):
     """Return True if obj is a subclass of Actor, False if not.
         ## so that actors do not share mutable state.
         self._actor._cast(json.dumps(message, default=handle_address))
 
-    def call(self, method, message, timeout=None):
+    def call(self, method, message=None, timeout=None):
         """Send a message to the Actor this object addresses.
         Wait for a result. If a timeout in seconds is passed, raise
         eventlet.TimeoutError if no result is returned in less than the timeout.
 CALL_PATTERN = {'call': str, 'method': str, 'address': Address, 'message': object}
 RESPONSE_PATTERN = {'response': str, 'message': object}
 INVALID_METHOD_PATTERN = {'response': str, 'invalid_method': str}
-
+EXCEPTION_PATTERN = {'response': str, 'exception':object}
 
 def lazy_property(property_name, property_factory, doc=None):
     def get(self):
                 return None,None
         return self._match_patterns(patterns)
 
+    def respond(self, orig_message, response=None):
+        if not shape.is_shaped(orig_message, CALL_PATTERN):
+            raise InvalidCallMessage(str(orig_message))
+        orig_message['address'].cast({'response':orig_message['call'],
+                                      'message':response})
+
+    def respond_invalid_method(self, orig_message, method):
+        if not shape.is_shaped(orig_message, CALL_PATTERN):
+            raise InvalidCallMessage(str(orig_message))
+        orig_message['address'].cast({'response':orig_message['call'],
+                                      'invalid_method':method})
+
+    def respond_exception(self, orig_message, exception):
+        if not shape.is_shaped(orig_message, CALL_PATTERN):
+            raise InvalidCallMessage(str(orig_message))
+        orig_message['address'].cast({'response':orig_message['call'],
+                                      'exception':exception})
     def add_link(self, address, trap_exit=True):
         """Link the Actor at the given Address to this Actor.
 
         try:
             while True:
                 pattern, message = self.receive(CALL_PATTERN)
-                address = message['address']
                 method = getattr(self, message['method'], None)
                 if method is None:
-                    address.cast({'response': message['call'], 'invalid_method': message['method']})
+                    self.respond_invalid_method(message, message['method'])
                     continue
                 try:
-                    result = method(message['message'])
-                    address.cast({'response': message['call'], 'message': result})
+                    self.respond(message,  method(message['message']))
                 except Exception, e:
                     formatted = exc.format_exc()
-                    address.cast({'response': message['call'], 'exception': formatted})
+                    self.respond_exception(message, formatted)
         finally:
             self.stop(*args, **kw)
 

File pyact/actor_test.py

 import unittest
 import eventlet
 from pyact import actor
+from pyact import exc
 
 
 EXCEPTION_MARKER = "Child had an exception"
         class ActiveActorMonitor(actor.Actor):
             def main(self):
                 activea = actor.spawn(ActiveActor)
-                cycle1 = activea.call('get_cycle', None)
+                cycle1 = activea.call('get_cycle')
                 self.sleep(0.001)
-                cycle2 = activea.call('get_cycle', None)
-                activea.call('die',None)
+                cycle2 = activea.call('get_cycle')
+                activea.call('die')
                 return cycle2 > cycle1
 
         self.assertEquals(actor.spawn(ActiveActorMonitor).wait(), True)
 
         self.assertEquals(actor.spawn(TimeoutCallParent).wait(), "Hi There")
 
+    def test_call_response_method(self):
+        """Start an Actor that starts another Actor and then uses
+        call on the Address. Response is send back using the response() method. 
+        Assert that the  parent gets a response from the child and returns it.
+        """
+        class CallChild(actor.Actor):
+            def main(self):
+                pat,msg = self.receive({'call':str, 'address':object,
+                                        'method':str, 'message':object})
+                if msg['method'] == 'method':
+                    self.respond(msg,'Hi There')
+        class CallParent(actor.Actor):
+            def main(self):
+                return actor.spawn(CallChild).call('method')
+        self.assertEquals(actor.spawn(CallParent).wait(), 'Hi There')
+
+    def test_call_invalid_method(self):
+        """Start an Actor that starts another Actor and then
+        uses call on the Address but using an invalid method. An
+        invalid method response should be returned.
+        """
+        class CallChild(actor.Actor):
+            def main(self):
+                pat,msg = self.receive(actor.CALL_PATTERN)
+                self.respond_invalid_method(msg,msg['method'])
+        class CallParent(actor.Actor):
+            def main(self):
+                return actor.spawn(CallChild).call('invalmeth')
+        self.assertRaises(actor.RemoteAttributeError, actor.spawn(CallParent).wait)
+        
+    def test_call_with_remote_exception(self):
+        """Start an Actor that starts another actor and calls a method
+        on it. The first actor will respond with an exception.
+        """
+        class CallChild(actor.Actor):
+            def main(self):
+                pat,msg = self.receive(actor.CALL_PATTERN)
+                try:
+                    raise ValueError("testexc")
+                except ValueError,e:
+                    formatted = exc.format_exc()
+                    self.respond_exception(msg, formatted)
+        class CallParent(actor.Actor):
+            def main(self):
+                return actor.spawn(CallChild).call('amethod')
+        self.assertRaises(actor.RemoteException, actor.spawn(CallParent).wait)
+        
+        
     def test_timeout(self):
         """Start an Actor that starts another Actor that accepts a call and
         never responds. The parent calls the child with a small timeout value.