Chapter 7. Remoting

Coupling Aspect Oriented Programming with different types of Python remoting services makes it easy to convert your local application into a distributed one. Technically, the remoting segment of Spring Python doesn't use AOP. However, it is very similar in the concept that you won't have to modify either your servers or your clients.

Distributed applications have multiple objects. These can be spread across different instances of the Python interpreter on the same machine, as well on different machines on the network. The key factor is that they need to talk to each other. The developer shouldn't have to spend a large effort coding a custom solution. Another common practice in the realm of distributed programming is that fact that programmers often develop standalone. When it comes time to distribute the application to production, the configuration may be very different. Spring Python solves this by making the link between client and server objects a step of configuration not coding.

In the context of this section of documentation, the term client refers to a client-application that is trying to access some remote service. The service is referred to as the server object. The term remote is subjective. It can either mean a different thread, a different interpretor, or the other side of the world over an Internet connection. As long as both parties agree on the configuration, they all share the same solution.

7.1. External dependencies

Spring Python currently supports and requires the installation of at least one of the libraries:

  • Pyro (Python Remote Objects) - a pure Python transport mechanism

  • Hessian - support for Hessian has just started. So far, you can call python-to-java based on libraries released from Caucho.

7.2. Remoting with PYRO (Python Remote Objects)

7.2.1. Decoupling a simple service, to setup for remoting

For starters, let's define a simple service.

class Service(object):
    def get_data(self, param):
        return "You got remote data => %s" % param

Now, we will create it locally and then call it.

service = Service()
print service.get_data("Hello")

"You got remote data => Hello"

Okay, imagine that you want to relocate this service to another instance of Python, or perhaps another machine on your network. To make this easy, let's utilize Inversion Of Control, and transform this service into a Spring service. First, we need to define an application context. We will create a file called applicationContext.xml.

<?xml version="1.0" encoding="UTF-8"?>
<objects xmlns="http://www.springframework.org/springpython/schema/objects/1.1"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/springpython/schema/objects/1.1
       		http://springpython.webfactional.com/schema/context/spring-python-context-1.1.xsd">

    <object id="service" class="Service"/>
    
</objects>

The client code is changed to this:

appContext = ApplicationContext(XMLConfig("applicationContext.xml"))
service = appContext.get_object("service")
print service.get_data("Hello")

"You got remote data => Hello"

Not too tough, ehh? Well, guess what. That little step just decoupled the client from directly creating the service. Now we can step in and configure things for remote procedure calls without the client knowing it.

7.2.2. Exporting a Spring Service Using Inversion Of Control

In order to reach our service remotely, we have to export it. Spring Python provides PyroServiceExporter to export your service through Pyro. Add this to your application context.

<?xml version="1.0" encoding="UTF-8"?>
<objects xmlns="http://www.springframework.org/springpython/schema/objects/1.1"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/springpython/schema/objects/1.1
       		http://springpython.webfactional.com/schema/context/spring-python-context-1.1.xsd">
            
    <object id="remoteService" class="Service"/>

    <object id="service_exporter" class="springpython.remoting.pyro.PyroServiceExporter">
        <property name="service_name" value="ServiceName"/>
        <property name="service" ref="remoteService"/>
    </object>

    <object id="service" class="springpython.remoting.pyro.PyroProxyFactory">
        <property name="service_url" value="PYROLOC://localhost:7766/ServiceName"/>
    </object>

</objects>

Three things have happened:

  1. Our original service's object name has been changed to remoteService.

  2. Another object was introduced called service_exporter. It references object remoteService, and provides a proxied interface through a Pyro URL.

  3. We created a client called service. That is the same name our client code it looking for. It won't know the difference!

7.2.2.1. Hostname/Port overrides

Pyro defaults to advertising the service at localhost:7766. However, you can easily override that by setting the service_host and service_port properties of the PyroServiceExporter object, either through setter or constructor injection.

<?xml version="1.0" encoding="UTF-8"?>
<objects xmlns="http://www.springframework.org/springpython/schema/objects/1.1"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/springpython/schema/objects/1.1
       		http://springpython.webfactional.com/schema/context/spring-python-context-1.1.xsd">

    <object id="remoteService" class="Service"/>

    <object id="service_exporter" class="springpython.remoting.pyro.PyroServiceExporter">
        <property name="service_name" value="ServiceName"/>
        <property name="service" ref="remoteService"/>
        <property name="service_host" value="127.0.0.1"/>
        <property name="service_port" value="7000"/>
    </object>

    <object id="service" class="springpython.remoting.pyro.PyroProxyFactory">
        <property name="service_url" value="PYROLOC://127.0.0.1:7000/ServiceName"/>
    </object>

