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:
- Find the location identifier of the required object. This is done by using the Pyro Name Server, 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.
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:
- Create object instances using a extremely tiny bit of Pyro plumbing
- Give names to those instances, and register these with the Name Server
- 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
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:
- Dynamic proxy.
This is a very special Python object provided by Pyro. It is a general proxy for all remote objects!
- 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:
- 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 Name Server is running on. You can then directly contact this
machine. The Name Server will respond with its location identifier.
- Obtain the location identifier using another way such as file transfer or email. The Name Server writes its
location identifier to a special file and you can read it from there. Then you can create a Name Server proxy
directly, bypassing the Name Server 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, 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:
- 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.
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).