2. Pyro Concepts

Introduction

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 shell scripts

Pyro provides several tools to help you during development of a Pyro application. The Pyro Name Server is also started and controlled by two of these scripts. See the chapter on the Name Server for more information about them.

Client program

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:
  1. Find the location identifier of the required object. This is done by using the Pyro Name Server, see below.
  2. 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.

Server program

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:
  1. Create object instances using a extremely tiny bit of Pyro plumbing
  2. Give names to those instances, and register these with the Name Server
  3. Announce to Pyro that it has to take care of these instances
  4. Tell Pyro to sit idle in a loop waiting for incoming method calls

Remote object

Aside from the restrictions given in the Features and Guidelines chapter, 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.

Proxy

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:
  1. Dynamic proxy.
    This is a very special Python object provided by Pyro. It is a general proxy for all remote objects!
  2. Dynamic proxy with attribute access support.
    This allows you to access object attributes directly with normal Python syntax (for instance, print RemoteObj.name prints the attribute 'name' of the remote object 'RemoteObj'). 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. Note: pay attention to the following issue when using attribute proxies: they might raise unexptected exceptions on the client! If your attribute is an object of a certain class that is available on the server, but not on the client, you will not be able to access the attribute on the client! Also, when you are using 'nested' attributes (such as RemoteObj.person.address.street) you must make sure that all needed classes (and modules) are available on the client. It is perhaps easiest to put such classes in separate modules and place them somewhere where the client is able to import them. An explicit import statement is not necessary in the client. When done correctly, you're also able to call 'nested' methods such as RemoteObj.person.address.getStreet(). The "attributes" example shows how it's done. But there are some serious pitfalls when using 'nested' attribute or method access! Please read the info about this in the Features and Guidelines chapter!
Proxies are bound to certain location identifiers, so they know on whose behalf the're running.

Proxy objects can be pickled: you can toss proxy objects around within your Pyro system. For more info see the guidelines in the "Features and Guidelines" chapter.

Pyro daemon

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.

Location discovery and naming

One of the most useful services a distributed object system can have is a Name Server. Such a service is a central database that knows the names and corresponding locations of all objects in the system.
Pyro has a Name Server that performs this task very well. Pyro Servers register their objects and location with the Name 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 URIs look just like WWW URLs and can be read quite nicely).
You might be wondering: how can a Pyro client find the Name Server itself?! Good question. There are three possibilities: 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, Pyro's Name Server is a fully hierarchical naming service that has a filesystem-like directory structure of groups and object names in those groups. See the Name Server chapter for a description of the naming scheme.
Notice: the Pyro.xxxxx namespace is reserved for Pyro itself (for instance, the Name Server is called Pyro.NameServer). Don't use it.

There are two other ways to connect to a certain object you know the name of. These are the PYRONAME:// and the PYROLOC:// URI formats, instead of the regular PYRO:// URIs. The first is essentially a shortcut to the Name Server; it will find and query the Name Server under the surface (you don't have to do anything). The second bypasses the Name Server completely and directly queries a Pyro server host for the required object. You can find more information on these two methods in the Name Server chapter.

Communication protocol

The communication between a client and a server basically consists of two kinds of messages: 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.

Because PYRO uses pickle, everything that has to go over the wire should be pickleable. If you are using objects that can't be pickled, you will get a TypeError. Examples of objects that cannot be pickled are file objects and socket objects. Pyro can also use other marshaling methods (like XML marshaling) but still, the data you're transmitting must be pickleable. Transmitting file objects is impossible for instance.

A different protocol is used for the initial communication with the Name Server. Pyro uses UDP broadcasting over IP to discover the Name Server in the local subnet, and asks it to report back. Once Pyro knows the location ID of the Name Server, it switches to the PYRO protocol, because the Name Server is just another Pyro object!

Exceptions

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 has no way of telling whether the exception was raised locally or in the remote object. Well, there is a way, but you should really not depend on it. It's only to facilitate debugging in case of a remote exception (the remote exception is contained within the local exception). Any errors from Pyro itself 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. However, Pyro comes to the rescue: the stack trace of the remote exception is passed over the network to the calling object and can be printed there by using a utility function. So it is possible to see what remote code caused the problem.

Logging

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.

If it is available Pyro can also use the standard (or defacto) Python logging functionality from the logging module. Enable this by setting the PYRO_STDLOGGING config item to 1 (see config chapter). Also you can specify an alternate path for the logging configuration file via the PYRO_STDLOGGING_CFGFILE config item (see config chapter).