</objects>

In this variation, your service is being hosted on port 7000 instead of the default 7766. This is also key, if you need to advertise to another IP address, to make it visible to another host.

Now when the client runs, it will fetch the PyroProxyFactory, which will use Pyro to look up the exported module, and end up calling our remote Spring service. And notice how neither our service nor the client have changed!

[Note]Python doesn't need an interface declaration for the client proxy

If you have used Spring Java's remoting client proxy beans, then you may be used to the idiom of specifying the interface of the client proxy. Due to Python's dynamic nature, you don't have to do this.

We can now split up this application into two objects. Running the remote service on another server only requires us to edit the client's application context, changing the URL to get to the service. All without telling the client and server code.

7.2.3. Do I have to use XML?

No. Again, Spring Python provides you the freedom to do things using the IoC container, or programmatically.

To do the same configuration as shown above looks like this:

from springpython.remoting.pyro import PyroServiceExporter
from springpython.remoting.pyro import PyroProxyFactory

# Create the service
remoteService = Service()

# Export it via Pyro using Spring Python's utility classes
service_exporter = PyroServiceExporter()
service_exporter.service_name = "ServiceName"
service_exporter.service = remoteService 
service_exporter.after_properties_set()

# Get a handle on a client-side proxy that will remotely call the service.
service = PyroProxyFactory()
service.service_url = "PYROLOC://127.0.0.1:7000/ServiceName"

# Call the service just you did in the original, simplified version.
print service.get_data("Hello")

Against, you can override the hostname/port values as well

...
# Export it via Pyro using Spring Python's utility classes
service_exporter = PyroServiceExporter()
service_exporter.service_name = "ServiceName"
service_exporter.service = remoteService
service_exporter.service_host = "127.0.0.1" # or perhaps the machines actual hostname
service_exporter.service_port = 7000
service_exporter.after_properties_set()
...

That is effectively the same steps that the IoC container executes.

[Note]Don't forget after_properties_set!

Since PyroServiceExporter is an InitializingObject, you must call after_properties_set in order for it to start the Pyro thread. Normally the IoC container will do this step for you, but if you choose to create the proxy yourself, you are responsible for this step.

7.2.4. Splitting up the client and the server

This configuration sets us up to run the server and the client in two different Python VMs. All we have to do is split things into two parts.

Copy the following into server.xml:

<?xml version="1.0" encoding="UTF-8"?>
<objects xmlns="http://www.springframework.org/springpython/schema/objects/1.1"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/springpython/schema/objects/1.1
       		http://springpython.webfactional.com/schema/context/spring-python-context-1.1.xsd">

    <object id="remoteService" class="server.Service"/>

    <object id="service_exporter" class="springpython.remoting.pyro.PyroServiceExporter">
        <property name="service_name" value="ServiceName"/>
        <property name="service" ref="remoteService"/>
        <property name="service_host" value="127.0.0.1"/>
        <property name="service_port" value="7000"/>
    </object>

</objects>

Copy the following into server.py:

import logging
from springpython.config import XMLConfig
from springpython.context import ApplicationContext

class Service(object):
    def get_data(self, param):
        return "You got remote data => %s" % param

if __name__ == "__main__":
    # Turn on some logging in order to see what is happening behind the scenes...
    logger = logging.getLogger("springpython")
    loggingLevel = logging.DEBUG
    logger.setLevel(loggingLevel)
    ch = logging.StreamHandler()
    ch.setLevel(loggingLevel)
    formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
    ch.setFormatter(formatter)
    logger.addHandler(ch)

    appContext = ApplicationContext(XMLConfig("server.xml"))

Copy the following into client.xml:

<?xml version="1.0" encoding="UTF-8"?>
<objects xmlns="http://www.springframework.org/springpython/schema/objects/1.1"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/springpython/schema/objects/1.1
       		http://springpython.webfactional.com/schema/context/spring-python-context-1.1.xsd">

    <object id="service" class="springpython.remoting.pyro.PyroProxyFactory">
        <property name="service_url" value="PYROLOC://127.0.0.1:7000/ServiceName"/>
    </object>

