2. Pyro Concepts
For a good understanding of Pyro it is necessary to know the different elements in a Pyro system. This chapter summarizes them.
Keep in mind that in a distributed object system, the client/server style is difficult to see. Most of the time all parts of the system switch roles, one moment it's a client calling a remote object, the other moment it is itself an object that is called from other parts of the system.
For a good understanding though, it is important to see that during a single method call, there are always two distinct parts of the system: the client part that initiates the method call, and the server part that accepts and executes the call.
To be precise, there are actually three parts: between the client and the server is the distributed object middleware, in this case: Pyro.
Another issue is that a client can - of course! - use more than one remote object, but also that a single server can have more than one object implementation.
Thus, single objects in their own right are neither a client nor a server. I'll define the executable parts of the system that contain the objects as clients and servers (depending on their actual role).
For simple Pyro applications, usually the different Python modules clearly show the different parts of the system, and they are the clients and servers I'm talking about here.
If you want a technical and more in-depth description of the material presented here, read the chapter on Pyro's implementation, or just browse the source code.
Pyro provides several tools to help you during development of a Pyro application. The Pyro Naming Service is also started and controlled by two of these scripts. See the chapter on the Naming Service for more information about them.
This is the part of your system that sends requests to the server program, to perform certain actions. It's the code that actually uses the remote objects by calling their methods.
Pyro client programs look suspiciously like normal Python programs. But that's the whole point of Pyro: it enables you to build distributed object systems with minimal effort. It makes the use of remote objects (almost) transparent.
Client code has to perform some initialization and setup steps. And because we're talking remote objects here, they cannot create object instances in the usual way. They have to use a two-step mechanism:
- Find the location identifier of the required object. This is done by using the Pyro Naming Service, see below.
- Create a special kind of object that actually calls the remote object. This is called a proxy, see below.
Once it has this proxy object, the client can call it just as if it were a regular -local- Python object.
The server is the home of the objects that are accessed remotely. Every object instance has to be part of a Python program, so this is it. The server has to do several things:
- Create object instances using a extremely tiny bit of Pyro plumbing
- Give names to those instances, and register these with the Naming Service
- Announce to Pyro that it has to take care of these instances
- Tell Pyro to sit idle in a loop waiting for incoming method calls
Aside from the restrictions given in the chapter on Rules and Limitations, a Pyro object is just a regular Python object.
The object doesn't know and doesn't have to know it's part of a Pyro server, and called remotely.
A proxy is a special kind of object that acts as if it were the actual -remote- object. Pyro clients have to use proxies to forward method calls to the remote objects, and pass results back to the calling code.
Pyro knows two kinds of proxies, and you are free to chose from them:
- Static proxy.
This is a Python object that is a proxy for exactly one other object. There is a Pyro tool to generate this proxy code for you. You better not write it yourself because there is a chance it won't be compatible with future Pyro versions.
- Dynamic proxy.
This is a very special Python object provided by Pyro. It is a general proxy for all remote objects! Because it is general, you lose some things: performance is worse than static proxies, and run-time checking of method calls and arguments is less strict and requires an actual call to the remote object.
- Dynamic proxy with attribute access support.
Pyro 1.2 introduced a new dynamic proxy type: one that allows you to access object attributes directly with normal Python syntax.
Because this requires even more builtin logic, this proxy is a few percent slower than the others. You can choose to use this proxy, or one of the others.
Proxies are bound to certain location identifiers, so they know on whose behalf the're running.
It's getting more technical now. Each server has to have a way of getting remote method calls and dispatching them to the required objects. For this task Pyro provides a Daemon. A server just creates one of those and tells it to sit waiting for incoming requests. The Daemon takes care of everything from that moment on.
Every server has one Daemon that knows about all the Pyro objects the server provides.
One of the most useful services a distributed object system can have is a naming service. Such a service is a central database that knows the names and corresponding locations of all objects in the system.
Pyro has a Naming Server that performs this task very well. Pyro Servers register their objects and location with the Naming Server.
Pyro Clients query the server for location identifiers. They do this by providing (human-readable) object names. They get a Pyro Universal Resource Identifier (URI) in return (which is not intended for humans. However, as it is, the current PYRO URI's look just like WWW URL's and can be read quite nicely).
You might be wondering: how can a Pyro client find the Naming Server itself?! Good question. There are three possibilities:
- Rely on the broadcast capability of the underlying network. This is extremely easy to use (you don't have to do anything!) and works in most cases.
- Somehow obtain the hostname of the machine the Naming Service is running on. You can then directly contact this machine. The Naming Service will respond with its location identifier.
- Obtain the location identifier using another way such as file transfer or email. The Naming Service writes its location identifier to a special file and you can read it from there. Then you can create a Naming Service proxy directly, bypassing the Naming Service Locator completely.
Object names can be anything you like as long as there isn't another object with that name already. So it's good practice to use some sort of hierarchical naming scheme, just like Java uses for Java package naming. This reduces the risk of naming clashes dramatically.
In fact, since version 1.1, Pyro's Naming Service is a fully hierarchical naming service that has a filesystem-like directory structure of groups and object names in those groups.
See the Naming Service chapter for a description of the naming scheme.
Notice: the Pyro.xxxxx
namespace is reserved for Pyro itself (for instance, the naming server is called Pyro.NameServer
). Don't use it.
The communication between a client and a server basically consists of two kinds of messages:
- Method call request.
This message consists of some identification of the target object, the method called, and the arguments for this call.
- Return value reply.
This message is no more than the return value of the method call.
By default Pyro uses the PYRO protocol (duh!) that relies on Python's built-in pickle
facility to create these messages. The transport over the network is done using TCP/IP.
The way I designed Pyro should make it easy to use other protocols, but for now, only the PYRO protocol is implemented.
A different protocol is used for the initial communication with the Naming Server. Pyro uses UDP broadcasting over IP to discover the Naming Server in the local subnet, and asks it to report back.
Once Pyro knows the location ID of the Naming Server, it switches to the PYRO protocol, because the Naming Server is just another Pyro object!
What happens when an error occurs in your server?
Pyro can't help you when your server crashes. But it does catch the Python exceptions that occur in your remote objects. They are sent back to the calling code and raised again, just as if they occurred locally. The occurrence is logged in the server log. Thus, Pyro makes no distinction between user generated exceptions (in the remote object) or exceptions that occur because of runtime errors, such as divide by zero.
The client (as it should!) has no clean way of telling whether the exception was raised locally or in the remote object. Any Pyro induced errors will be signaled by a PyroError
exception or one of its exception subclasses.
There is a slight problem with this scheme: traceback objects and stack traces are virtually meaningless if the exception occurred in the remote object.
A good trace facility is paramount in a complex system. Therefore Pyro provides a simple to use logger. It writes messages to a configurable logfile, annotated with a timestamp. It distinguishes errors, warnings and regular notices.
Pyro uses it depending on the tracelevel you configured.
You can use the logging facility in your own code too. There is a special user version of the logger that operates independently of the Pyro system logger. You can configure the trace level and logfile location uniquely for both loggers.
By default, logging is turned off completely. You can simply turn it on during the course of your program's execution, or beforehand by setting a simple Pyro configuration option.