Simple RPC Python Module Part II

April 11th, 2011

Let’s have a look at the client implementation. On the client side, we mainly use two objects to communicate with the server: the client object and the attribute wrapper object.

The client object appears, to the user, as pretty much identical to the server object: all the (public) methods of the server object can be accessed on the client object as well, and — for as far as possible — the results of these methods should be identical as well. The client object’s communication with the server object should be as transparent as possible. As such, the implementation of the client object may strike as rather simple:

class PyRPCClient(object):
    def __init__(self, address, debug=False):
        """
        Connection to an RPC server.  Initialized with a given address as
        expected by the socket module.

        If the optional flag `debug' is set to true, when an exception occurs,
        before raising it, the client will print the formatted traceback from
        the server side.  This is useful for debugging.
        """
        self.address = address
        self.debug = debug

    def __getattribute__(self, attribute):
        """
        Overloaded attribute accessor, to transfer local attribute access to
        RPC calls.
        """
        # NOTE: Because we overload __getattribute__, we have to access our
        # OWN attributes by using the __getattribute__ method of our parent
        # class, `object'.  Otherwise we end up in an infinite loop.
        return PyRPCAttributeWrapper(object.__getattribute__(self, "address"),
                attribute, object.__getattribute__(self, "debug"))
There’s really nothing special about it. All this implementation does, is to return a PyRPCAttributeWrapper instance for each and every attribute that is being accessed on the client object. It is this attribute wrapper object that does all the magic. So let’s move on to that.

When instantiated, all the attribute wrapper object is given, are the address of the server object, the attribute to call, and a debugging flag. The constructor is rather trivial, it simply stores these values into members of the instance, for later reference.

As I explained in the previous post, the limitations of this design impose that only methods of the server object can be accessed. As such, the only other method of the attribute wrapper, is the __call__() method. This special Python construct allows an instance of this class to be executed like a function or — in this case — a method.

def __call__(self, *args, **kwargs):
    # We need to loop here, in case the server is too busy to serve us
    # now.
    loop = True
    while loop:
        data = pickle.dumps((self.attribute, args, kwargs))
        # Ten connection attempts.
        for i in range(10):
            # Instantiation here for repeated connection attempts.
            try:
                self.__socket = socket.socket(socket.AF_INET,
                        socket.SOCK_STREAM)
                self.__socket.connect(self.address)
            except:
                # Connection failed, wait a bit and then retry.
                self.__socket = None
                time.sleep(0.001)
            else:
                # Connection successful!  Exit the for loop.
                break
        if not self.__socket:
            # Ten connection attempts failed.
            raise Exception("Failed to connect to %s (call: %s)", (
                    self.address, (self.attribute, args, kwargs)))
        length = self.__socket.send(data)
        while length < len(data):
            data = data[length:]
            length = self.__socket.send(data)
        # If the following call fails with a "Connection reset by peer"
        # error, we actually need to start all over again.
        try:
            response = self.__socket.recv(1024)
        except socket.error, err:
            if err[0] == 104: # Connection reset by peer.
                # We should try again.
                self.__socket.close() # Close to reconnect again.
                del self.__socket # Clear up memory right away.
                # XXX Should we limit this loop to a certain amount of
                # iterations?
                # Wait a little before re-trying.
                time.sleep(0.001)
            else:
                # All other exceptions should still be raised.
                raise err
        else:
            loop = False
            break
    # End of while loop.
    loop = True
    while loop:
        try:
            result = pickle.loads(response)
            loop = False
        except:
            # Not enough, read more.
            response += self.__socket.recv(1024)
    self.__socket.close()
    # Check for exceptions.
    if isinstance(result, PyRPCException):
        if self.debug:
            # Print the traceback info.
            print "Server side traceback:\n" + result.traceback
        raise result.exception
    else:
        return result
There is no need to query the signature of the method on the server side, because Python allows us to simply catch any possible signature using the two special arguments *args and **kwargs, to catch any fixed or named argument, respectively (note that the names are simply conventions, the important bits are the asterisks).

The main execution consists of two loops. In the first loop, the call data is serialised using the pickle module and then ten attempts are made to connect to the server object. Please note that the pickle module is not safe as such, but as I explained in the previous post, security was never an issue when designing this module.

Once connected, the data is transmitted to the server object. This data consists of a simple tuple, containing the name of the attribute being called and all fixed and named arguments. I found that in the setup I was deploying this module, I sometimes would get a “connection reset by peer” error, which meant that the transmission had to be repeated.

The second loop deals with receiving the server’s response. Other than reading the data back over the connection, this part of the code only deals with the special case in which the returned object is an instance of the PyRPCException class. This is a special class the server object uses to represent exceptions that occurred while executing the requested method. If such an object is received by the client, the given exception is raised to — again — transparently mimic the server object’s behaviour. Otherwise, the object that was received is returned.

Next time, we shall look at the implementation of the exception class as well as some details of the server implementation.

in Python

Leave a Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Spam protection by WP Captcha-Free