</objects>

Copy the following into client.py:

import logging
from springpython.config import XMLConfig
from springpython.context import ApplicationContext

if __name__ == "__main__":
    # Turn on some logging in order to see what is happening behind the scenes...
    logger = logging.getLogger("springpython")
    loggingLevel = logging.DEBUG
    logger.setLevel(loggingLevel)
    ch = logging.StreamHandler()
    ch.setLevel(loggingLevel)
    formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
    ch.setFormatter(formatter)
    logger.addHandler(ch)

    appContext = ApplicationContext(XMLConfig("client.xml"))
    service = appContext.get_object("service")
    print "CLIENT: %s" % service.get_data("Hello")

First, launch the server script, and then launch the client script, both on the same machine. They should be able to talk to each other with no problem at all, producing some log chatter like this:

$ python server.py &
[1] 20854

2009-01-08 12:06:20,021 - springpython.container.ObjectContainer - DEBUG - === Scanning configuration <springpython.config.XMLConfig object at 0xb7fa276c> for object definitions ===
2009-01-08 12:06:20,021 - springpython.config.XMLConfig - DEBUG - ==============================================================
2009-01-08 12:06:20,022 - springpython.config.XMLConfig - DEBUG - * Parsing server.xml
2009-01-08 12:06:20,025 - springpython.config.XMLConfig - DEBUG - ==============================================================
2009-01-08 12:06:20,025 - springpython.container.ObjectContainer - DEBUG - remoteService object definition does not exist. Adding to list of definitions.
2009-01-08 12:06:20,026 - springpython.container.ObjectContainer - DEBUG - service_exporter object definition does not exist. Adding to list of definitions.
2009-01-08 12:06:20,026 - springpython.container.ObjectContainer - DEBUG - === Done reading object definitions. ===
2009-01-08 12:06:20,026 - springpython.context.ApplicationContext - DEBUG - Eagerly fetching remoteService
2009-01-08 12:06:20,026 - springpython.context.ApplicationContext - DEBUG - Did NOT find object 'remoteService' in the singleton storage.
2009-01-08 12:06:20,026 - springpython.context.ApplicationContext - DEBUG - Creating an instance of id=remoteService props=[] scope=scope.SINGLETON factory=ReflectiveObjectFactory(server.Service)
2009-01-08 12:06:20,026 - springpython.factory.ReflectiveObjectFactory - DEBUG - Creating an instance of server.Service
2009-01-08 12:06:20,027 - springpython.context.ApplicationContext - DEBUG - Stored object 'remoteService' in container's singleton storage
2009-01-08 12:06:20,027 - springpython.context.ApplicationContext - DEBUG - Eagerly fetching service_exporter
2009-01-08 12:06:20,027 - springpython.context.ApplicationContext - DEBUG - Did NOT find object 'service_exporter' in the singleton storage.
2009-01-08 12:06:20,027 - springpython.context.ApplicationContext - DEBUG - Creating an instance of id=service_exporter props=[<springpython.config.ValueDef object at 0xb7a4664c>, <springpython.config.ReferenceDef object at 0xb7a468ac>, <springpython.config.ValueDef object at 0xb7a4692c>, <springpython.config.ValueDef object at 0xb7a46d2c>] scope=scope.SINGLETON factory=ReflectiveObjectFactory(springpython.remoting.pyro.PyroServiceExporter)
2009-01-08 12:06:20,028 - springpython.factory.ReflectiveObjectFactory - DEBUG - Creating an instance of springpython.remoting.pyro.PyroServiceExporter
2009-01-08 12:06:20,028 - springpython.context.ApplicationContext - DEBUG - Stored object 'service_exporter' in container's singleton storage
2009-01-08 12:06:20,028 - springpython.remoting.pyro.PyroServiceExporter - DEBUG - Exporting ServiceName as a Pyro service at 127.0.0.1:7000
2009-01-08 12:06:20,029 - springpython.remoting.pyro.PyroDaemonHolder - DEBUG - Registering ServiceName at 127.0.0.1:7000 with the Pyro server
2009-01-08 12:06:20,029 - springpython.remoting.pyro.PyroDaemonHolder - DEBUG - Pyro thread needs to be started at 127.0.0.1:7000
2009-01-08 12:06:20,030 - springpython.remoting.pyro.PyroDaemonHolder._PyroThread - DEBUG - Starting up Pyro server thread for 127.0.0.1:7000

$ python client.py

2009-01-08 12:06:26,291 - springpython.container.ObjectContainer - DEBUG - === Scanning configuration <springpython.config.XMLConfig object at 0xb7ed45ac> for object definitions ===
2009-01-08 12:06:26,292 - springpython.config.XMLConfig - DEBUG - ==============================================================
2009-01-08 12:06:26,292 - springpython.config.XMLConfig - DEBUG - * Parsing client.xml
2009-01-08 12:06:26,294 - springpython.config.XMLConfig - DEBUG - ==============================================================
2009-01-08 12:06:26,294 - springpython.container.ObjectContainer - DEBUG - service object definition does not exist. Adding to list of definitions.
2009-01-08 12:06:26,294 - springpython.container.ObjectContainer - DEBUG - === Done reading object definitions. ===
2009-01-08 12:06:26,295 - springpython.context.ApplicationContext - DEBUG - Eagerly fetching service
2009-01-08 12:06:26,295 - springpython.context.ApplicationContext - DEBUG - Did NOT find object 'service' in the singleton storage.
2009-01-08 12:06:26,295 - springpython.context.ApplicationContext - DEBUG - Creating an instance of id=service props=[<springpython.config.ValueDef object at 0xb797948c>] scope=scope.SINGLETON factory=ReflectiveObjectFactory(springpython.remoting.pyro.PyroProxyFactory)
2009-01-08 12:06:26,295 - springpython.factory.ReflectiveObjectFactory - DEBUG - Creating an instance of springpython.remoting.pyro.PyroProxyFactory
2009-01-08 12:06:26,295 - springpython.context.ApplicationContext - DEBUG - Stored object 'service' in container's singleton storage

CLIENT: You got remote data => Hello

This shows one instance of Python running the client, connecting to the instance of Python hosting the server module. After that, moving these scripts to other machines only requires changing the hostname in the XML files.

7.3. Remoting with Hessian

[Note]Caucho's python library for Hessian is incomplete

Due to minimal functionality provided by Caucho's Hessian library for python, there is minimal documentation to show its functionality.

The following shows how to connect a client to a Hessian-exported service. This can theoretically be any technology. Currently, Java objects are converted into python dictionaries, meaning that the data and transferred, but there are not method calls available.

<?xml version="1.0" encoding="UTF-8"?>
<objects xmlns="http://www.springframework.org/springpython/schema/objects/1.1"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/springpython/schema/objects/1.1
       		http://springpython.webfactional.com/schema/context/spring-python-context-1.1.xsd">

	<object id="personService" class="springpython.remoting.hessian.HessianProxyFactory">
		<property name="service_url"><value>http://localhost:8080/</value></property>
	</object>

</objects>

The Caucho library appears to only support Python being a client, and not yet as a service, so there is no HessianServiceExporter available yet.

7.4. High-Availability/Clustering Solutions

This props you up for many options to increase availability. It is possible to run a copy of the server on multiple machines. You could then institute some type of round-robin router to go to different URLs. You could easily run ten copies of the remote service.

pool = []
for i in range(10):
    service_exporter = PyroServiceExporter(service_name = "ServiceName%s" % i, service = Service())
    pool.append(service_exporter)

(Yeah, I know, you can probably do this in one line with a list comprehension).

Now you have ten copies of the server running, each under a distinct name.

For any client, your configuration is a slight tweak.

services = []
for i in range(10):
    services.append(PyroProxyFactory(service_url = "PYROLOC://localhost:7766/ServiceName%s" % i))

Now you have an array of possible services to reach, easily spread between different machines. With a little client-side utility class, we can implement a round-robin solution.

class HighAvailabilityService(object):
    def __init__(self, service_pool):
        self.service_pool = service_pool
        self.index = 0
    def get_data(self, param):
        self.index = (self.index+1) % len(self.service_pool)
        try:
            return self.service_pool[self.index].get_data(param)
        except:
            del(self.service_pool[i])
            return self.get_data(param)

service = HighAvailabilityService(service_pool = services)
service.get_data("Hello")
service.get_data("World")

Notice how each call to the HighAvailabilityService class causes the internal index to increment and roll over. If a service doesn't appear to be reachable, it is deleted from the list and attempted again. A little more sophisticated error handling should be added in case there are no services available. And there needs to be a way to grow the services. But this gets us off to a good start.