Python 7.0pl5 User Manual

Dumpleton
Software
Consulting
Pty Limited
OSE
Version 7.0pl5
Python Manual
19 January 2003
Copyright 2001-2003 Dumpleton Software Consulting Pty Limited
2

Table o f Contents

Table of Contents...........................3
Manual Overview ..........................7
Python Modules .............................9
Module Descr iptions.......................10
Installation and Setup......................1 0
Additional Information....................11
Logging Facility...........................13
Logging a Message .........................13
Specifying a Log File......................14
Specifying a Log Channel...............16
Logging Python Exceptions............16
Exceptions in a Callback .................17
Program Setup..............................19
Configuration Database...................19
Configuration File...........................2 0
Naming Hierarchies ........................2 1
Environment Variables....................21
Unique Identifiers............................22
Process Identity...............................23
Event Framework.........................25
Scheduling a Job .............................25
Real Time Events............................28
Destroying Agents...........................29
Alarms and Timers..........................29
Recurring Actions ...........................30
Socket Events..................................31
Program Signals..............................32
Program Shutdown ..........................33
Service Agents.............................37
Service Naming...............................38
Service Audience ............................39
Anonymous Service........................39
Service Groups................................40
Service Registry..............................40
Service Announcements..................43
Group Announcements....................44
3
Table of Contents
Service Lookup ...............................44
Service Reports.............................47
Publishing Reports ..........................4 8
Monitoring Reports.........................48
Lifetime of Reports .........................50
Identity of Subscribers .................... 5 2
Existence of Publishers ...................54
Service Requests...........................57
Sending a Request...........................57
Handling a Response....................... 59
Identifying a Response....................60
Detecting a Failure ..........................61
Lack of Response ............................63
Servicing a Request.........................64
Generating a Failure........................6 5
Delaying a Response.......................66
Handling Structured Types..............85
Servlet Framework.......................87
Framework Overview......................87
The HTTP Daemon......................... 88
The File Server................................89
Client Authorisation........................90
User A uthorisation..........................91
HTTP Server Objects ......................91
The Error Servlet............................. 92
The Redirect S ervlet........................93
The Echo Servlet.............................93
The File Servlet............................... 93
Logging of Requests........................94
Servlet Objects .............................95
Processing a Request.......................95
Persistent Connections....................96
Identity of the Sender. .....................68
Invalid Request Method ..................6 9
Local S ervice Requests ...................6 9
Message Exchange .......................71
Exchange Initialisation....................72
Service Availability.........................72
Connection Announcements ...........74
Authorisation of Clients..................75
Distributed Exchange Server...........75
Multiple Exchange Groups..............76
Scalability of the Framework..........7 8
Message Encoding........................79
Supported Data Types..................... 8 0
Mapping of Scalar Types ................8 1
User Defined Types.........................83
Adding New Mappings ...................8 3
Delaying a Response.......................97
Destruction of Servlets....................98
Processing Content.......................... 99
The Form Servlet...........................100
Slow HTTP Clients.......................101
Servlet Plugins............................105
Python Plugin................................105
Module Caching............................107
Writing a Plugin............................108
Plugin Aliasing..............................108
Remote Access ...........................111
The RPC Gateway.........................112
The Client Application..................113
Restricting Client Access ..............113
Duplicate Services.........................114
User Defined Types.......................115
4
Managing User Sessions...............116
The SOAP Gateway......................1 21
The XML-RPC Gateway...............118
Using Multiple Gateways..............123
5
Table of Contents
6

Manual Overview

This manual covers the Python wrappers around the OSE C++ class library. The wrappers make avail­able functionality related to the logging system, the real time events system, the service agent frame­work for creating distributed applications, the HTTP servlet framework and the RPC over HTTP interfaces.
Python Modules Lists the available Python modules and their pur-
pose. Includes brief details regarding installation and setup of the users environment.
Logging Facility Describes the message logging facility,including
how to direct messages to a specific log channel, how to log messages to a file or to process them within the actual application.
Program Setup Describes the interface to the configuration data-
base, user environment and other process infor­mation.
Event Framework Describes the interface to the event system, how
to schedule jobs and setup callbacks in response to real time events such as timers, signals and socket activity.
7
Manual Overview
Service Agents Describes how to create service agents, add them
Service Reports Describes how to subscribe to reports published
Service Requests Describes how to send requests to remote or
Message Exchange Describes how to connect up processes to form a
to groups, subscribe to announcements regarding specific services or membership of specific serv­ice groups.
by specific services. Ie., describes the publish/ subscribe functionality provided by the service agent framework.
local service agents and how to handle any response or error which results. Ie., describes the messaging or request/reply functionality of the service agent framework.
distributed application, including a decentralised message exchange and exchange groups.
Message Encoding Describes the Python types which can be used in
messages and how this can be extended to incor­porate new scalar data types.
Servlet Framework Describes the HTTP daemon and servlet frame-
work, including the predefined servlets and how to create customised HTTP server objects.
Servlet Objects Describes how to create new servlet objects
including how to handle forms, client congestion and delayed responses.
Servlet Plugins Describes how to create servlet plugins and how
to use the Python plugin to dynamically load servlets at runtime from the file system.
Remote Access Describes the RPCover HTTP interfaces into the
service agent framework, including support for NET-RPC, XML-RPC and SOAP protocols.
8

Python Modules

OSE includes a number of Python modules. The main module is a wrapper around functionality pro­vided in the OSE C++ class library. Those parts of the OSE C++ class library for which a Python wrap­per are provided are the logging system, the real time events system, the service agent framework for creating distributed applications and the HTTP servlet framework.
Additional modules provide access to the OSE service agent frameworkusing an RPC over HTTP pro­tocol called NET-RPC as well as the XML-RPC and SOAP protocol. Note that the XML-RPC and SOAP protocols come with restrictions deriving from problems in the respective protocols and the NET-RPC protocol provides the best integration.
Because interfaces are provided for the OSE service agent framework in both C++ and Python, an ap­plication may be spreadacross multiple processes and consist of processes written using either C++ or Python code. Using shared libraries and dynamic loading, C or C++ code could also be loaded into Python to perform some functions if desired.
Overall, the Python wrappers provide an interface to the functionality of the OSE C++ class library which is easierto use than if the C ++ classlibrary were useddirectly. This makes the Python wrappers ideal for building up the overallstructure of a distributed system, with C++ code being used only when necessary.
9
Python Modules

Module Descriptions

The Python modules, their names and their purpose are described below.
Module Purpose
netsvc This is the main module and provides wrappers around the
functionality of the OSE C++ class library. It includes all that is required for building distributed applications using the service agent framework.
netrpc This module provides a client implementation of the RPC
over HTTP protocol implemented by OSE called NET-RPC.
netsvc.xmlrpc This module includes the XML-RPC gateway for OSE. Any
server code is the same as for when the NET-RPC protocol is used. The only difference is which gateway you instantiate.
netrpc.xmlrpc This module provides a client implementation of the XML-
RPC protocol. The module is interface compatible with the"netrpc" module.
netsvc.soap This module includes the SOAP gateway for OSE. Any
server code is the same as for when the NET-RPC protocol is used. The only difference is which gateway you instantiate.
netrpc.soap This module provides a client implementation of the SOAP
protocol. The module is interface compatible with the"netrpc" module.

Installation and Setup

The "netsvc" module requires the main OSE C++ class library to be installed, as well as the Python extension library. The version of "makeit" installed when OSE is installed needs to be run in the "python" subdirectory of the OSE source code. This final step will install the two Python modules, a dynamically loadable module which drags in the OSE C++ class libraries and a GUI based debugger for the service agent framework called "spyon". The exact steps which need to be followed are given in the "INSTALL" file in the OSE source code.
When the Python modules are installed, they are not installed into your Python installation, but into the same area that OSE is installed. In order that Python can find the modules, you will need to set your PYTHONPATH environment variable to include the appropriate library directory in the OSE installa­tion. For OSE 7.0, if installed into its standard location, the directory will be:
/usr/local/ose/7.0/lib/python
10

Additional Information

An OSE installation supports libraries for different architectures. In order that the shared libraries for your specific platform can be found by the Python module, you should ensurethat the OSE_HOST var- iable is set to the same value it was set to when OSE was installed. For example:
OSE_HOST=X86_LINUX
If you want to be able to run the "spyon" debugger, your PATH environment variable should include the OSE bin directory. For OSE 7.0, if installed into its standard location, the directory will be:
/usr/local/ose/7.0/bin
If you want to be able to build up a version of the Python wrappers with a DLL for Win32, you have two choices. The first requires you to have access to either the Cygnus Win32 toolkit or MKS toolkit, and the Microsoft C++ compiler. In this case the normal build procedure for OSE is followed. If you only have access to the Microsoft C++ compiler, a native makefile is provided with the source code in the "win32" directory. You should follow the instructions contained in that directory.
Note that if you wish to use either the SOAP client or SOAP gateway, you will need to separately ob­tain and install the "ZSI" package from the "pywebsvcs" project on SourceForge. The project site ad­dress is "http://sourceforge.net/projects/pywebsvcs". You must have version 1.2 RC2 or later of the ZSI package.
Additional Information
As the main Python moduleis a wrapperaround functionality provided in the OSE C++ class libraries, it may be worthwhile to also consult the manual pages for the corresponding classes in the C++ class library and the general C++ class library manual. The behaviour of some features is controlled using environment variables and not all of these may be mentioned in the manual for the Python modules.
11
Python Modules
12

Logging Facility

The logging facility provides you with a mechanism for generating and capturing messages generated by your application. These can be automatically saved to a log file, or intercepted and dealt with in some other way. The majority of functionality for this feature is provided by the OTC_Logger class in the OSE C++ class library.
Some of the features of the logging facility are optional and controlled via environment variables.You should consult the manual page for the OTC_Logger class and the general OSE C++ class library manual as a number of these features will not be described here or covered only briefly.

Logging a Message

The logging facility provides you with the ability to log a message string with a specified priority or level assigned to it. The level is analogous to that used by the UNIX function called "syslog()".
Level Usage
LOG_EMERGENCY A panic condition. LOG_ALERT A condition that should be corrected immediately,
such as a corrupted system database.
LOG_CRITICAL Critical conditions, such as hard device errors. LOG_ERROR Errors. LOG_WARNING Warning messages.
13
Logging Facility
Level Usage
LOG_NOTICE Conditions that are not error conditions, but that
may require special handling.
LOG_INFO Informational messages. LOG_DEBUG Message that contain information normally of use
only when debugging a program.
To log a message,a handle to an instanceof the Logger class is acquired and the "notify()"mem­ber function is called.
import netsvc logger = netsvc.Logger() logger.notify(netsvc.LOG_DEBUG,"message")
The format of a message when displayed will be:
DEBUG: message
The string before the ":" corresponds to the level assigned to the message. The remainder of the line after the ":" is the actual message. If you wish to have the time and process ID appear in the prefix, call the "enableLongFormat()" member function. Whether the longer form of prefix is enabled can be queried using the "longFormatEnabled()" member function. It can be disabled using the "disableLongFormat()" member function.
By default,messages will appear on the standard erroroutput. If you wish to disable the display of mes­sages onto the standard error output, call the "disableStderrOutput()" member function. Con­versely, the "enableStderrOutput()" member function can be called to enable display of messages onto the standard error output if previously disabled. Whether messages are currently being displayed onto the standard error output can be queried by calling the member function "stder- rOutputEnabled()".

Specifying a Log File

At any time, messages can be captured into a single file by specifying the name of a log file using the member function "setLogFile()". If a log file is currently in use, the name of the log file can be queried using the "logFile()" member function.
logger.setLogFile("/var/tmp/application.log")
14
Specifying a Log File
The string used to specify the name of a log file may incorporate the following special tags.
Tag Purpose
%h Willencode the hostname of the machine into the name of the log file. %p Will encode the process ID into the name of the log file. %Y Will encode the current year as 4 digits into the name of the log file. %y Will encode the current year as 2 digits into the name of the log file. %m Will encode the current month of the year as a zero padded 2 digit
number into the name of the log file.
%d Willencode the current day of month as a zero padded 2 digit number
into the name of the log file.
When the tags corresponding to dates are used, a new log file will automatically be created when the value corresponding to a date component changes. The following will for example result in a new log file being created each day.
logger.setLogFile("/var/tmp/application-%Y-%m-%d.log")
Note that older log files will not be removed automatically, so some other mechanism such as a cron job will need to be employed to remove them.
The name of a log file can also be set using the OTCLIB_LOGFILE environment variable instead of calling "setLogFile()". Similarly, output to the standard error output can be disabled using the OTCLIB_NOLOGSTDERR environment variable and the inclusion of the time and the process ID in the message prefix enabled using the OTCLIB_LOGLONGFORMAT environment variable. If used, these environment variables must be set before the application is run or at least before the "netsvc" module is imported for the first time.
import os os.putenv("OTCLIB_LOGFILE","/var/tmp/application-%Y-%m-%d.log") import netsvc
When an application first attempts to open a log file, if i t already exists it will be truncated. If you do not want the log file truncated, but want messages to be appended to an existing log file, the OTCLIB_APPENDLOGFILE environment variablemust be set.Again, this needs tobe set prior to the application being run or at least before the "netsvc" module is imported for the first time.
Note that if any of these environment variables are used, but calls are subsequently made to the corre­sponding member functions of the Logger class from within the application, the values of the envi­ronment variables will effectively be overridden from that point onwards.
15
Logging Facility

Specifying a Log Channel

When logging a message, a log channel may also be specified. If the name of a log channel starts with a character other than an alphanumeric character, the message will not be displayed on the standard error output or appear in the log file. If it is displayed or captured in the log file, the name of the log channel does not appear anywhere in the message. The intent of the log channel is to allow one part of an application to capture specific messages produced by another part of the application and deal with them in a special way.
To log a message against a specific log channel, the member function "notifyChannel()"isused. The name of the log channel is supplied as the first argument.
logger.notifyChannel("VISIBLE",netsvc.LOG_DEBUG,"message") logger.notifyChannel("#HIDDEN",netsvc.LOG_DEBUG,"message")
Messages logged against a specific log channel, can be captured by calling the member function "monitorChannel()", supplying the name of the log channel and a callback function.
def callback(channel,level,message):
print (channel,level,message)
logger.monitorChannel("#HIDDEN",callback)
The message supplied to the callback function is the original message and does not contain the prefix describing the priority or level assigned to the message, nor does it contain any details relating to the current time or process ID. If you are going to subsequently log the message to a file, you would need to add these details yourself if you require them.
Only one callback can be associated with a particular log channel. If multiple callbacks are required for a particular log channel, separate instances of the Logger class should be used. To stop monitor­ing a specific log channel, the member function "monitorChannel()" is called again but with "None" supplied in place of the callback function.
If the callback function was a member of a class, it is important to deregister the callback, else a refer­ence to the instance of the class will be maintained and it may not get deleted. You can also deregister all of the callbacks associated with a particular instance of the Logger class by calling the member function "destroyReferences()". This would be necessary if the class containing the callbacks also held a reference to the instance of the Logger class. In this case, a circular reference would exist and neither object would ever be destroyed.

Logging Python Exceptions

To make the task of logging details of a Python exception easier, the "logException()" function is provided by the "netsvc" module. This function should only be called from within the context of a Python "except" clause. The information logged is similar to that displayed by Python when an exception is not caught and includes details of the exception and a stack trace.
16

Exceptions in a Callback

try:
function()
except SystemExit:
raise
except:
netsvc.logException() sys.exit()
The details of the exception are logged with level "LOG_ERROR" and a specific log channel is not specified. If you wanted to log the details of the exception to a specific log channel, or vary the level, you can use the "exceptionDetails()" function of the "netsvc" module to obtain the same in­formationthatwould be loggedby the "logException()" function and then call the "notify()" member function of an instance of the Logger class yourself.
try:
function()
except SystemExit:
raise
except:
details = netsvc.exceptionDetails() logger.notifyChannel("WARNING",netsvc.LOG_WARNING,details) pass
If you don’t want the stack trace and only want the description of the exception, use the function "ex­ceptionDescription()" instead. The result of calling either of these functions need not be used
with the logger, but could be displayed using any other available mechanism as well. Note that the "exceptionDetails()" and "exceptionDescription()" functions are also
available in the "netrpc" module if you are using that in a standalone client application.
Exceptions in a Callback
Whenever a callback is executed, it occurs as a result of a call from C++ code into Python code. Be­cause of the mix of C++ code and Python code, if an exception occurs within the callback function, Python can’t by itself properly shutdown the application. This is further complicated by the fact that a callback can be called within the context of a callback from the event dispatcher.
As a consequence, whenany callback into Python code fromC++ occurs, if a Python exceptionoccurs andthe callback itself doesn’t catch it and deal with it, it will be caught with the details of the exception being logged. The event dispatcher will then be stopped if it is running and the "SystemExit"ex­ception raised in order to prevent Python from running any further code. The outcome is the same as when only Python code is being used, except that the details of the exception are displayed using the logging facility rather than being dumped directly onto the standard error output.
17
Logging Facility
18

Program Setup

As Python is an interpreted language, configuration of an application can be carried out by editing the actual scripts. In some circumstances however, it is still easier or more practical to rely upon a config­uration database or environment variables. When using OSE this is especially the case, as an applica­tion can be a mix of C++ and Python code and configuration data may need to be accessible from code written in both languages.
To support this the Python wrappersprovide an interface tothe configuration database of the OSE C++ class library. The corresponding class in the OSE C++ class library which provides this functionality is the OTC_Program class. Not all functionality of this class is mirrored in the Python interface a s Python has its own way of doing most of what is provided by this class. Access is however provided to aspects of the configuration database and environment variable database. The functionality for gen­erating unique identifiers is also exposed.

Configuration Database

The configuration database is an in memory database. The database may be populated by calls from within the application, or by loading in a configuration file. The configuration database may also be saved to a file. In essence, the configuration database is not much more than a dictionary mapping names to values.
To initially load the configuration database from a file, the "loadConfig()" function is used. A sin­gle configuration item may be explicitly merged into the configuration database using the "mergeConfig()" function. A query can subsequently be made against the configuration database using the "lookupConfig()" function. If no match is found in the configuration database for the item in question, the value None is returned.
19
Program Setup
import netsvc import os
netsvc.loadConfig("database.cfg") netsvc.mergeConfig("PWD",os.getcwd())
print netsvc.lookupConfig("PWD")
A single configuration item can be removed from the database using the "removeConfig()" func­tion. The configuration database can be completely emptied using the function "removeAllCon- fig()". The contents of the configuration database can be saved to a file using the "saveConfig()" function.
netsvc.removeConfig("PWD") netsvc.saveConfig("database.cfg")

Configuration File

The only real restrictions in regard to naming is that the colon character should not be used anywhere in a name, a name should not being with an exclamation mark and whitespace should not be used at the start or end of a name. The colon character cannot be used as it used in a configuration file to sep­arate the name from the value. A leading exclamation mark should not be used as it is used to denote a comment.
If these characters are used in a name and the configuration database is saved to a file, the results when that configuration file is read back in will not be the same. The only other special character when used in a configuration file is a back slash, which when used at the end of the line, indicates the following line is part of the same value. Note that the leading whitespace and the whitespace either side of the colon will be ignored when the configuration file is read in.
! comment single-line-value : value multi-line-value : value\
value
When reading in a configuration file using "loadConfig()", an exception is raised only if the file doesn’t exist or the file couldn’t be opened. If there are no errors in the file, the value None is returned. If there are errors in the file, a string is returned which contains details of the errors and what action has been taken. By default, the details of the errors are also output via the logging systemon the default log channel.
If details of any errors should be output on a specific log channel, an optional second argumentcan be supplied to the "loadConfig()" function giving the name of the log channel. If the value None is supplied in place of the name of the log channel, the details of the errors will not be output via the log­ger at all. The value None could be used if you wish to amend the details of the errors before they are logged.
20

Naming H ierarchies

file = "database.cfg" errors = netsvc.loadConfig(file,None) if errors:
errors = "Error reading %s\n%s" % (‘file‘,errors) netsvc.Logger().notify(netsvc.LOG_DEBUG,errors)
Naming Hierarchies
Ifa naming hierarchy is required,the components of thehierarchy within the name should be separated by using a period.
compiler.preprocessor.debug-level : 0 compiler.parser.debug-level : 1 compiler.code-generator.debug-level : 0 compiler.assembler.debug-level : 0
In general, the purpose of using a naming hie rarchy is to associate properties with the same name with different parts of an application, or with different instances of some object. Tocater for default values, rather than enumerating all possible objects, a wildcard can be specified in place of a single component in a naming hierarchy. This says to match any component name in this position. Only those items which need to be different then need to be explicitly specified.
compiler.*.debug-level : 0 compiler.parser.debug-level : 1
When a lookup is made against the database, a check is first made for any entry which matches exactly the name of interest. If this name is not present, a search is then made of the entries containing a wild­card. If a match is found, the value associated with the wildcard entry will be returned. If there are multiple wildcard entries which match a lookup against the configuration database, that which has the longest leading exact match will be used.

Environment Variables

In addition to the configuration database, an interface is also provided to the standard operating system environment variables.Python does already provide aninterfacefor this, however the Python interface does have a few quirks which can sometimes make it less than useful.
One problem with the standard Python interface is that when "os.putenv()" is used to set an envi­ronment variable, that variable is not then visible using "os.getenv()". This is because "os.getenv()"uses"os.environ", which is a copy of the environment which is populated at startup and any changes to environment variables are not reflected in that copy.
As such, although changes to the environment will be seen by subprocesses, they will not be visiblein the same process. This means that a n environment variable can’t at the same time be used to transfer information to a different part of the application.
21
Program Setup
To lookup the value of an environmentvariable the function "lookupEnviron()"isused.Ifanew environment variable needs to be set, or an existing value changed, the function "mergeEnvi- ron()" is used. Anychanges to the environment variables will be visibleimmediately, but there is no way to get a list of all environment variables which are set. When a lookup is made but no such envi­ronment variable exists, the value None is returned.
netsvc.mergeEnviron("PWD",os.getcwd()) print netsvc.lookupEnviron("PWD")
In addition to these functions, the function "expandEnviron()" is provided. This function accepts a string and replaces any reference to an environment variable specified using Bourne shell syntax, with that environment variables actual value. The intent in providing this functionis that it can be used in conjunction with the configuration database, allowing configuration items to refer to environment variables.
application.log-files : ${HOME}/logs
Note that the expansion isn’tautomatic when a lookup is made against the configuration database.The application code will have to explicitly expand the value obtained form the configuration database.
value = netsvc.lookupConfig("application.log-files") directory = netsvc.expandEnviron(value)

Unique Identifiers

In many applications, it is often useful to be able to create abstract identifiers to uniquely identify ob­jects or resources. These might be used to identify user sessions in a web based application, specific requests in a distributed messaging system, or even the particular service agent which a request in a distributed messaging system is targeted at.
Such identifiers may only need to be unique within the context of the lifetime of the application, or possibly may need to be globally unique. In the case of the latter, to be rigourous this would normally require an external database to be maintained which tracks what identifiers have been used. In most cases however, it is not necessary to go to that extent and a simplistic means can be used to generate a psuedo unique identifier which is sufficient.
To generate such identifiersthe function "uniqueId()" is provided. The function can provide iden­tifiers in either a short or long format. In the short format, the identifier contains components which identify the host on which the process is running, the process id andan incremental counter. In the long format, time values are also included which tie the identifier to an instant in time.
id1 = netsvc.uniqueId(netsvc.ID_SHORT_FORMAT) id2 = netsvc.uniqueId(netsvc.ID_LONG_FORMAT)
If you wish to incorporate your own prefix into the identifier, an optional second argument can be sup­plied to the "uniqueId()" function.
22

Process Identity

id1 = netsvc.uniqueId(netsvc.ID_SHORT_FORMAT,"$SID?")
The short format identifier is suitable for use within the context of a single process. Duplicates would only be encounterd if the incremental count of the number of identifiers exceeded what can be stored within a 32 bit integer value. If this were to occur, the counter would wrap around to zero and conflicts might thus arise if the existing identifier were still active.
The short format identifier could also be used within the context of a constrained distributed applica­tionprovided that the nature of the application is such that knowlegeof what the identifier is associated with is always discarded when the process the identifier is bound to is destroyed. This would be nec­essary, as the identifier could be reused if the process id was reused at some latter point.
If a better gaurantee of uniqueness over time is required, the long format identifier should be used. In this case, the identifier also records the time at which the first identifier was generated by the process, as well as a time delta as to when that particular identifier was generated. Incorporation of time infor­mation avoids problems with the incremental counter overflowing and reuse of the same process id at a latter point in time.
Process Identity
A further feature which is useful in distributed applications is a way of identifying specific processes. Such an identifier can be generated by combining the name of the host and the process id into a single string. To facilitate this, the function "processIdentity()" is provided.
identity = netsvc.processIdentity()
23
Program Setup
24

Event Framework

The main support for concurrency in the OSE C++ class libraries comes in the form of a mechanism for building event driven systems. This is based around a central job queue and a dispatcher, which takes successive jobs fromthe queue and executes them. To support real time systems, there also exist a number of event sources which will schedule jobs to trigger an agent to be notified when an event of interest occurs.The major event sources include timers, signals and the availability of data for reading on a socket.
The major classes in the OSE C++ class library involved in providing this functionality are the OTC_Dispatcher, OTC_EVAgent and OTC_Job classes, plus thevarious event classes relatedto the event sources. In the C++ implementation, communication of events is mainly performed by pass­ing around event objects and having a single event handler method in an agent to deal with them. In the Python implementation, separate callback functions can be registered by an agent against each event of interest.
Note that only the major features of the C++ implementation are reflected in the Python interface. Py­thon does not provide a means of creating your own event types or event sources. A Python agent is also not able to process any events except those from the major event sources.

Scheduling a Job

Scheduling of jobs comes in the form of registering a callback function with the dispatcher for execu­tion. A job may be scheduled as a priorityjob, a standard job, or an idle job. Thetype of job determines where in the order of existing jobs, a new job will be placed. Any priority jobs are executed before a standard job is processed. When there are no priority jobs or standard jobs remaining, any pending idle
25
Event Fr amework
jobs will be reclassifiedas standard jobs and subsequently executed. When scheduling a job, if jobs of the same type already exist, the new job will be placed at the end of the list of jobs of the same type.
To schedulea job the dispatcher member function "schedule()" must be called,supplying the call­back function and the type of job. To set the dispatcher running, the member function "run()"is called. If the only feature of the event system which is used is that of scheduling jobs, the "run()" function will return when there are no more jobs to execute. A job may prematurely stop the dispatcher by calling the "stop()" member function. If a callback raises an exception which is not caught and processed within the callback itself, the details of the exception will be logged, the dispatcher stopped and Python exited immediately.
def callback(message="hi"):
print message
dispatcher = netsvc.Dispatcher() dispatcher.schedule(callback,netsvc.IDLE_JOB) dispatcher.schedule(callback,netsvc.STANDARD_JOB) dispatcher.schedule(callback,netsvc.PRIORITY_JOB) dispatcher.run()
The callback supplied when schedulinga job can be a normal function or amember function associated with an instance of a class. If a callback function is scheduled directly with the dispatcher in this way, it will be called with no arguments and cannot be cancelled once scheduled.
If it is necessary to pass arguments to a callback function, an instance of the Job class must be used in place of the actual callback function. The Job class will hold a reference to the real callback func­tion as well as the arguments. When the job is executed it will call the callback function with the sup­plied arguments.
job = netsvc.Job(callback,("bye",)) dispatcher.schedule(job,netsvc.IDLE_JOB)
In addition to providing a means of supplying arguments to a callback function, the Job class provides a means of cancelling execution of a callback function. In order to do this, a reference to the instance of the Job class should be kept. Ifit is subsequently necessary to cancel execution of the callback prior to it having being called, the "cancel()" member function of the Job class should be called.
job = None def callback1():
print "hi" job.cancel()
def callback2():
print "hi"
dispatcher.schedule(callback1,netsvc.PRIORITY_JOB) job = netsvc.Job(callback2) dispatcher.schedule(job,netsvc.STANDARD_JOB)
26
Scheduling a Job
All that is occuring here is that when the "cancel()" member function is called, a flag is set. When the job is executed it will note that the flag is set and will not execute the callback function. If the call­backfunction is amember function of a class, itis importantto ensure that any reference tothe instance of the Job class is destroyed when nolonger required. If this is not done and the reference is a member variable of the same class the callback function is a member of, a circular reference will exist and that instance of the class will not be able to be destroyed.
Any arguments to be passed to the callback function would by default be supplied when the instance of the Job class is created. If it is necessary to generate an instance of the Job class such that it can be passed to another part of the program, but the arguments to the callback function are not known at that time, it is instead possible to supply the arguments at the time the job is scheduled. This is done byusing the "schedule()" member function of the Job class rather than that of the dispatcher. Any arguments supplied in this way will override those provided when the instance of the Job class is cre­ated.
job = None def callback1(message):
print message job.schedule(netsvc.STANDARD_JOB,("override",))
def callback2(message)
print message
job = netsvc.Job(callback1,("default",)) job.schedule(netsvc.STANDARD_JOB) job = netsvc.Job(callback2)
This would allow for instance a class which accepts callback registrations to return a reference to a Job class which will later be used to schedule the callback with an as yet undetermined set of argu­ments. The client who registered the callback could however cancel execution of the callback before it is called.
Once "cancel()" has been called on an instance of a Job class, whether or not it has already been scheduled, the callback function will never be executed. To reset the flag which makes the callback function runnable, the "reset()" member function should be called. To determine if the an instance of the Job class is still in a runnable state, a truth test can be performed on it.
if job:
# job wasn’t cancelled job.schedule(netsvc.STANDARD_JOB)
else:
# job was cancelled pass
If you wish to use the Job class separate to the dispatcher, you can trigger execution of the callback function by calling the "execute()" member function. If any arguments are supplied to the "exe-
27
Event Fr amework
cute()" member function, these willoverride any which may have been supplied when that instance of the Job class was created.

Real Time Events

The Python interface provides the ability to register interest in a number of real time events. These are program shutdown, one off alarms or actions, recurring actions, timers, signals and data activity on sockets. That an event of interest has occurred is notified by execution of a callback supplied at the time that interest in an event is registered.
In the C++ implementation, the methods for expressinginterest in a specific type of event were spread across numerous classes. In the Python interface, all functions for registration of interest in events are contained within the Agent base class. Any object interested in receiving notification of an event oc­curring is expected to derive from the Agent class.
The simplest type of notification isn’t really a real time event at all, but a variation on the concept of scheduling a job with the dispatcher. Instead of calling the "schedule()" member function of the dispatcher, the "scheduleAction()" member function of the Agent base class is called.
The major difference between using "scheduleAction()" and "schedule()" is that when us­ing "scheduleAction()" you can optionally supply an additional string argument to be used as an identifier for that job. This identifier can be used to cancel the job before it actually gets executed by calling "cancelAction()". If thecallback funcion accepts a single argument, the identifierwill also be passed to the callback function as argument. The identifier can thus be used to distinguish be­tween different jobs calling the same callback function. If an identifier is not explicitly provided, a unique internal identifier will be created. Whether or not the identifier is set explicitly or created inter­nally, the identifier used is returned as the result of the "scheduleAction()" method.
class Object(netsvc.Agent):
def __init__(self):
self.scheduleAction(self.callback1,netsvc.PRIORITY_JOB)
def callback1(self):
self.scheduleAction(self.callback2,netsvc.IDLE_JOB,"hi") self.scheduleAction(self.callback2,netsvc.IDLE_JOB,"bye")
def callback2(self,name):
print name if name == "hi":
self.cancelAction("bye")
dispatcher = netsvc.Dispatcher() object = Object() dispatcher.run()
When using the Agent class, you still need to run the dispatcher. You do not need to schedule any jobs directly with the dispactcher, but any initial agents need to be created prior to the dispatcher being run. Note that in scheduling a job with a particular identifier, any job already scheduled with that agent
28

Destroying Agents

using the same identifier will first be cancelled. If you want to cancel all jobs scheduled using the "scheduleAction()" member function you should call the "cancelAllActions()" member function.
Destroying Agents
Ensuring that any outstanding job is cancelled, or deregistering interest in any event source, is impor­tant if you are endeavouring to destroy an agent object. If registrations are not cancelled, a circular ref­erence will exist between data held by the instance of the Agent base class and the derived object. Suchcircular references defeat the Python reference countingmechanism, meaning that theobject may never be destroyed.
To combat this particular situation, the member function"destroyReferences()" is included in the Agent base class.This will cancel all outstandingjobs and cancel any interest inother eventsourc­es as well, destroying any circular references in the process. Provided there are no other references to the object elsewhere, Python should now be able to destroy it.
If you have circular references within your derived class, you may wish to extend this method in your own class so as to undo those circular references. Using the same member function name will make it less confusing to a user of your class as they will only have to call one function. If this is done, you should ensure however that the last thing the derived version of the method does is call the version of the method in the immediate base class.

Alarms and Timers

Alarms and timers are a means of having a callback function executed at some point of time in the fu­ture. The difference between an alarm and a timer is that an alarm is defined by an absolute value or point in time, where as a timer is defined by a relative offset in time. For an alarm this means supplying the clock time in seconds at which the callback should be executed. For a timer this means supplying the number of seconds from now at which point the callback should be executed.
class Object(netsvc.Agent):
def __init__(self):
offset = 60 now = time.time() then = now + offset self.setAlarm(self.callback1,then) self.startTimer(self.callback2,offset,"timeout-1") self.startTimer(self.callback2,offset+10,"timeout-2")
def callback1(self):
print "alarm"
def callback2(self,name):
print name if name == "timeout-1":
self.cancelTimer("timeout-2")
29
Event Fr amework
The member function for setting an alarm is "setAlarm()" and that for starting a timer is "start­Timer()".The first argument is the callback function, the second argument is the absolute or relative
time and the third argument is an optional identifier for that alarm or timer. Scheduling an alarm or timer with an identifier matching that of an alarm or timer which hasn’t yet expired will cause that un­expired alarm or timer to be cancelled.
Bothtypes of events are one off events, with the registrationbeingcancelled oncethe callback has been executed. The identifier may also be used to cancel an alarm or timer before it expires. To cancel an alarm use "cancelAlarm()" and to cancel a timer use "cancelTimer()". To cancel all pending alarms use "cancelAllAlarms()" and to cancel all pending timers use "cancelAllTim- ers()". If an identifier is not excplicitly provided, an internal identifier will be automatically created with it being returned as the result of the function being called to schedule the callback.

Recurring Actions

A recurring action is where a job is run at regular intervals. Precisely when the callback function asso­ciatedwith a jobis executed isdetermined by a s pecification of theform used by the UNIX cron utility. The specification consists of five fields each separated by white space. The fields specify:
• minute (0-59),
• hour (0-23),
• day of the month (1-31),
• month of the year (1-12),
• day of the week (0-6 with 0=Sunday). A field may be an asterisk "*", which always stands for "first-last". Ranges of numbers are al-
lowed. Ranges are two numbers separated with a hyphen. The specified range is inclusive. For exam­ple, 8-11 for an "hours" entry specifies execution at hours 8, 9, 10 and 11.
Lists are allowed. A list is a set of numbers (or ranges) separated by commas. For example, "1,2,5,9" and "0-4,8-12". Step values can be used in conjunction with ranges. Followinga range with "/number" specifies skips of the number’s value through the range. For example, "0-23/2" can be used in the hours field to specify the callback function be executed every other hour. Steps are also permitted after an asterisk, so if you want to say "every two hours", just use "*/2".
Names can also be used for the "month" and "day of week" fields. Use the first three letters of the par­ticular day or month (lower case, or first letter only uppercase).
The day that a callbackfunction is to be executed can be specified by two fields, day of month and day of week. If both fields arerestricted (ie.,aren’t "*"),the callback functionwill be executedwhen either field matches the current time. For example, "30 4 1,15 * 5" would cause the callback function to be executed at 4:30 am on the 1st and 15th of each month, plus every Friday.
To schedulethis type of job, the "scheduleAction()"function is used except that instead of spec­ifying the job type as the second argument, the specification string should be used.
30

Socket Events

class Object(netsvc.Agent):
def __init__(self):
self.scheduleAction(self.daily,"00***","daily") self.scheduleAction(self.weekly,"00**Sat","weekly") self.scheduleAction(self.monthly,"001**","monthly") self.scheduleAction(self.yearly,"0 0 1 Jan *","yearly") self.scheduleAction(self.holiday,"0 0 25 Dec *","christmas")
def daily(self):
print "daily"
def weekly(self):
print "weekly"
def monthly(self):
print "monthly"
def yearly(self):
print "yearly"
def holiday(self,name):
print name
As a recurring action by nature will always run at some point in the future, you have to explicitly call "cancelAction()" to stop it from running, even if it hasalready run at some point in time already. If you make an error in the specification string such that it is invalid, no indication will be given and the job will simply never be executed. The "cancelAllActions()" member function, as well as cancelling actions associated with a once off call of a callback function, will also cancel all recurring actions.
Socket Events
In an event driven system, it is important that any callback not unnecessarily block waiting for some­thing to happen. If a callback does block, it prevents any other part of the system from doing some­thing.The main reason which a callback may block is due to an attemptto readdata from a socket when there is no data waiting to be read. In an event driven system, an application should register interest in the availability of data on a socket and only attempt to read data from the socket when it is known that there is some available.
It isalso advantageous in a event driven system for sockets to be placed into non blocking mode. When a socket is in non blocking mode, if data is written to a socket andthe socket is full an error is returned indicating that the call would have blocked. The code can now register interest in the possibility of be­ing able to write data to a socket and subsequently be notified when such a call would be successful. In the mean time, other parts of the system can still do something.
To register interest in either of these events, the member function "subscribeSocket()" should be used. The first argument to the function should be the callback function, the second argument the socket descriptorand the third argument the type of events. If the third argument is notsupplied, it will default to SOCKET_POLLIN, indicating interest in the availability of data on a socket for reading.
31
Event Fr amework
Other possible values for the third argument are SOCKET_POLLOUT and SOCKET_POLLPRI.The value SOCKET_POLLPRI is similar toSOCKET_POLLIN except that it relates to therebeing priority out of band data being available forreading. Out of band datais not a featurewhich is used much these days and isn’t implemented the same on all systems. It is probably best to avoid using out of band data.
A final value of SOCKET_POLLOUT indicates interest in when data can be safely written to the socket without the call blocking. Note that this will generally nearly always be the case, so you should only subscribe to this event on a socket, when you know that writing to the socket would cause it to block. Once you have been notified that it is safe to write to a socket and you have written your data, you should immediately unsubscribe to this event on a socket, otherwise your callback will continually be called.
class Agent(netsvc.Agent):
def __init__(self,host,port):
netsvc.Agent.__init__(self) self._host = host self._port = port self.scheduleAction(self.connect,netsvc.STANDARD_JOB)
def connect(self):
self._sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) try:
self._sock.connect((host,port))
except:
dispatcher.stop()
else:
self.subscribeSocket(self.read,self._sock.fileno())
def read(self,fileno,event):
if fileno != self._sock.fileno():
return
if event == netsvc.SOCKET_POLLIN:
data = self._sock.recv(1024) if len(data) == 0:
self.unsubscribeSocket(self._sock.fileno()) self._sock.close() dispatcher.stop()
else:
sys.stdout.write(data)
When you are no longer interested in a particular event on a socket, you can unsubscribe to that event using the "unsubscribeSocket()" member function. If called with only a single argument, all events currentlyof interest on that socket will be unsubscribed. To unsubscribe to only a specific event type, pass the type of event as the second argument.

Program Signals

The most common circumstance in which an application may receive a program signal is when it is being killed as result of a user interrupting it by typing control-C, or if running UNIX, when the oper-
32

Program Shutdown

ating system is being shutdown. Other uses for program signals are to force an application to reread a configuration file.
These three cases are typically indicated by the programsignals SIGINT, SIGTERM and SIGHUP.A robust application should at least catch the first two of thesesignals and causethe program to shutdown gracefully. This may entail ensuring that any data is written out to files, removal of file locks, closing off of database connections etc.
To subscribe to a signal, the member function "subscribeSignal()" should be us ed. The first ar­gument should be a callback function to be called when a particular signal occurs and the second ar­gument the particular signal of interest. A particular agent may only supply one callback for any particular signal, but different agents may subscribe to the same signal with both being notified when it occurs. Although an interest in such a signal is usually persistent, it is possible to unsubscribe from a particular signal using the memberfunction "unsubscribeSignal()" and unsubscribe from all signals using "unsubscribeAllSignals()".
class Agent(netsvc.Agent):
def __init__(self):
netsvc.Agent.__init__(self) self.subscribeSignal(self.signal,signal.SIGINT) self.subscribeSignal(self.signal,signal.SIGTERM)
def signal(self,signum):
self.scheduleAction(self.stop,netsvc.PRIORITY_JOB)
def stop(self):
netsvc.Dispatcher().stop()
In practice,only one of the agents subscribed to SIGINT and SIGTERM should actually shutdownthe dispatcher. This agent should however, not shutdown the dispatcher immediately as other agents may not yet have received their notification that the signal occurred. The agent should instead schedule a priority job to actually stop the dispatcher. This priority job will only be executed after all outstanding signal notifications have been delivered.
Program Shutdown
Subscription to a program signal provides a means of immediately shutting down an application when caused to do so by an external signal. What program signals don’t do however, is provide a means of initiating agracefulshutdown of an application from within theapplicationitself. An application could send itself a signal, however, this isn’t necessarily practical.
A further problem is that in an event driven system, it may not always be possible to perform every­thing that is required in a single callback function. What is instead needed is the ability to run the ap­plication for a further finite amount of time so that any outstanding operations can be finalised first. At the end of that time, then the application can be stopped.
To support this slightly more orderly mechanism for program shutdown, the member function "scheduleShutdown()" is provided. When an agent wishes to force the program to shutdown it
33
Event Fr amework
should call this member function. This member function can also be called when an external signal intended to shutdown the program is received. Doing this in the latter case means you don’t need to have separate code for the two different cases.
If an agent is interested in the fact that the program is being shutdown, it can call the "subscribe- Shutdown()" member function, supplying a callback function to be called when such an event does occur. Note that the call to "scheduleShutdown()" will result in the dispatcher being stopped au­tomatically, so you do not need to do it explicitly.If necessary, an agent can unsubscribe fromprogram shutdown notifications by calling the member function "unsubscribeShutdown()".
class Agent(netsvc.Agent):
def __init__(self):
self.subscribeShutdown(self.shutdown) self.subscribeSignal(self.signal,signal.SIGINT) self.subscribeSignal(self.signal,signal.SIGTERM) self.startTimer(self.timeout,60)
def timeout(self):
self.scheduleShutdown()
def signal(self,signum):
self.scheduleShutdown()
def shutdown(self,category):
if category == netsvc.SHUTDOWN_PENDING:
# shutdown is pending
else:
# shutdown has arrived
When shutdown is initiated, any callback function supplied by an agent will actually be called twice. Thefirst timeit is called, it willbecalled withthe value "SHUTDOWN_PENDING". Onceall subscribed agents have been notified that shutdown is pending, the callback function will then be subsequently called again, this time with the value "SHUTDOWN_ARRIVED". Upon all agents receiving the second notification, the dispatcher will be stopped and the process will exit.
Notethat the second of these notifications will not occurimmediately after the first. Exactly how much time may pass is dependent on a number of factors. The first determining factor is the argument sup­plied to the "scheduleShutdown()" member function. If no argument is supplied, or a value of "0" is supplied, there will be an inbuilt delay of 1 second between shutdown being scheduled and the program actually being shutdown.
This implicit delay gives scope for activities which can’t be factored into a single callback function time to be carried out. For example, it may be necessary to send data via a socket to some remote host and wait for the response. If the default value of 1 second is insufficient, or is too long a time, it can be overridden in a number of ways.
The first way of overriding the default value of 1 second is by setting the environment variable OTCLIB_SHUTDOWNDELAY. If this is done, it should be set to a value representing the number of milliseconds to wait. An alternative is to modify each call of "scheduleShutdown()" and explic-
34
Program Shutdown
itly provide the time delay as an argument. If this is done, the argument should express the number of full or partial seconds as a floating point value.
Using a time delay is a useful starting point, as it provides a means of defining an upper bound on the amount of time you wish to allow the system to run before it is stopped. Having a small delay and en­suring everything is done in that time is preferable, as in certain circumstances such as the operating system sending a SIGTERM to an application on system shutdown, the operating system will usually forcibly shutdown your application using SIGKILL after 5 seconds if it doesn’t do so of its own ac­cord.
Although getting away from the goal of having only one mechanism for shutting down a program, in this circumstance, it may still be preferable to separately identify a SIGTERM signal and deal with it differently. Here you might only do anything that is absolutely essential and stop the process immedi­ately. What is the best approach will depend on the specific application in question.
If the problem of a SIGTERM signal is ignored, a further mechanism for delaying actual shutdown of a process is also provided. If upon receiving notification of a pending shutdown, an agent knows it needs towait for some event to occur first,it can call the "suspendShutdown()" member function. If this is done, although the shutdown delay may expire, actual program shutdown will not occur until a corresponding call to the "resumeShutdown()" member function. If more than one agent calls "suspendShutdown()", actual shutdown will not occur until "resumeShutdown()" has been called a matching number of times.
Although it is possible to suspend the shutdown process in this way, it is not possible to cancel it com­pletely. But then, if an agent doesn’t call "resumeShutdown()" at some point it would never actu­allyoccur. This wouldn’t be very useful however, as more than likely partsof the applicationmay have placed themselves into a dormant state.
Finally, as scheduling program shutdown upon a signal occurring would be done in practically all pro­grams, support for this has been factored into the actual dispatcher. Thus, instead of dedicating a spe­cific agent to catch any signals, the main program file can contain:
dispatcher = netsvc.Dispatcher() dispatcher.monitor(signal.SIGINT) dispatcher.monitor(signal.SIGTERM)
If this interface is used however, the only means of overriding the delay between shutdown being scheduled and actual shutdown is by the OTCLIB_SHUTDOWNDELAY environment variable.
The dispatcher also provides the member function "shutdown()". This behaves much the same as the "scheduleShutdown()" member function of the Agent class. The presence of the "shut- down()" member function in the dispatcher, allows code which is distinct from an agent to also schedule a program shutdown.
35
Event Fr amework
Note that whatever mechanism is used to initiate program shutdown using these features, messages will be displayed via the logger indicating that shutdown has been scheduled and that it has arrived. Additional messages will be displayed via the logger when the shutdown process is suspended and re­sumed.
36

Service Agents

The service agent framework in OSE provides request/reply and publish/subscribe features similar to that found in message oriented middleware packages. Unlike most of the available packages, the serv­iceagent framework does not have aflat namespace with respect to naming, but uses an object oriented model, with each service having its own namespace with respect to subject names for subscriptions and request method names.
Building on this object oriented approach, it is possible to subscribe to the existence of specific serv­ices, or to groups of services as well as aspects of the services themselves. By using subscription to groups, an application can be setup to dynamically handle the introduction and withdrawal of new services rather than being hardwired. Services are also able to monitor when subscriptions occur and identify who is making the subscription if necessary.
All the features of the service agent framework can be applied within the scope of a single process, or across a group of distributed processes. A specific service need not even be aware that a service it makes use of is in a remote process as the interface and means of interacting with that service are the same. Services may therefore be moved around between processes or onto different machines and the key parts of the application will not need to be changed.
As the Python interface is simply a wrapper on top of functionality provided by the OSE C++ class library, you are not restricted to writing service agents in just Python. In a distributed application for example, one process may be entirely written in C++, another may use only the Python wrappers, and a third a mix of both if dynamic loading into a Python program were used. This flexibility means you can use Python where simplicity is important, but C++ where better performance may be desirable.
37
Service Agents
The major classes in the OSE C++ class library involved in providing this functionality are the OTC_SVBroker,OTC_SVRegistry andOTC_EVAgent classesalong withvarious event classes. In a distributed application the OTC_Exchange class comes into play along with the various classes used to implement the interprocess communications mechanism.

Service Naming

When using the C++ class library, implementation of a service agent entails the use of a number of different classes together. In the Python interface this has all been brought together in the Service class. If you wish to create your own instance of a service agent, you need only derive a class from the Service class and then instantiate it.
The mostimportant aspect of creating a service agent is the need to assign it a name. This name is what is used by other services to access your particular instance of a service agent. Having selected a name, itshould be supplied to the Service base class at the point of initialisation. If you wished to call your service "alarm-monitor", the constructor of your class might look as follows.
class AlarmMonitor(netsvc.Service):
def __init__(self,name="alarm-monitor"):
netsvc.Service.__init__(self,name)
In general there is no restriction on whatyou can put in a servicename. It is suggested though that you avoid any form of whitespace or non printable characters so as to make debugging easier.
In assigning a name to a service agent, there is nothing to stop you from having more than one service with the same name. Often the ability to have more than one service with the same name is useful, but in other situations it may be regarded as an error. As a policy on how to handle more than one service with the same name willbe dependent on the actual application, implementation of any scheme to deal with it is left up to the user.
If you want to query what the service name is for an instance of a service agent, it can be queried using the "serviceName()" member function. If you need to know the unique identity of a serviceagent, it can be queried using the "agentIdentity()" member function. Even when two services share the same name, they will still have distinct agent identities. These as well as other details relating to a service agent can also be obtained from the object returned by the "serviceBinding()" member function.
Note that the Service class ultimately derives from the Agent class and as such all features of the event system are also accessible from a service agent. The Service class also builds on the same model used by the Agent class with respect to destruction of an object instance and the cleaning up of circular references. As such the Service class contains a derived implementation of the "de- stroyReferences()" member function found in the Agent class. Any derived service agent should use this function in the same way as defined for the Agent class.
38

Service Audience

Service Audience
When you create a service, the existance of that service will be broadcast to all connected processes. If you wish to restrict visibility of a service to just the process the service is contained in, or a subset of the connected processes, a service audience can be defined.
To define the service audience, an extra argument needs to be supplied to the Service base class when it is initialised. By default the service audience is "*" to indicate that knowledge of the service should be broadcast as widely as possible. Setting the service audience to an empty string, will restrict visibility of the service to the local process.
class AlarmMonitor(netsvc.Service):
def __init__(self,name="alarm-monitor",audience="*"):
netsvc.Service.__init__(self,name,audience)
Other values can be supplied for the service audience and their meaning will depend upon how the in­terprocess communications links of the service agent framework are configured. This aspect of the service audience field will be discussed when support for distributed applications is covered.
Note that in setting theservice audience, you are also restricting your service agent as far as what serv­ices it can subscribe to. If you set the service audience to that indicating the local process only, you will only be able to subscribe to services which exist in the local process. This is because services in remote processes will not know anything about you. If you need to be able to subscribe to services no matter wherethey are, you wouldgenerallybe best leaving the serviceaudience set to the default value.

Anonymous Service

Although referred to as a service, a service agent can act in the role of either a client or server. That is, as a client it is a user of other services and would not expect to have subscriptions made against it or receive requests. In this situation the name assignedto the serviceis immaterialand it is valid to supply an empty service name. In fact, if you do not explicitly supply a service name when initialising the
Service base class, it will default to an empty string.
class AnonymousService(netsvc.Service):
def __init__(self):
netsvc.Service.__init__(self)
In general it is still preferable to supply a non empty value for the service name. Doing so will mean that the service agent will appear as a separate entity within any debugging tools and although the ap­plication itself may not need to use that service agent in the role of a server, you might still include functionality which can be used from debugging tools so you know what the service agent is doing.
39
Service Agents

Service Groups

Whena service agent is created, the name of the serviceis notionally listedin a global group. In respect of this global group, unless you track the coming into existance of every single service agent, there is noway to make conclusions abouta subset of services. Even if you do track thecreation of everysingle service agent, the only way you might be able to distinguish a service agent as belonging to some group, is to introduce into the name of the service agent some form of artificial naming hierarchy.
Rather than rely on an artificial means of grouping service agents based on the service agent names, a separate conceptof service groups is implemented. To add a service agent toa specific group, the "jo- inGroup()" member function can be called at any point after the Service base class has been in­itialised. That is, adding a service agent to a service group does not specifically have to been done in the constructor but can be done at a later time. To remove a service agent from a service group it has joined, the "leaveGroup()" member function can be called.
class EquipmentAgent(netsvc.Service):
def __init__(self,name,audience="*"):
netsvc.Service.__init__(self,name,audience) self.joinGroup("equipment-agents")
As with service names, it is recommended that you avoid using any form of whitespace or unprintable characters in servicegroup names. The empty servicegroup should also not be usedto avoid confusion with the global group.

Service Registry

The service registry is where information about available services is recorded. Each process in a dis­tributed applicaton has its own service registry. The service registry in a process will list any services which are local to that process as well as any of which knowledge has been imported into the process from a remote process.
That each process has its own service registry means that the service agent framework can work quite happilywithinthe context of a single process, as well as within the context of a distributed application. That is, when you only have a single process it isn’t necessary for that process to be connected to a central server for the system to work. In this respect, eachservice registryacts as a peer to other service registries and not in a client/server mode.
A further consequence of this is that even when a process is part of a distributed application and the central message exchange process is terminated, any processes which were connected to it are not forced to restart themselves. In this scenario, any interested parties would be notified of the fact that remote services are no longer accessible and would take any action as appropiate. When the central message process is restarted, processes would automatically reconnect, with interested parties being notified that the remote services are once more accessible.
40
Service Registry
Any service agent may make queries againstits local service registry and get back an immediate result which reflects the current stateof the service registry. A service agent may alsosubscribe to the service registry or aspects of it and be notified in real time of changes made to the service registry. When sub­scribing to the service registry itself, a service agent would be notified of all available services, when those services join or leave groups and when those services are withdrawn.
Subscribing to the service registry as a whole is a useful debugging tool as it can produce an audit trail relating to the creation and deletion of services as well as group memberships. When used as a debug­ging tool as well as in other cases, it may not be appropriate that a service agent be created merely that the service registry can be queried. To this end, the member functions of the Service class relating to the service registry are also available through the Monitor class. In fact, the Service class de­rives from the Monitor class.
To setup a subscription against the service registry as a whole, the member function "subscrib- eRegistry()"is used. A subscriptionto the serviceregistry can laterbe removed using the member function "unsubscribeRegistry()".
class RegistryMonitor(netsvc.Monitor):
def __init__(self):
netsvc.Monitor.__init__(self) self.subscribeRegistry(self.announce)
def announce(self,binding,group,status):
if group == None:
# global group action = "WITHDRAWN" if status == netsvc.SERVICE_AVAILABLE:
action = "AVAILABLE" name = binding.serviceName() identity = binding.agentIdentity() print "SERVICE-%s: %s (%s)" % (action,`name`,identity)
else:
# specific group action = "LEAVE" if status == netsvc.SERVICE_AVAILABLE:
action = "JOIN" name = binding.serviceName() identity = binding.agentIdentity() print "%s-GROUP[%s]: %s (%s)" % \
(action,`group`,`name`,identity)
When making queries or subscriptions against the service registry, details of a specific service are re­turned in the form of a service binding object. This is the same type of object returned by the "serv- iceBinding()" member function of a specific service agent. Where an operation needs to refer to a particular service it will be usually done in terms of this service binding object rather than the infor­mation it carries.
41
Service Agents
Member functions of a service binding object which may prove useful include "serviceName()", "agentIdentity()", "serviceAudience()", "processAddress()" and "serviceLo- cation()". Of these, "serviceLocation()" returns either "SERVICE_LOCAL"or "SERVICE_REMOTE", giving an indication if the service is located in the same process or a remote process. The "processAddress()" member function will return an internal address relatingto the actual process the service is located in.
Althoughthe shorthand "agentIdentity()" member function provides a more readable value, the "serviceAddress()" member function is also provided and returns the internal address used to identify the service. Note though that if in a distributed application an intermediary process along the route to the actual service is restarted, when all processes reconnect, the service address will be differ­ent where as the process identity a nd agent identity would be the same. This reflects the fact that it is still the same service, but the route used to contact the service has now changed as the intemediary process was restarted.
When subscribing to the service registry as a whole, each notification will also include a group and status value. When the group is "None", the notification refers to either the availabilityor withdrawal of a service. For any other value of group, it indicates that a specific service is joining or leaving that group. Whether a service has become available or has been withdrawn, or similarly whether a service has joined or left a group is given by the status value. When the status is "SERVICE_AVAILABLE", a service has become available or has joined a group as appropriate. When the status value is "SERVICE_WITHDRAWN" the service has either been withdrawn or has left a group as appropriate.
Note that when the status indicates that a service has become available it doesn’t mean that the service only just got created. In the case that a service is in a remote process, it may be the case that a service has existed for some time, but because the local process has only just connected into a distributed ap­plication it has only just become aware of that fact.
Similarly, when a service is withdrawn, if the service was in a remote process it means the service can no longer be contacted.This may have occurred becausethe service itself has been destroyed, the proc­ess in which the service existed has been destroyed or that an intemediary process involved in the com­munication path for contacting that process has been destroyed and the remote process is currently no longer contactable.
By subscribing to the service registry it is possible to receive in real time notitifications regarding the availability of services as such events happen. If you only wish to find out which services are available at a particular instant in time, you can use the " serviceAgents()" member function. Note that de­pending on the number of service agents available,calling this member function repetitivelycan incur significant overhead. If possible this member function should be used sparingly and a subscription against the service registry used instead.
42

Service Announcements

Service Announcements
If a service agent subscribes to the registry using a specific service name, the service agent will be no­tified when any service with that name becomes available or is subsequently withdrawn. When sub­scribing to the registry using a specific service name, no notification is given regarding groups that those same services may join.
class ServiceMonitor(netsvc.Monitor):
def __init__(self,name):
netsvc.Monitor.__init__(self) self.subscribeServiceName(self.announce,name)
def announce(self,binding,status):
action = "WITHDRAWN" if status == netsvc.SERVICE_AVAILABLE:
action = "AVAILABLE"
name = binding.serviceName() identity = binding.agentIdentity() print "SERVICE-%s: %s (%s)" % (action,`name`,identity)
The name of the member function for subscribingto the existance of a service agent by name is "sub­scribeServiceName()". A subscription can be cancelled by calling the member function "un­subscribeServiceName()".
Having identified a particular service agent, it is often useful to know when that specific service agent is no longer available. The notifications provided when you call the member function "subscribe- ServiceName()" will tell you that, but if the service binding had been received through some other means and you weren’t receiving the notifications, it is preferable that you be able to receive a notifi­cation just in relation to the specific service agent you are using. In this case, the service address can be obtained from the service binding by calling "serviceAddress()" and the member function "subscribeServiceAddress()" used instead. This subscription can be cancelledby callingthe "unsubscribeServiceAddress()" member function.
class ClientService(netsvc.Service):
def __init__(self,binding):
netsvc.Service.__init__(self) address = binding.serviceAddress() self.subscribeServiceAddress(self.announce,address) self._binding = binding # start using service
def self.announce(self,binding,group,status):
if group == None:
if binding.agentIdentity() == self._binding.agentIdentity():
if status == netsvc.SERVICE_WITHDRAWN:
self.unsubscribeServiceAddress(binding.serviceAddress()) self._binding = None # stop using service
43
Service Agents

Group Announcements

If a service agent subscribes to the service registry using a specific service group, it will be notified when any service joins or leaves that group. Notice that a service has left a particular group will also be be notified when the service is withdrawn and the service hadn’t explicitly left the group before hand. The member functions relating to service group subscriptions are "subscribeService- Group()" and "unsubscribeServiceGroup()".
Subscription to a service group is most often used as a way of finding out what services exist which perform a certain function. As an example, service agents which provide an interface to equipment in a telecommunications network could join a particular group. A service which has the task of monitor­ing alarms generated by the same equipment could then subscribe to that service group and be notified about each equipment agent. Knowing about each equipment agent, the alarm monitor could then sub­scribe to any alarm reports generated by the equipment agents.
class EquipmentMonitor(netsvc.Service):
def __init__(self):
netsvc.Service.__init__(self,"equipment-monitor") self.subscribeServiceGroup(self.announce,"equipment-agents")
def announce(self,binding,group,status):
if status == netsvc.SERVICE_AVAILABLE:
self.monitorReports(self.alarm,binding,"alarm.*")
else:
self.ignoreReports(binding)
def alarm(self,service,subject,content):
print subject,content
By using a service group it is therefore possible to make an application respond dynamically to the in­troduction of new service agents. In the case of the equipment alarm monitor for a telecommunications network, it wouldnot be necessary to hardwire in details of the equipment. Instead, when adding a new piece of a equipment, the service agent providing an interface to that equipment need only add itself to the appropriate service group.
Sucha mechanismcould alsobe used tomonitor alarmsraised asa result of problems in the application itself and need not be alarms generated by some piece of equipment. This mechanism could therefore also be used as the basis of an application health monitoring system.

Service Lookup

The ability to subscribe to the service registry provides a means of tracking the existance of service agents over time. The alternative to subscribing to the service registry to find out about available serv­ices, is to do a lookup against the service registry. Performing a lookup will tell you immediately what service agents exist at that particular point in time. No subscription will be registered when doing a lookup though, so if you need to know when a service agent is subsequently withdrawn it still may be appropriate to subscribe to the registry using the address of the specific service agent you use.
44
Service Lookup
class PollingService(netsvc.Service):
def __init__(self,name,period=60):
netsvc.Service.__init__(self) self._name = name self._period = period self.initiateRequests("poll")
def initiateRequests(self,tag):
bindings = self.lookupServiceName(self._name) for binding in bindings:
service = self.serviceEndPoint(binding) # presume remote service provides uptime method id = service.uptime() self.processResponse(self.handleResult,id)
self.startTimer(self.initiateRequests,self._period,"poll")
def handleResult(self,result):
address = self.currentResponse().sender() binding = self.lookupServiceAddress(address) if binding != None:
print binding.agentIdentity(),result
Anumber of different types of lookup can be made againstthe service registry. The first two allow you to lookup all service agents which have a particular service name, or all service agents which are cur­rentlya member of a specific service group. Thetwo member functions corresponding to these lookups are "lookupServiceName()" and "lookupServiceGroup()". Both these lookup functions return a list of service binding objects corresponding to the service agents found. If there are no service agents matching the search criteria, an empty list is returned.
The third type of lookup is that of looking up a specific service agent using its service address. In this case you will need to have been able to obtain the service addressby some other means first.The mem­ber function here is "lookupServiceAddress()". The result will be the service binding object corresponding to that service agent, or None if the service agent is no longer available.
In order to obtain a list of all services known of by the service registry, the member function "serv- iceAgents()" can be used. This should however be used sparingly because of the overhead which might be incurred when there are large numbers of services. If possible, subscription against the serv­iceregistry should stillbe used if it is necessary to track allavailableservices. Overhead canbe reduced by using subscription and caching the results as Python data structures, with Python objects accessing the cache directly. This avoids the translation from C++ data structures to Python data structures.
Similarly to service agents, a list of all service groups can be obtained by calling the member function "serviceGroups()".If it is necessary to determine which service groups a particular service agent is a member of, an optional argument can be supplied to "serviceGroups()",that argument being the service address of the service agent of interest.
45
Service Agents
46

Service Reports

When using the service agent framework, in addition to being able to subscribe to the service registry in order to receive announcements regarding the existence of services, it is also possible to subscribe to actual services. When subscribed to a service, if that service publishes a report with a subject match­ing the subscription, the subscriber will automatically receive it.
Referredto as publish/subscribe, this is a common feature of packages implementingmessage oriented middleware services. Note that in this implementation, the design and interface are driven by simplic­ity. As a result, the implementation is not underlaid by persistent message queues. While a subscriber exists and is known of by the publisher, it will receive any reports for which it hasa valid subscription. If a subscriber is destroyed but is subsequently restarted, it will only receive reports published from that time onwards, it will not receive reports which may have been published in the time that it was offline.
The system design can thereforebe likened to a system implementing instant messaging as opposed to a mailing list. In instant messaging you will only see those messages which are posted into a group while you are online, whereas with a mailing list a ny messages posted while you were away will still be there for you to see when you log back in.
As a basic system, this model of operation is suitable for many applications, but not all. If you are de­veloping a system where it is imperative that you never miss a message, you would be advised to pur­chase a commercial message oriented middleware package. You will of course have to deal with the extra complexity and cost that entails.
47
Service Reports

Publishing Reports

If a service agent needs to publish a report, the member function "publishReport()"isused.In publishing a report, it will generally be the case that a service agent does it without caring who may actually be subscribed to that report. This is often referred to as anonymous publishing and results in a more loosely coupled system which can adjust dynamically to changes. That is, it is not necessary to hardwire into a service to whom it should send a report, instead, a service which is interested in the report will subscribe to it and the underlying system will handle everything else.
self.publishReport("subject.string","value") self.publishReport("subject.integer",12345) self.publishReport("subject.float",1.2345) self.publishReport("subject.list",[1,2,3,4,5]) self.publishReport("subject.dict",{"one":1,"two":2})
When publishing a report, a service agent needs to supply a subject which in some way identifies the purpose of the report, as well as the content of the report. It is through subscription to specificsubjects that subscribers will indicate their interest in specific reports. The subject name assigned to a report can have any value, but it is suggested that a hierachical naming convention be used. That is, use one or more name components, where each component is separated by a period.
heartbeat news.local.sanitation news.domestic.politics notifications.shutdown
By using a naming hierarchy, it becomes possible to aggregate reports into groupings which can then be easily subscribed to as a whole. Note that there is nothing special about a period as the separator for the name components. Other separators which are often used for performing the same task are a slash or a colon.

Monitoring Reports

A desire to subscribe to reports published by another serviceis indicated by a serviceagentcalling the "monitorReports()" member function. In setting up such a subscription, the s ervice agent must supply a callback function to be called when a report is received, the name of the service or the service binding object of the specific service agent to which it is subscribing and an indication of what reports it is interested in.
In the simplest case, a subscription can supply the exact same subject name under which a report is published. Alternatively, it can use special wildcard characters to allow it to pick up reports published against related subjects.The two special wildcardcharacterswhich can be used are"*" and "?". These can be incorporated anywhere in the subscription pattern.
The "?" can be used to match a single character within the subject name, where as a "*" will match any number of characters. Note that each will matchany character, including aperiodor slash.As such,
48
Monitoring Reports
a subscription of "system.*" will match "system.time" and "system.statistics.us- ers", but not "system". To subscribe to any reports from a particular publisher, "*" would be used.
Note that the subscription pattern described here is the default. It is actually possible within the C++ implementation of a service agent to override the default and supply an alternate matching algorithm. For example, in a bridge to the TIB/Rendevous package, a service agent would most likely redefine the matching algorithm to match that of that package. Therefore, when subscribing to a service agent, always check first exactly which scheme it uses.
class Publisher(netsvc.Service):
def __init__(self):
netsvc.Service.__init__("publisher") self.joinGroup("publishers") self.publishReport("system.ctime",netsvc.DateTime(),-1) self.startTimer(self.timeout,10,"heartbeat")
def timeout(self,tag):
self.publishReport("system.time",netsvc.DateTime()) self.startTimer(self.timeout,10,"heartbeat")
class Subscriber(netsvc.Service):
def __init__(self):
netsvc.Service.__init__(self) # subscribe to any service agent with name "publisher" self.monitorReports(self.report,"publisher","system.*")
def report(self,service,subject,content):
binding = self.currentReport().publisher() identity = binding.agentIdentity() publisher = "(%s/%s)" % (‘service‘,identity) if subject == "system.ctime":
now = str(netsvc.DateTime()) print "%s became available at %s" % (publisher,now) print "%s originally started at %s" % (publisher,str(content))
elif subject == "system.time":
print "%s was still alive at %s" % (publisher,str(content))
When called, the callback supplied by the subscriber will be passed three arguments. These are the service binding object for the service agent which published the report, the subject under which the report was published and the content of the report.
The service binding object for the service agent which published the report is provided for a number of reasons. The first is that since more than one service agent may use the sameservice name, it is pos­sible that a subscription based on service name might result in responses from more than once service agent. The service binding object is therefore supplied so that it is possible to distinguish from whom a report originated. The service binding object may also be used to identity a particular service agent and send a request to it. This may be less of an issue if when subscribing to a service agent, the service binding object for the specificservice agent of interest is used as opposed to a service name. This elim­inates the possibility of getting reports from unrelated service agents us ing the same service name.
49
Service Reports
class Subscriber(netsvc.Service):
def __init__(self):
netsvc.Service.__init__(self) # subscribe to the service group "publishers" self.subscribeServiceGroup(self.announce,"publishers")
def announce(self,binding,group,status):
if status == netsvc.SERVICE_AVAILABLE:
# now subscribe to service agent which is member of group self.monitorReports(self.report,binding,"system.*")
else:
self.ignoreReports(binding)
def report(self,service,subject,content):
binding = self.currentReport().publisher() identity = binding.agentIdentity() publisher = "(%s/%s)" % (‘service‘,identity) if subject == "system.ctime":
now = str(netsvc.DateTime()) print "%s became available at %s" % (publisher,now) print "%s originally started at %s" % (publisher,str(content))
elif subject == "system.time":
print "%s was still alive at %s" % (publisher,str(content))
As expected, the subject is that under which any report was published. As to the content of the report, this is not limited to being a string, but can be any of the basi c Python scalar types, a list, tuple or dic­tionary, as well as the None type and a number of extended types. User defined scalar types can also be used providing that appropriate encoders/decoders are available.
If you wish to cancel a subscription to a service, the "ignoreReports()" member function should be used. This should be supplied the name of the service and the exact same subject pattern usedwhen subscribing to the reports in the first place. If no subject pattern is supplied, all subscriptions against that service name will be removed.

Lifetime of Reports

When publishing a report, the report will be sent to any service agents which have a current subscrip­tion which matches the subject associated with the report. The default behaviour is then such that the publishing service forgets all about the report. In this case, if a new subscription arrived immediately after, it would only be sent any reports which were published after its subscription was received. The new subscriber would not receive a copy of the report which was published just before its subscription was received.
In some situations however, it is desirable that a new subscriber be able to obtain the last report which mayhave been published against any subject it is interested in. This isuseful in the context thata report is used to reflect the status of a service. By being able to obtain the last published report, a subscriber can know the current state of the service immediately and doesn’t have to explicitly request it or wait for the status to change.
50
Lifetime of Reports
For such cases, it is possible to supply an optional lifetime for a report. That is, a time in seconds for which the report should be cached by the publishing service. When such a value is supplied, if a sub­scription arrives within that time, it will be sent a copy of that report. If a value of "-1" is supplied for the lifetime, it will effectively cache the report indefinitely.
# publish and cache indefinitely self.publishReport("system.status","idle",-1)
# publish and cache for 60 seconds self.publishReport("system.action","twiddle thumb",60)
# publish but don’t cache self.publishReport("system.thought","bored")
A cached report will only be discarded if a new report is published against the same subject, or the lifetime specified expires. If a new report published against the same subject has no lifetime associated with it, the cached report will be discarded, but the new report will not be cached. Note that with this mechanism, only the last report published on a specific subject will ever be cached when a lifetime value is provided.
To make the implementation as simple as possible, a report which has been cached against a subject with a finite lifetime and which has expired, will only be discarded when a new report with the same subject name is published, or a new subscription arrives which would have matched the subject. This is done to avoid having to setup internal timers to trigger destruction of the report at the moment it ex­pired.
A consequence of this approach however, is that a report may consume resources unnecessarily be­yond the lifetime which it was supposed to exist. If this becomes an issue, it is possible for a service agent to periodically purge any expired reports itself. This can be done by calling the member function "purgeReports()".
class Publisher(netsvc.Service):
def __init__(self):
netsvc.Service.__init__(self,"publisher") # purge expired reports every 15 minutes self.scheduleAction(self.purgeReports,"*/15****")
In addition to being able to explicitly purge expired reports for performance reasons, a service agent may also prematurely expire and purge reports which are older than a certain time. The member func­tion for this is "expireReports()" and accepts a subject pattern and optional age in seconds. The age defaults to "0" which would result in any cached report matching the subject pattern be ing imme­diately expired and purged. If a non zero value for age is supplied, only reports which were older than that age would be expired and purged. To apply this to all cached reports, regardless of subject, the "expireAllReports()" member function can be used.
51
Service Reports
Although"purgeReports()" exists specificallyto deal with potential performanceissues in a very limited number of cases, the "expireReports()" and "expireAllReports()" member func­tions are useful where a service may have reset itself and it was necessary to discard all cached reports so that new subscribers didn’t receive them.

Identity of Subscribers

In most circumstances the identity of a subscriber is not important, however, such information can be quite useful in a few circumstances. At present this information is available by overriding a method in the service agent base class.
class Publisher(netsvc.Service):
def __init__(self):
netsvc.Service.__init__(self,"publisher")
def handleSubscription(self,subscription):
subscriber = subscription.subscriber() if subscription.status() == netsvc.SUBSCRIPTION_REQUESTED:
if self.matchSubject(subscription.subject(),"system.time"):
self.sendReport(subscriber,"system.time",netsvc.DateTime())
One can use this feature in preference to caching reports when they are published. That is, rather than caching a report when it is published so that a new subscriber automatically receives it, generate the report only when the subscription arrives. Obviously however, in this approach we would only want to have the report sent to the particular subscriber and not to all subscribers as they would potentially get duplicates otherwise.
Tocater forthis scenario,the member function "sendReport()" is supplied. Inthis variant of report publishing, the first argument is the service binding object of the subscriber obtained from the sub­scription notification. This report will only be sent to the subscriber inquestion and willnot be cached. Note that if a report was also cached against the subject in question, the subscriber would still receive itas well. Bothanonymous publishing andtargetedreports should therefore not be used in combination for a specific subject as it may give undesired results.
The member function "matchSubject()" is supplied to assist in determining if the subject pattern contained in the subscription matches that of a particular subject. The first argument to "matchSub- ject()" should be the pattern and the second the actual subject. Although not used here, the opposite to the status value " SUBSCRIPTION_REQUESTED" is the value "SUBSCRIPTION_WITHDRAWN".
Note that if the "sendReport()" member function is used to send a report and the recipient has a subscription against the publishing service, but doesn’t have a subscription against that service match­ing that subject, the report will not be delivered via the callback it originally supplied with its subscrip­tion. A similar situation is where a service receives an unsolicited report, or had since unsubscribed from the reports. In these cases there is no current callback in place for reception of the report. When this occurs the member function "unexpectedReport()" will be called. A service agent may if it desires override this member function so as to deal with any such unexpected reports.
52
Identity of Subscribers
A further use of the mechanism for identifying a subscribers identity, is so that subscriptions can be tracked and for processing or interception of data only to be undertaken while there are subscribers in­terested in the results. This avoids unnecessarily publishing reports when it is known there would be no one to send them to.
class LogMonitor(netsvc.Service):
def __init__(self):
name = "logmon@%s" % netsvc.processIdentity() netsvc.Service.__init__(self,name) self._logger = netsvc.Logger() self._channels = {}
def notify(self,channel,level,message):
agent = channel[1:-1] report = {} report["agent"] = agent report["level"] = level report["message"] = message self.publishReport(agent,report)
def handleSubscription(self,subscription):
agent = subscription.subject() channel = "(%s)" % agent if subscription.status() == netsvc.SUBSCRIPTION_REQUESTED:
if len(agent) != 0:
subscriber = subscription.subscriber().agentIdentity()
if not self._channels.has_key(channel):
self._channels[channel] = [] self._logger.monitorChannel(channel,self.notify) self._channels[channel].append(subscriber)
else:
if self._channels.has_key(channel):
subscriber = subscription.subscriber().agentIdentity()
if subscriber in self._channels[channel]:
index = self._channels[channel].index(subscriber) del self._channels[channel][index] if len(self._channels[channel]) == 0:
del self._channels[channel] self._logger.monitorChannel(channel,None)
logger = netsvc.Logger()
class Publisher(netsvc.Service):
def __init__(self):
netsvc.Service.__init__(self,"publisher") self._channel = "(%s)" % self.agentIdentity()
def debug(self,message):
logger.notifyChannel(self._channel,netsvc.LOG_DEBUG,message)
In this use of subscription information, the subscription to a specific subject is used to trigger intercep­tion of messages logged via the logger interface. For the time that subscriptions exist for a particular
53
Service Reports
subject corresponding to a log channel, the log messages on that log channel will be intercepted and published. This can be useful as a remote debugging mechanism and will not unnecessarily load the process as information is only being captured and published when it is actually required.

Existence of Publishers

When a subscription to a service is made, if the service holds any cached reports with a subject match­ing the subscription, the subscriber will receive them immediately. If however there were no such re­ports, the subscriber will not receive any reports until some are published having a subject which matched its subscription. Even when there are reports which can be sent back immediately, if there are reports against multiple subjects, there is no guarantee as to which order they will be received in.
As a consequence, using the reception of a report as an indicator that a service has become available is not a good approach to take. This is because a report may not be received until some time after the service becameavailable and the subscription accepted. Further, thereis no indicationwhen the service is no longer available.
One way as previously described of knowing when a service becomes available or when it is with­drawn, is to subscribeto the service registry. Although this willwork, if you have restricted the service audience of your service agent, it will also possibly tell you about services outside of the scope of what you can subscribe to.
To avoid this difficulty, the member function "handlePublisherNotification()" is provid­ed. This member function can be overridden in your service agent and will be called only when a sub­scription has been matched up and accepted by the service being subscribed to. Note that this notification will only occur for the first subscription against a particular service agent.
This member function willalso be called to acknowledge withdrawal of the last subscription against a particular service agent, or when a service agent to which you were subscribed has been withdrawn.
class Subscriber(netsvc.Service):
def __init__(self):
netsvc.Service.__init__(self) self.monitorReports("publisher","*")
def handlePublisherNotification(self,notification):
name = notification.publisher().serviceName() identity = notification.publisher().agentIdentity() publisher = "(%s/%s)" % (‘name‘,identity) if notification.status() == SERVICE_AVAILABLE:
print "%s AVAILABLE" % publisher
else:
print "%s WITHDRAWN" % publisher
Knowledge of when a subscription has been accepted or when the service agent subscribed to has been withdrawn can be useful when there is more than once service agent with the same name, and it is nec-
54
Existence of Publishers
essary to track the lifetime of each. It is also useful where it might be necessary to immediately send off a request to each service agent to obtain information not available via published reports.
55
Service Reports
56

Service Requests

The ability within the service agent framework to find out what services exist and the ability of a serv­ice agent to publish reports can be useful in itself, but more often than not one wantsto make a specific request of a service to perform some action. In most cases such an action would result in a response, be it the return of data related to the request being made or an error indication. Referred to as request/ reply, this is probably the most fundamental feature of message oriented middleware software.
As with the implementation of the publish/subscribe feature, simplicity has been a major driving force in influencing the design. As a result, the implementation of the request/reply feature should not be likened to that of point to point messages using persistant messages queues. In this implementation, if a service to which you want to send a request doesn’t exist you will not be able to send your request, nor is a service able to receive any requests sent when it wasn’t running.
Although persistant message queues are not a feature of this implementation, the request/reply and publish/subscribe features can actually be seen as sitting at a lower level of abstraction. As a result, it would be possible to build on top of these features and implement persistent message queues and gau­ranteed modes of delivery if required. For many systems such features aren’t required though, so they are not implemented with the aim being to make the software as simple as possible to use and under­stand.

Sending a Request

In order to send a request to a service you need to first obtain a service endpoint object. This is a special Python object which holds an internal reference to the service binding object for the serviceand which will automatically dispatch your request for you. To obtain a service endpoint, the member function "serviceEndPoint()"isused.
57
Service Requests
When invoking "serviceEndPoint()", the member function needs to be supplied with either a service binding object for the particular service agent to which you wish to send the request, or the name of the service. When a service name is supplied, a lookup will be made against the service reg­istry and the first service agent found with that service name will be used.
To invoke the request against the remote service agent, the service endpoint object is used as a proxy. That is, a member function call is made a gainst the object as if it were the actual service object you wished to call. Theonly difference is that the call isn’t synchronous but asynchronous. This means that the result is not returned immediately.
As to the parameters to the call, multiple arguments can be supplied, with any of the basic Python sca­lar types, a list, tuple, dictionary, the None type, as well as a number of extended types being able to be used. User defined scalar types can also be used providing that appropriate encoders are available. Note that keyword arguments cannot be used and will be ignored.
class PagerClient(netsvc.Service):
def __init__(self,number,message):
netsvc.Service.__init__(self,"","") service = self.serviceEndPoint("SMS") if service:
service.send(number,message)
When a service endpoint is created by using a service name, you should always check whether a serv­ice agent with that name could actually be found. This is done by doing a truth test against the service endpoint object or comparing it to None. Note that although it may equate to None, the service end­point object is a distinct object in its own right. If you don’t check the validity of the service endpoint object and still make a request against the service, a special exception indicating that such a service isn’t available will be raised.
class PagerClient(netsvc.Service):
def __init__(self,number,message):
netsvc.Service.__init__(self,"","") service = self.serviceEndPoint("SMS") try:
service.send(number,message)
except netsvc.ServiceUnavailable:
# ...
Obviously the service name by itself can only be used if you don’t care which instance of a service is used when there is morethan one. If you wanted to select a specific service agent, or wanted to be able to send a request to all service agents with the same service name, you would need to perform a lookup against the service registry to obtain the full list of service agents.
class Client(netsvc.Service):
def __init__(self,name):
netsvc.Service.__init__(self,"","") bindings = self.lookupServiceName(name)
58

Handling a Response

for binding in bindings:
service = self.serviceEndPoint(binding) if service:
service.reset()
Handling a Response
When you send a service request, you do not get an immediate response back. That is, the call is asyn­chronous. If you want to be able to capture any response generated from a request, you need to capture the conversation id associated with the request and then register a callback to handle the response. The conversation id is the value returned when you make the call against the service endpoint object. Hav­ing obtained the conversation id you must then register a callback to handle the response using the member function "processResponse()". If you also want to be notified that the request has failed, you will also need to set up a separate callback using the "processFailure()" member function.
class Client(netsvc.Service):
def __init__(self,name):
netsvc.Service.__init__(self,"","") service = self.serviceEndPoint("SMS") if service:
id = service.uptime() self.processReponse(self.uptimeResponse,id) self.processFailure(self.uptimeFailure,id)
def uptimeResponse(self,result):
print result
def uptimeFailure(self):
print "failure"
The callbacks which you put in place to handle the result and/or failure will be automatically deregis­tered when a response is received. This will be the case whether the response is valid or was a failure indication. Prior to having received a response, if you decide you are no longer interested in the re­sponse, you can call the member function "ignoreResponse()" supplying the conversation id. If you are submitting multiple requests in one go, you must call the "processResponse()" and/or "processFailure()" member functions for a conversation id before you send any subsequent re­quest.
Note that prior to the release of OSE 7.0pl5, instead of using the "processResponse()"and "processFailure()" member functions, one would use the "monitorResponse()" method. This method in effect combined the operation of both of the new methods albeit it with subtle differ­encesas far as the arguments the callbackwould be passed and thefunctionality it implemented. Using the new methods it is possible to register separate callbacks for handling of the result versus a failure. It is even possible to only register interest in one or the otherof the result or a failure notification. The "monitorResponse()" member function should as a result now be viewed as deprecated and should not be used.
59
Service Requests

Identifying a Response

If a callback is being registered to handle the response from multiple service requests, you will most likely need to be able to identify to which requesta response belongs to. To get the conversation ID of the original request, the "conversationId()" member function can be called.
class Client(netsvc.Service):
def __init__(self,name):
netsvc.Service.__init__(self,"","") bindings = self.lookupServiceName(name) for binding in bindings:
service = self.serviceEndPoint(binding) if service:
id = service.uptime() self.processResponse(self.uptimeResponse,id) print "request",binding.agentIdentity(),id
def uptimeResponse(self,result):
id = self.conversationId() print "result",id,result
Instead of requesting the conversation id, it is also possible to define your callback so as to take two arguments instead of one, these being the conversation id and the result instead of just the result.
class Client(netsvc.Service):
def __init__(self,name):
netsvc.Service.__init__(self,"","") bindings = self.lookupServiceName(name) for binding in bindings:
service = self.serviceEndPoint(binding) if service:
id = service.uptime() self.processResponse(self.uptimeResponse,id) print "request",binding.agentIdentity(),id
def uptimeResponse(self,id,result):
print "result",id,result
These are not keyword arguments, but positional parameters which the code which calls the callback function supplies or not based on the number of arguments the callback accepts. In other words, the callback must accept the appropriate number of arguments as necessary and in the specified order. If you know that the remote method being called doesn’t actually return a valid response, ie., it returns a void or null response, you can even leave out the parameters altogether.
class Client(netsvc.Service):
def __init__(self,name):
netsvc.Service.__init__(self,"","") bindings = self.lookupServiceName(name) for binding in bindings:
service = self.serviceEndPoint(binding) if service:
60

Detecting a Failure

id = service.reset()
self.processResponse(self.resetResponse,id)
print "request",binding.agentIdentity(),id
def resetResponse(self):
print "result"
In addition to "conversationId()" the member function "currentResponse()"isalso available. This member function returns an object providing both the "conversationId()"and "sender()" member functions. If you need the service binding object for the service agent who sent the response, you can perform a lookup against the service registry using the service address provided by "sender()". Note though that you shouldn’t assume that the service binding object will be avail­able as the remote service may have been withdrawn by the time you make your query.
Detecting a Failure
If you send a service request to a service agent and you need to detect if a failureoccurs, you will need to have registered a callback using the "processFailure()" member function. A failure may oc­cur due to the service not supplying a method to handle the request you made, an incorrect number of arguments being supplied, an error within the method being called or because the remote service agent was withdrawn before a response was received.
Whena failure does occur, the details of the failure can be obtained in a number of ways. If the callback you provide doesn’t take any arguments, you can obtain a failure object detailing the error which oc­curred by calling the "currentFailure()" member function. The member functions provided by the failureobject are "error()", "description()", "origin()" and "details()". The con- versation id associated with the request which failed can be obtained using the member function "conversationId()".
Of the member functions provided by the failure object, the "error()" member function returns an integer error code. The "description()" member function returns a text description of the error. The "origin()" member function returns a string which in some way identifies the origin of the er­ror and "detail()" may contain as text, extra details relating to the error which has occurred.
class Client(netsvc.Service):
def __init__(self,name):
netsvc.Service.__init__(self,"","") service = self.serviceEndPoint("SMS") if service:
id = service.uptime() self.processResponse(self.uptimeResponse,id) self.processFailure(self.uptimeFailure,id)
def uptimeResponse(self,result):
print result
def uptimeFailure(self):
id = self.conversationId() failure = self.currentFailure()
61
Service Requests
if failure.origin() == "netsvc" and \
failure.error() == netsvc.SERVER_METHOD_UNAVAILABLE:
# method didn’t exist
As an alternative to using the "conversationId()" member function to obtain the conversation id of the failed request, if the callback accepts a single argument, the conversation id will be passed as an argument to the callback function.
class Client(netsvc.Service):
def __init__(self,name):
netsvc.Service.__init__(self,"","") service = self.serviceEndPoint("SMS") if service:
id = service.uptime() self.processResponse(self.uptimeResponse,id) self.processFailure(self.uptimeFailure,id)
def uptimeResponse(self,result):
print result
def uptimeFailure(self,id):
failure = self.currentFailure() if failure.origin() == "netsvc" and \
failure.error() == netsvc.SERVER_METHOD_UNAVAILABLE:
# method didn’t exist
This ability to have details of the failure suppliedas arguments to the callback function also extends to the contents of the failure object if the callback function accepts an additional four parameters in ad­dition to that for the conversation id.
class Client(netsvc.Service):
def __init__(self,name):
netsvc.Service.__init__(self,"","") service = self.serviceEndPoint("SMS") if service:
id = service.uptime() self.processResponse(self.uptimeResponse,id) self.processFailure(self.uptimeFailure,id)
def uptimeResponse(self,result):
print result
def uptimeFailure(self,id,error,description,origin,details):
if origin == "netsvc" and error == netsvc.SERVER_METHOD_UNAVAILABLE:
# method didn’t exist
These are not keyword arguments, but positional parameters which the code which calls the callback function supplies or not based on the number of arguments the callback accepts. In other words, the callback must accept the appropriate number of arguments as necessary and in the specified order.
62

Lack of Response

Lack of Response
When you send a request, there is no gaurantee that the remote service agent hasn’t been destroyed even before it receives your request. If a remote service agent delays sending an immediate response to your request, the problem might also arise that the remote service agent is destroyed before it com­pletes the response. Finally, an intermediate process relaying your request might be shutdown or crash meaning either the request or response is lost.
In orderto handle these situations, whenthe "processFailure()" member function is used to reg­ister interest in the failure of a request, it will automatically setup a subscriptionon the existance of the remote service agent against which the request has been made. In the event that the remote service agent becomes unavailable before a response is received,an application error will be returned as a fail­ure to provide notification of this occuring.
class Client(netsvc.Service):
def __init__(self,name):
netsvc.Service.__init__(self,"","") service = self.serviceEndPoint("SMS") if service:
id = service.uptime() self.processResponse(self.uptimeResponse,id) self.processFailure(self.uptimeFailure,id)
def uptimeResponse(self,result):
print result
def uptimeFailure(self,id,error,description,origin,details):
if origin == "netsvc" and error == netsvc.SERVER_APPLICATION_ERROR:
# request has failed, possibly because no response was received
Notethat the "monitorResponse()" member function which has beenmade deprecated as of OSE
7.0pl5,does not setup a subscription to the existance of the remote service agent. Thus, if youare using this older member function to catch the failure of a request, you will not get any failure notification in these circumstances.
Although the "processFailure()" member function will ensure that a failure is returned if no re­sponse is received prior to the remote service agent becoming unavailable, programming errors or ex­ternal communications failuresin code associated with the remote service agent might stillresult in no response beingreceived where the remote service agent still exists. If this is an issue and you also want to implement a timeout whereby if no response has been received within a certain period of time, a timeout value can be supplied when you call the "processFailure()" member function.
class Client(netsvc.Service):
def __init__(self,name):
netsvc.Service.__init__(self,"","") service = self.serviceEndPoint("SMS") if service:
id = service.uptime() self.processResponse(self.uptimeResponse,id)
63
Service Requests
self.processFailure(self.uptimeFailure,id,60)
def uptimeResponse(self,result):
print result
def uptimeFailure(self,id,error,description,origin,details):
if origin == "netsvc" and error == netsvc.CLIENT_REQUEST_TIMEOUT:
# timeout occurred
When a timeout occurs, it will be notified as a request failure. The timeout should be the maximum number of seconds to wait. The callback will be automatically deregisteredand if the responsedid sub­sequently arrive it would be ignored. If you wanted a timeout to occur but didn’t want the callback to be deregistered, you would need to create your own timer. If that timer uses the conversation id corre­sponding to the request as the timer name, the timer will be automatically stopped if a response does actually arrive. You should not use the conversation id to set up a timer if you have already defined a timeout when calling the member function "processFailure()" as internally it will use the con- versation id for its own timer.

Servicing a Request

When you send a request, if the remote service agent is implemented using the Python interface, not just any member function of the service can be called. In order that a member function of a service can be called, the service agent must have exported it as a public method. This is done by calling the mem­ber function "exportMethod()" and it would normally be done from within the constructor of the service agent.
class PagingService(netsvc.Service):
def __init__(self,name="SMS"):
netsvc.Service.__init__(self,name) self.exportMethod(self.time) self.exportMethod(self.uptime) self.exportMethod(self.send)
def time(self):
return netsvc.DateTime()
def uptime(self):
# ...
def send(self,number,message):
# ...
By default the method name associated with the member function will be its actual name. If you wish to export a member function under a different method name, the method name can be supplied as an extra argument to the "exportMethod()" member function.
class PagingService(netsvc.Service):
def __init__(self,name="SMS"):
netsvc.Service.__init__(self,name) self.exportMethod(self.sendMessage,"send")
def sendMessage(self,number,message):
# ...
64

Generating a Failure

The reason for requiring that methods be explicitly exported is that it would usually be quite dangerous toallow open access to all member functions of a class. Thisis because any class is likely to implement methods to assist in intermediate stages of processing a request. Providing automatic access to such member functions could compromise the operation of the class.
When a method is invoked as a result of a service request, the default behaviour will be that the value returned from the method will be what is returned to the caller as the response. If necessary, a method may explicitly indicate that a failure response should instead be returned. A method can also indicate that a delayed response will be sent. This latter case is useful when the service needs to do something first in order to generate a suitable response.
Generating a Failure
If a method encounters an error and raises an exception this will be caught by the service agent frame­work and a failure response will be generated. The value of the origin for this type of failure will be "netsvc" and the value of the error code will be "SERVER_APPLICATION_ERROR". If you want to generate a failure response which is specific to your application, you should catch any exceptions and indicate the type of failure response by calling the member function "abortResponse()".
class Database(netsvc.Service):
def __init__(self,name="database",**kw):
netsvc.Service.__init__(self,name) self._database = MySQLdb.connect(**kw) self.exportMethod(self.execute)
def execute(self,query,args=None):
try:
cursor = self._database.cursor() cursor.execute(query,args) if cursor.description == None:
return cursor.rowcount return cursor.fetchall()
except MySQLdb.ProgrammingError,exception:
details = netsvc.exceptionDetails() self.abortResponse(1,"Programming Error","db",details)
except MySQLdb.Error,(error,description):
self.abortResponse(error,description,"mysql")
Thefour arguments to the member function "abortResponse()"are the errorcode, thedescription of the error, the origin and any additional details. It is recommended that an origin which clearly iden­tifies the source of the error, or namespace from which the error codes are derived always be used. If an origin is not used, it becomes impossible to programmatically deal with an error when different as­pects of a service generate overlapping error code sets.
Note that the "abortResponse()" member function will in turn raise its own special exception. When this exception is caught by the service agent framework, it will be translated into a failure re­sponse as described by the arguments used to call "abortResponse()". As a new exception is
65
Service Requests
raised, you should avoid an except clause which catches all exceptions in any code which encloses code which might call "abortResponse()". Alternatively, you should explicitly pass on excep­tions of type ServiceFailure.
try:
self.execute(...)
except netsvc.ServiceFailure:
raise
except:
self.abortResponse(...)
If many of the public methods of a service generate the same type of exceptions, rather than provide code to catch the exceptions in every method, it is possible to override the member function "exe- cuteMethod()". This member function is called by the service agent framework to call the actual member function referred to by a service request. It is important to preserve the existing functionality of this method otherwise service requests will not execute correctly.
class Database(netsvc.Service):
# ... def executeMethod(self,name,method,params):
try:
return netsvc.Service.executeMethod(self,name,method,params)
except MySQLdb.ProgrammingError,exception:
details = netsvc.exceptionDetails() self.abortResponse(1,"Programming Error","db",details)
except MySQLdb.Error,(error,description):
self.abortResponse(error,description,"mysql")
The member function "executeMethod()" might also be overridden if you want to track what re­quests are being made against a service. The arguments to the member function are the name of the method, the actual member function and the parameters to be supplied when the member function is called.

Delaying a Response

In a distributed application, it is sometimes the case that when a method is called it doesn’t have the information necessary to generate an immediate response. This may be the ca se where it needs to ini­tiate its own service requests to accumulate the data needed to generate the result. Because the service agent framework is based on an event driven system, it is not possible for the method to simply block waiting for its own data. This is because the method must return before anything else can execute.
Todeal withthis, the memberfunctions"suspendResponse()" and "resumeResponse()"are provided.If the "suspendResponse()" member functionis called,it will raise an exception which will be caught by the service agent framework. The name of this exception is DelayedResponse and lets the service agent framework know that a response will be sent at a later time.
66
Delaying a Response
When the member function "suspendResponse()" is called, a callback function should be sup­plied as argument which finalises the request and returns the appropriate result. The callback passed to"suspendResponse()" will only be called when the "resumeResponse()"method is called at some later point in time. In particular, you would call "resumeResponse()" once you have col­lected together all the data which forms the result for the original request.
class DatabaseProxy(netsvc.Service):
def __init__(self,name="database-proxy")
netsvc.Service.__init__(self,name): self.exportMethod(self.tablesRequest,"tables") self._request = {} self._result = {}
def tablesRequest(self):
service = self.serviceEndPoint("database") id = service.execute("show tables") self.processResponse(self.queryResponse,id) self.processFailure(self,queryFailure,id) self._request[id] = self.conversationId() self.suspendResponse(self.tablesResult)
def tablesResult(self):
request = self.conversationId() result = self._result[request] del self._result[request] return result
def queryResponse(self,id,result):
if self._request.has_key(id):
request = self._request[id] self._result[request] = result del self._request[id] self.resumeResponse(request)
def queryFailure(self,id,error,description,origin,details):
if self._request.has_key(id):
request = self._request[id] del self._request[id] self.cancelResponse(request,error,description,origin,failure)
As can be seen, it will be necessary to save away state information about a suspended request so it can be later resumed. In this example the conversation id of the original request is associated with the con­versation id of the downstream request. When the result of the downstream request is received, it can be saved away and the originalrequestresumed withthe cachedresult beingreturned. In the event that the downstream request fails, the "cancelResponse()" method is used to abort the original re­quest.
As with the "abortResponse()" member function, if "suspendResponse()" is being called from within a method, it will be necessary for any code to be explicit about what exceptions it catches, or to at least catch the DelayedResponse exception and pass it on as is.
67
Service Requests
Note that "suspendResponse()" and "resumeResponse()" were only added in OSE 7.0pl5 and are a layer on top of the "delayResponse()" method which only performed the single opera- tion of raising the exception of type DelayedResponse. The newer functions shouldmake the task of implementing a delayed responese easier, so if you are using "delayResponse()" you should change your code to use the newer functions.

Identity of the Sender

Normally it is not necessary to know the identity of the sender of a request. If a means of identifying who has initiated the request is required however, the details of the current request can be queried to obtain the address of the sender. This can be useful where a separate session object in the form of a new service is created to manage interaction with a particular client. To obtain the request object for the current request the "currentRequest()" member function is used.
By calling the "sender()" member function of a request object, the service address of the service agent initiating the request can be obtained. Having created a separate session for that client, all re­quests for that session can be authenticated as being from the same service agent. Such a scheme may even have as a prelude a log in mechanism to ensure that a service agent making the request has suffi­cient privileges to initiate a separate session.
Whether or not a login and password is required,the idea is that the method usedto initiate the session returns the name of the service created to manage the session. Such a session object should monitor the existence of the service agent who initiated the session such that the session can be destroyed au­tomatically when the owner is withdrawn.
class Session(netsvc.Service):
def __init__(self,name,client):
netsvc.Service.__init__(self,name) self._client = client self.subscribeServiceAddress(self.announce,client) self.exportMethod(self.close)
def announce(self,binding,status):
if status = netsvc.SERVICE_WITHDRAWN:
self.destroyReferences()
def close(self):
client = self.currentRequest().sender() if client != self._client:
self.abortResponse(1,"Not Owner of Session")
self.destroyReferences()
class Service(netsvc.Service):
def __init__(self,name="service"):
netsvc.Service.__init__(self,name) self._count = 0 self.exportMethod(self.login)
def login(self,login,passwd):
# authorise login/passwd
68

Invalid Request Method

client = self.currentRequest().sender() self._count = self._count + 1 name "%s/%d" % (self.serviceName(),self._count) session = Session(name,client) return name
Such a mechanism as described can’t be used if such a request to create a session may originate over an RPC over HTTP connection. This is because the service agent which acts as proxy for the request is transientand willbe destroyed once the request has completed. Further, the serviceagent which acts as proxy isn’t visible outside of its own process.
The alternative to binding the session to a particular service agent is to create a pseudo unique name for the service managing the session. To ensure that the session object is destroyed, a timer could be used to trigger the destruction of the service after a certain period of inactivity. Each request made against the service would reset the timer giving it a new lease on life. The timeout may be something which is fixed or which could be defined as one of the arguments supplied in the request to create the session.
Invalid Request Method
When a service request arrives with a method name which the service doesn’t provide, the member function "invalidMethod()" is called. By default this method will generate a failure response with origin of "netsvc" and error code of "SERVER_METHOD_UNAVAILABLE". This member function might be overridden if the ability to dump out information about requests against invalid methods was wanted. Any derived implementation of this member function should still call the base class version to generate the appropriate failure response indicating a method was unavailable.
class Service(netsvc.Service):
# ... def invalidMethod(self,methodName,params):
print methodName,params netsvc.service.invalidMethod(methodName,params)

Local Service Requests

Use of the interface described so far for initiating a service request is the preferred interface. This is because it will not matter if the service to which the request is being sent is in the same process or an­other process. It also will not matter if the service is written in the same language or a different lan­guage. However, when the service is in the same process and is also written in Python a short cut is available. Thiswill avoid the complexity of usingdelayed responses, but does mandate that the service being called always be in the same process.
Access to this short cut is through the Python class LocalService. An instance of the class is cre­ated with the name of the service against which the call is to be made. A call is then made against the object as if it werethe actual service. This is the same as when a serviceendpoint object is used except
69
Service Requests
that the result is returned immediately. Note that since the responseis immediate, you can’t call a meth­od which itself would try and use a delayed response.
class DatabaseProxy(netsvc.Service):
def __init__(self,name="database-proxy")
netsvc.Service.__init__(self,name): self.exportMethod(self.tables) self._active = {}
def tables(self):
service = netsvc.LocalService("database") return service.execute("show tables")
As with a service endpoint object, if there is more than one service agent with the same name, the first one found will be used. The only restriction is that candidate service agents will only come from the set of service agents in the same process which are also implemented in Python. If a request is made against an instance of LocalService and no service agent could be found, an exception of type ServiceUnavailable will be raised. To avoid this, you can also perform a truth test against the object.
Although the request is channelled through directly to the service instance, it is still not possible to call methods of the service which haven’t been exported. When this occurs, an exception of type Serv- iceFailure is raised where the origin is set to "netsvc" and the error code is set to "SERVER_METHOD_UNAVAILABLE". Any other errors raised by the method being called are simi­larly indicated using the ServiceFailure exception. Note that each of the attributes of the failure, ie., the error code, description, origin and details, are available using member variables and not mem­ber functions as is the case with a failure response object.
If the member function making the request is servicing a request from another service, it may be ap­propriate to translate the exceptions into different types of failure responses. As is, the exceptions would translate into a failure response with the same details. This may be confusing for example if it were an exception indicating that a method was unavailable. The remote service making the request would errornously think that it had called an invalid method when it was actually the implementation of the method which it had called which had done the wrong thing.
Note that the LocalService class is being deprecated and will most likely not be available in a future version of OSE. You are therefore advised not to write any new codeusing it and change existingcode to use the full messaging system features.
70

Message Exchange

The features of the service agent framework may be used standalone within a single process or across a set of connected processes. That is, useof the service agent framework is not dependent on a process being able to connect to a central message exchange process. When combined with the HTTP servlet framework and RPC over HTTP interface, a single process may be more than adequate for many ap­plications, especially simple web based services.
If such a service starts to out grow the bounds of a single process however, the application can easily be split up across multiple processes or machines. This will enable services to be distributed based on load or proximity to required resources. Be ing able to split up the application in this way is also ad­vantageous in that it becomes easier to introduce into the application distinct components which are written in C++ as opposed to Python.
Unlike most message oriented middleware packages, there is no dedicated message exchange process. Instead, the components relating to client and server aspects of the mechanism for implementing a dis­tributed service agent framework are directly accessible. This means that it is possible to take an ex­isting application and embed within it a message exchange server endpoint. Growing your application then becomes a simple matter of creating new processes which incorporate a message exchange client endpoint and have it connect to your original application.
The major classes in the OSE C++ class library used to provide this functionality are the OTC_Exchange, OTC_InetClient and OTC_InetListener classes. Note that the Python in­terface only provides the ability to create connections between processes which make use of the INET socket protocol. When the C++ interface is used directly, on a UNIX platform there is also the option of using the UNIX socket protocol.
71
Message Exchange

Exchange Initialisation

To create a message exchange endpoint in a process, the Exchange class is used. When creating an instance of the Exchange class it is necessary to specify whether it is performing the role of a mes­sage exchange server or that of a client. A message exchange server is a process which takes on the role of being a hub for message distribution. That is, a message exchange server is a process which accepts connections from one or more message exchange clientsand distributes messages betweenthe client processes as appropriate.
Two different approaches can be taken in regard to the message exchange server. The first is that the message exchange server component can be embedded within an existing application and new clients attach to that existing application. Alternatively, a separate process can be created which embeds just the message exchange server component and the existing application, now modelled as a client, along with any new clients connect to this new process.
In both server configurations, initialisation of the message exchange server endpoint is the same. Sub­sequent to initialisation, the endpoint is then directed to listen on a specific port for any client connec­tions.
port = 11111 exchange = netsvc.Exchange(netsvc.EXCHANGE_SERVER) exchange.listen(port)
In the case of a message exchange client, instead of listening for connections, the endpoint is directed to connect to a message exchange server.
host = "localhost" port = 11111 retry = 5 exchange = netsvc.Exchange(netsvc.EXCHANGE_CLIENT) exchange.connect(host,port,retry)
Because it is possible that the message exchange server is not available, a retry delay can be specified. When supplied this will result in successive attempts to connect to the server until a connection is es­tablished. The retry delay when supplied needs to be specified in seconds.
Note that if a connection to the server is lost, the client will also attempt to reconnect automatically after the retry delay time has expired. This has the affect that a client will always try to stay connected toits server without you needing to take any specific action. Your processwill not be prematurely shut­down if a connection cannot be established or if a connection is lost.

Service Availability

Unless the service audience of a service agent has been set so as to restrict its visibility, a service will automatically become visible within connected processes through the service registry of the remote process. That is, if a particular service is located within the sameprocess as the message exchange serv-
72
Service Availability
er endpoint and a new client connects, a subscriber to that service in the client will be notified that the service is available. Similarly, any se rvice within a client will become visible from the server as w ell as other connected clients.
Although the service is located in a separate process, the same service registry interface is used to sub­scribe to the presence of the service. Subscription to reports produced by the service and the issuing of requests against that service are also mediated through the same interface aspreviously described. The only exception to this is that the LocalService proxy class cannot be used to communicate with any service in a remote process, it being restricted to services implemented using Python which appear in the same process.
Except for the LocalService proxy class, that there is no distinction in t he interface to communi­cate between services whether they be in the sameor a remote process, means that it is a simple matter to split an application across multiple processes. If a distinct message exchange server process is used, all that is required is that each process embed a message exchange client and connect to the message exchange server.
As an example, a process supporting a service which publishes periodic reports would be written as follows.
class Publisher(netsvc.Service):
def __init__(self):
netsvc.Service.__init__("publisher") self.publishReport("system.ctime",netsvc.DateTime(),-1) self.startTimer(self.timeout,10,"heartbeat")
def timeout(self,tag):
self.publishReport("system.time",netsvc.DateTime()) self.startTimer(self.timeout,10,"heartbeat")
dispatcher = netsvc.Dispatcher() dispatcher.monitor(signal.SIGINT) exchange = netsvc.Exchange(netsvc.EXCHANGE_CLIENT) exchange.connect("localhost",11111,5) dispatcher.run()
The process containing the corresponding subscriber to this service would then be written as follows.
class Subscriber(netsvc.Service):
def __init__(self):
netsvc.Service.__init__(self) self.monitorReports(self.report,"publisher","system.*")
def report(self,service,subject,content):
name = service.serviceName() identity = service.agentIdentity() publisher = "(%s/%s)" % (`name`,identity) if subject == "system.ctime":
now = str(netsvc.DateTime()) print "%s became available at %s" % (publisher,now)
73
Message Exchange
print "%s originally started at %s" % (publisher,str(content))
elif subject == "system.time":
print "%s was still alive at %s" % (publisher,str(content))
dispatcher = netsvc.Dispatcher() dispatcher.monitor(signal.SIGINT) exchange = netsvc.Exchange(netsvc.EXCHANGE_CLIENT) exchange.connect("localhost",11111,5) dispatcher.run()
The only difference is that a message exchange client has been added to each, the actual services are identical to what they were when used in the same process.
In regard to announcements of service availability and their subsequent withdrawal, when everything is in the same process, such an announcements means that the service had been created or destroyed. In the context of a distributed system, such an announcement means that a service is now visible or is no longer visible. Such an announcement doesn’t mean that the service was necessarily destroyed as it could be the case that the message exchange server process was shutdown. Thus the service could still exist, it just may not be reachable.
Because services may become unavailable, or connections lost and also because connections between processes will automatically restart when possible, it is important that client services take notice of an­nouncements regarding the availability of a service it is using. A client service should not assume that a service it is using will always be available and should be programmed to accommodate this fact.

Connection Announcements

Monitoring the existence of services gives precise information about when such services become avail­able. This however may be too much fine detail. If a client process needs to merely know when a con­nection had been established to the message exchange server, it is possible to create a derived version of the Exchange class and override the "handleConnection()" member function.
This member function will be called when a client has successfully connected to a server, when that connection is subsequently lost, or when an initial connection attempt fails. On the server side, the member function is called when a connection is accepted and when it is lost.
class Exchange(netsvc.Exchange):
def __init__(self,type):
netsvc.Exchange.__init__(self,type)
def handleConnection(self,announcement):
state = "INACTIVE" if announcement.state() == netsvc.CONNECTION_ACTIVE:
state = "ACTIVE" process = announcement.remoteProcess() address = announcement.remoteAddress() message = "%s %s (%s)" % (state,process,address) logger.notify(netsvc.LOG_NOTICE,message)
74

Authorisation of Clients

Overriding this method can be useful purely for logging purposes, but might also be used in a client process to trigger an announcement to activate the function of the processupon a connection becoming active. Consequently, the operation of a client process could be suspended or the process shutdown when no active connection could be established or the connection lost.
This latter mode of operation would be necessary when a retry delay is not specified when connecting a message exchange client to a se rver. In this situation the retry delay defaults to the value of "-1", indicating that one and only one connection attempt should be made. If this is used, a client should monitor to see if the connection fails and shutdown the process if it does. Similarly, if it does manage to connect to the server, when that connection is subsequently lost the process should again be shut­down.
Note that creation of a one off connection will currently consume resources that cannot be reclaimed. This is a limitation of the Python interface and is not present when using the OSE C++ class library directlywhich has a way of reclaiming the resources.As the intent is that the message exchange frame­work is for permanent connections, this is not seen as too problematic at this time and will only be ad­dressed at some time in the future.
Authorisation of Clients
As the message exchange framework provides direct access into an application, it may be desirable to restrict which hosts can connect in to an application. If this type of control is required, it can be imple­mented by creating anew derivedversion of the Exchange class and overriding the member function "authorise()". For each client connection that a server gets, this member function will be called with the IP address of the host the client is located on. A server may then reject or accept the connec­tion.
class Exchange(netsvc.Exchange):
def __init__(self,type,hosts=[]):
netsvc.Exchange.__init__(self,type) self._allow = hosts
def authorise(self,host):
return host in self._allow
To accept a connection the member function should return a true value and false otherwise. When a connection is rejected, the client will see it as a failed connection attempt.

Distributed Exchange Server

When an application is distributed across multiple machines, it may not be desirable that processes on one machine must connect to the message exchange server located on another machine. The problem here is that if the machine hosting the message exchange server is shutdown, none of the processes lo­cated on remote machines will be able to communicate with each other. In essence there is a single point of failure.
75
Message Exchange
When an application is distributed across multiple machines, it is often the case that even if one ma­chine were to be shutdown, the processes on a different machine might be able to quite happily keep operating so long as they could still communicate. To support this, a means of setting up a distributed version of the message exchange server is provided.
In this arrangement, each machine has its own message exchange server, with each message exchange server connected to all others. If a machineis now shutdown or connections to one machine lost, other machines will still be able to communicate with processes on any machines which are still accessible. That is, loss of the message exchange server on one machine will only directly impact that machine.
To setup a distributed exchange server, the message exchange server endpoint is created as before. The difference is that as well as listening on a port for new connections, client like connections are created to the other message exchange servers. The aim here is to effectively create a star connected network between the message exchange servers. That is, each message exchange server has a connection to all other message exchange servers.
port = 11111 exchange = Exchange(netsvc.EXCHANGE_SERVER) exchange.listen(port)
delay = 5 for host in hosts:
exchange.connect(host,port,delay)
Note that since connections are bidirectional, it is not necessary for each message exchange server to mutually connect to each other. Thatis, if you havetwo messageexchange servers,it is only necessary for one to connect to the other. In other words, the list of remote hosts in one would be empty, where as the list of the remote hosts in the other would be the reciprocal host. If two message exchange serv­ers do connect to each other, this will be detected and one connection will be ignored, however it should be avoided.

Multiple Exchange Groups

When creating a service agent, the default service audience is "*", indicating that knowledge of the service should be distributed as widely as possible.One alternative is to set the service audience to the empty string, which will always result in the service only being visible within its own process. What occurs for other values of the service audience property depends on the exchange group assigned to a message exchange endpoint.
By default,the exchange group of a message exchangeendpoint is empty,but maybe set by an option­al argument when initialising the class. A message exchange endpoint is only able to be connected to a complimentary message exchange endpoint which is a member of the same group. That is, a message exchange client endpoint can only connect to a message exchange server endpoint with the same ex­change group.
76
Multiple Exchange Groups
With respect to service visibility, a message exchange endpoint will only pass information about serv­ices if the service audience is "*", or if the service audience is the same as the exchange group. The only exception to this is when the exchange group is empty. In that case, an empty service audience will still restrict visibility of a service to its own process.
By using multiple exchange groups within an application, it becomes possible to segment an applica­tion into parts and restrict visibility of services to those parts of the applications which need to see them. As an example, a service may act as a front end for multiple back end services which do the real work and for which it is not necessary that they be visible.
In this example, the process containing the front end service, as well as creating a message exchange client endpoint for the default exchange group, would create its own message exchange server end­point. Thedefault namefor this exchangegroup would be overridden and a different port used for con­nections. Back end processes would then connect to this new port, with all services in the back end processes having a service audience matching that of the new exchange group.
class FrontEnd(netsvc.Service):
def __init__(self,name="database")
netsvc.Service.__init__(self,name) self.subscribeServiceGroup(self.announce,"backend")
def announce(self,binding,group,status):
if binding.serviceAudience() == "database":
# this is one of ours
default = netsvc.Exchange(netsvc.EXCHANGE_CLIENT) default.connect("localhost",11111,5) backend = netsvc.Exchange(netsvc.EXCHANGE_SERVER,"database") backend.listen(11112)
The front end service would use subscription to a service group to know about the existence of any back end services. Each of the back end services would in turn add themselves to the same group so the front end is aware of their existence. The front end service can check the service audience for a service to know for sure that it is one of its back end services and not an imposter visible through the default exchange group.
class BackEnd(netsvc.Service):
def __init__(self,name="",audience="database")
netsvc.Service.__init__(self,name,audience) self.joinGroup("backend")
backend = netsvc.Exchange(netsvc.EXCHANGE_CLIENT,"database") backend.connect("localhost",11112,5)
Having done this, any services within the back end process will only be visible from other back end processes and the front end process. The services in the back end process will not be visible within any process reachable from the front end process over the original message exchange client endpoint at-
77
Message Exchange
tached to the default exchange group. Back end services will still be able to see any services on the default exchange group which had a service audience of "*".
Note that different exchange groups should not overlap. That is, they should only ever share at most one process with any other exchange group. In effect, exchange groups when used should form a hier­archy. The only time that loops are allowed within the way processes are connected is when creating a distributed exchange server for a specific exchange group.

Scalability of the Framework

Because there is no dedicated message exchange server process serving as the sole repository of serv­ice information, the service registry in each process will contain a record of all services it can see. As the size of an application grows to have very large number of services this may result in the size of what otherwise should be a small process to grow unnecessarily.
Currently there are couple of approaches that can be taken to reduce this problem, however, it is rec­ommended that if you know that you will have very large numbers of services and specifically pub­lishers and subscribers, that you might be better off purchasing one of the commercial products which are specifically designed and targeted at such large scalesystems. Such products might not support the concept of distinct services and instead implement a flat name space for subscriptions, but they are more likely to scale better.
In other words, the design of the service agent framework and the message exchange framework lends itself to small to medium size systems. Don’t expectto be able to run the whole of the New Yorkstock market data feeds through this system as it will more than likely not suit your requirements.
Having made this disclaimer, the first things you cando to reduce growth in the size of the service reg­istry in each process,is to not export a service beyond the scope of a process unless you really need to. That is, if the service only needs to be visible within its own process, sets its service audience to be the empty string.
Such a service will not be visible outside of the process and that service will not be able to subscribe to services outside of the process, but in most cases the service will still be able to make a request against a remote service. Restricting the visibility of a service to its own process will also cut down on traffic between processes relating to the existence and withdrawal of services.
The next thing which can be done is to look more closely at the relationship that exists between serv­ices. If there are a group of related services which only need to talk to each, locate them together. This can be done by putting them in the same process and restricting visibility to that process, or by sepa­rating them from the remainder of the application by creating a distinct exchange group. In both cases, have only the services which need to be public actually visible globally.
78

Message Encoding

As the service agent framework is designed as a distributed system covering multiple programming languages, it is necessary that any data being passed around within a report, request or response be se­rialised into a form suitable for transmission as part of a message. At present the encoded form of the data uses a subset of XML. That is, it would qualify as being XML, however to make the implemen­tation easier, the code for decoding such messages will not accept arbitrary XML.
At presentthe exact form of the XML being used is not revealedas this is being reviewed and will most likely change. Further, the protocol used between message exchange endpoints is unique to this soft­ware. It too is being reviewed and will most likely be changed to use some more commonly accept form of handling message boundaries. Any new mechanism will likely also be designed to be able to proxy through HTTP servers, thus avoiding issues with closed firewalls.
That the precise details are not being revealed actually makes no difference as it has no bearing in re­lation to using the software. This is because everything is hidden under a high level API which hides such details, thus allowing for change in the formats used without requiring changes to applications using the software. The only instance where changes might have a visible affect is in respect to the NET-RPC protocol for RPC over HTTP. This would only be an issue if you tried to write your own client for this protocol.
The one aspect of how data is encoded which will not change is in relation to the means of identifying different types. Here the XML Schema Datatypes 2001 specification is used as a guide, with Python types being assigned corresponding types with respect to this specification. Through introduction of customised encoders and decoders, support for user defined scalar data types may however also be added.
79
Message Encoding

Supported Data Types

Communication between services is mediated through a layer of code which is written in C++. The only exception to this is when the LocalService class is used as a proxy to send a request to a serv­ice in the same process which is also implemented in Python. This means that except for when the Lo- calService class is being used, any data which is being transferred between services must go through a process of being encoded into a serialised form at the point of sending and then deserialised at the point of reception.
Data which is being sent between services is not limited to that of just a string. The data to be sent can consist of any of the basic Python scalar types, a list, a tuple or a dictionary. In addition to this, the Python None value may be used, as well as a number of extended types. The only limitation in respect of the Python compound types is that when using a dictionary, the keys must be of type string.Further, when a tuple appears within any data, the recipient will see it as a list and not a tuple. It is not possible to send data which is cyclically self referential.
self.publishReport("string","value") self.publishReport("list",[1,1L,1.1,None]) self.publishReport("dictionary",{"key":"value"})
The extended types which are supported are Boolean, Binary, Date, DateTime, Time and Du­ration. For the Boolean type, there are also predefined values for True and False.The Boolean type should behave correctly with respectto all truth type tests. If the default arguments for
theconstructor of Date and DateTime types are used,they will be initialised to the currentlocal date and current local date and time respectively.
self.publishReport("true",netsvc.True) self.publishReport("false,netsvc.False) self.publishReport("boolean",netsvc.Boolean(1))
self.publishReport("binary",netsvc.Binary("value"))
# current local date self.publishReport("date",netsvc.Date())
# current local date/time self.publishReport("dateTime",netsvc.DateTime())
Whenusing the various date andtime types, they shouldbe initialised with stringvalues corresponding to what type they represent. The format and range of these values should be the subset of values pos­sible under the ISO 8601 date/time standard as described by the XML Schema Datatypes 2001 speci­fication, examples of which a re illustrated below.
80
Type Format Sample
Date YYYY-MM-DD 2001-12-25

Mapping of Scalar Types

Type Format Sample
DateTime YYYY-MM-DDThh:mm:ss 2001-12-25T23:59:59 Time hh:mm:ss 23:59:59 Duration PnDTnHnMnS P1DT23H59M59S
For the date and time types, the current Python implementation does not do any checking to determine if the supplied values are valid, but will pass them as is. Note that the XML Schema Datatypes speci­fication does allow for a timezone in a date and time, but it is recommended that all date and time val­ues be sent as UTC. In the C++ library, only classes corresponding to Date and DateTime exist. These are OTC_Date and OTC_Time.TheOTC_Time class is not able to handle timezones.
The only difference between the Binary type and using a string is that the value suppliedvia the Bi- nary type, will be encoded internally using "base64" encoding when being passed around. This has relevance because in XML most control characters are not permitted in string values. An XML imple­mentation can also collapse a "\r\n" combination to just "\n". If such characters may appear in a string, you should use the Binary type to ensure that they are preserved as is. Note that you do not however have to encode the string using base64 encoding first as the internal implementation will do this for you automatically.
Mapping of Scalar Types
When data is being serialised, the names attributed to scalar types derive from the XML Schema Da­tatypes 2001 specification. The only exception to this is the None type, which notionally is passed around internally with an empty type value. The mapping from Python types to those described in the XML Schema Datatypes specification is as follows.
Python Type Encodes To XML Type
string xsd:string int xsd:int long xsd:long float xsd:double netsvc.Boolean xsd:boolean netsvc.Binary xsd:base64Binary netsvc.Date xsd:date netsvc.DateTime xsd:dateTime
81
Message Encoding
Python Type Encodes To XML Type
netsvc.Time xsd:time netsvc.Duration xsd:duration
If a service is implemented using the OSE C++ class library directly, different size versions of the in­teger and floating point types are available and can be generated in the serialised form of any data. A consequence of this is that when converting any data from its serialised form into instances of Python types, a broader range of possible values types need to be accommodated.
XML Type Decodes to Python Type
xsd:string string xsd:byte
int xsd:short xsd:int xsd:unsignedByte xsd:unsignedShort xsd:unsignedInt
xsd:long xsd:unsignedLong
int or long as appropri-
ate
xsd:integer, xsd:float
float xsd:double xsd:real
xsd:boolean netsvc.Boolean xsd:base64Binary netsvc.Binary xsd:date netsvc.Date xsd:dateTime netsvc.DateTime xsd:time netsvc.Time xsd:duration netsvc.Duration
Note that at the present time, not all of the XML data types in respect of non positive and non negative integersare accommodated. These will most likely be added at some time in the future, however in the short term they don’t add anything extra in relation to the Python interface. Support for the type
82

User Defined Types

"xsd:hexBinary" will also be added at some point in the future as well. If you wish to send a Uni­code string, you should convert it into a string using UTF-8 encoding.
User Defined Types
The intent with the XML Schema Datatypes specification is that additional scalar data types can be introducedby assigning anew name scoped within a distinct namespace. In respect of the types defined by this specification, the namespace "xsd" is used. Note that within this implementation, the name­space is not linked to a URI containing any form of definition for that type. If sending a data value of your own type, it is up to your code to ensure that both ends know what the type means.
The simplest way of adding your own types is by using the Opaque class. When initialised this takes twovalues, a stringidentifying the type of value anda string representingthe valueinits encoded form. It is not necessary to escape any characters in the encoded value which may be specialto XML as such values will be automatically escaped as necessary.
data = complex(1,1) type = "python:complex" self.publishReport("complex",netsvc.Opaque(type,data))
In reality, it isn’tactuallynecessaryto encode a Python complex value in the way shown as a special mapping is by default installed for this type. For this Python type the namespace "python"isused. If defining your own type it is recommended you use some other namespace value which is in some way specifically associated with your application or some third party standard relating to additional XML types.
As a special mapping is provided for the Python complex type, it will be decoded into an instance of the Pythoncomplex type on reception. If howevera mapping is not available for a specified type, the value will be converted back into an instance of the Opaque type. The type associated with the value can then be queried usingthe "type"attribute and the actual encoded data using the "data" attribute.
def dump(self,object):
if isinstance(object,netsvc.Opaque):
print object.type,object.data
The Opaque class provides a means of sending a value without a defined mapping, or of you being able to receive values for which no mapping is defined. If necessary the interface of the Opaque class can be used to dynamically handle such unknown values and perhaps still make some sense of them.

Adding New Mappings

Mappings for new types can be added at two levels. These are at global scope or such that they only applywithin thescope of a singleservice. If a type mapping isadded at global scope,you shouldrealise that such a mapping will be applied to any service. Adding new mappings with global scope should therefore be carefully considered as it may inadvertently affect the operation of another service.
83
Message Encoding
To add a new mapping at global scope the functions "encoder()" and "decoder()" should be used to register functions to do the appropriate conversions. When registering the encoder, the first ar­gument should be either the type object or class object as appropriate. When registering the decoder, the first argument should be the qualified name you have given the type.
The encoder function which you register should accept a single argument, that being an instance of your type. The function should return a tuple containing the qualified name you have given the type and the value encoded as a string. The decoder function should accept two arguments, they being the qualified name you have given the type and the value encoded as a string. The function should return the corresponding instance of the type as described by the encoded value. If the encoded value is invalid, the function should raise an appropriate exception.
def _encode_Complex(object):
return ("python:complex",repr(object))
def _decode_Complex(name,string):
object = eval(string,{},{}) if type(object) != types.ComplexType:
raise TypeError("invalid encoding for complex type")
return object
netsvc.encoder(types.ComplexType,_encode_Complex) netsvc.decoder("python:complex",_decode_Complex)
To define a mapping which applies only within the context of a single service, you need to override the member functions "encodeObject()" and "decodeValue()" as appropriate. Note that the default implementations of these methods will apply any global mappings which are present. If your version of these functions, don’t identify the type you are interested in, your function should call the base class version of the function. The arguments to these functions are similar to the global encoders and decoders.
class Database(netsvc.Service):
def __init__(self,name,**kw):
netsvc.Service.__init__(self,name) # ...
def encodeObject(self,object):
if hasattr(MySQLdb,"DateTime"):
if type(object) == MySQLdb.DateTimeType:
return ("xsd:string",object.strftime())
elif type(object) == MySQLdb.DateTimeDeltaType:
return ("xsd:string",str(object))
return netsvc.Service.encodeObject(self,object)
Providing a mapping which is specific to a service is most often used when the service interacts with a Python module which defines its own types for such values as date and time. In this circumstance, the mapping function can automatically translate an instance of the type into a type appropriatefor the encoded data. This avoids your own code having to manually translate values into corresponding val-
84

Handling Structured Types

uesof the correct typebefore hand. A servicemayalso override the defaultdecoders forextended types such as the date and time types if desired.
Handling Structured Types
The encoding mechanism for data does not provide a way of adding support for your own structured types, whereby the type of that object can also be transmitted. All objects need to be able to be con­verted into instances of scalar types, dictionaries, tuples or lists. To avoid havingto do this conversion manually, it is however possible to define an encoder for a structured type which will do this for you.
At the global level, such a function is again registered using the "encoder()" function. The differ- ence between this function and that for scalar types however, is that instead of returning a string giving the name of the scalar type, the value None should be returned in its place. The second value in the tuple should then be the instance of the structured type translated into either a scalar type, dictionary, tuple or list.
def _encode_UserList(object):
return (None,list(object))
netsvc.encoder(UserList.UserList,_encode_UserList)
Having returned the translated value, it will be represented to the encoder. Thus it is only necessary to translate the top level of the data structure as enclosed values will in turn be translated automatically if required and if an encoder is registered. This mechanism may also be used in an encoder specific to a service.
85
Message Encoding
86

Servlet Framework

The HTTP servlet framework can be used to providea window into your application. A number of pre­defined servlets are provided or you can create your own. You can also create your own server objects to map the servlets to appropriate parts of the URL namespace. Alternatively, a number of predefined server objects can be used for common tasks such as serving up files from the filesystem, or provision of RPC over HTTP services. Basic user authentication is implemented and clients can also be blocked based on their address.
The major classes in the OSE C++ class library involved in providing this functionality are the OTC_HttpDaemon, OTC_HttpServer and OTC_HttpServlet classes, plus the various de­rived servlet and server classes. The implementation of the HTTP servlet framework is based on the event system and multiple HTTP requests can be handled concurrently.
Although the framework is quite powerful, you should still keep in mind that its main purpose is for interacting with an application. If you are after a general purpose web server, you would probably be better off using a product like Apache. If you need the appearancethat the web site and application are one, use the "mod_proxy" plugin for Apache to redirect only a portion of the URL namespace. This actually has the added benefit that Apache can be used to setup a SSL connection with the client over anyinsecurenetwork, with communication between Apache andthe application on the secure network being normal HTTP.

Framework Overview

When a HTTP client makes a connection to the server process, a session manager is created to parse any requests made by that client. For each request, an attempt is made to find a server object which manages the part of the URL namespace that the request falls under. This server object is then asked
87
Servlet Framework
to provide a servlet to handle the actual request. If no server object is found corresponding to that por­tion of the URL namespace, or the server object is not able to provide a servlet to handle the request, a HTTP error response is returned to the client indicating that the resource corresponding to the sup­plied URL could not be found.
Where an appropriate servlet to handle the request is found, the session manager will initially pass off to the servlet the details of the request. This will include the type of request, the URL and the contents of any HTTP headers. The details initially provided to the servlet do not include any content associated with the request. Any content associated with a request will subsequently be passed to the servlet as it arrives. This will only occur though if the servlet wasn’t able to process the requestbased on the initial information and does actually require the content.
In the majorityof cases a request will not have any associated content and a servlet will be able toproc­ess the request straight away. Even if there is no content however, the servlet isn’t obligated to send a response immediately. This may be the situation if the servlet needs to wait until information from an­other source arrives before it can form the response. In this scenario, the servlet might send a request using themessaging framework to a remote service to obtain theinformation. When the responsefrom the remote service arrives, the servlet can then generate the response.
When the action of the servlet does depend on the content supplied with the request, the servlet would accumulate thecontent as it arrives until the amount of content matches that given in the content length header, or until someappropriate boundary is encountered. Now having all the content associated with the request, the servlet can process the request and send a response. Alternatively it could again delay the response if it needs to first send the content received to some remote service and wait for some re­sponse.
When a servlet sends a response to the HTTP client, as long as the servlet generates a content length in the HTTP headers, any request by a HTTP client to keep alive the session will be honoured. This allows the HTTP client to submit additional requests using the same connection if desired. In general the servlet framework adheres to the HTTP 1.0 protocol.

The HTTP Daemon

The Python class which listens for connection requests from HTTP clients is called HttpDaemon. When creating the HTTP daemon, you need to tell it which port to listen on and also register with it any HTTP server objects. When registering a HTTP server object, you need to identify which part of the URL namespace it manages. Finally, you need to start the daemon so that once the dispatcher is run it will actually listen and handle the requests.
dispatcher = netsvc.Dispatcher() dispatcher.monitor(signal.SIGINT)
daemon = netsvc.HttpDaemon(8000) filesrvr = netsvc.FileServer(os.getcwd()) daemon.attach("/",filesrvr)
88

The File Server

daemon.start()
dispatcher.run()
When a HTTP server object is registered, the first argument to the "attach()" member function should be the path under which resources made available by the HTTP server object are accessible. Except for the root directory, the path should not include a trailing "/". The path should also be in nor­malised form.That is,it should not include consecutive instancesof "/"within the path, or include the path components ".."or".".
If the path isn’t normalised in this respect, these paths will never match against any request as request URLs will always be normalised before attempting a match. The request URL is always normalised to avoid the possibility of malicious requests trying to access file type resources outside the available di­rectory tree.
If desired, a single HTTP server object may be registered multiple times within the one URL name­space. Registrations may also be done hierachically.That is, one registrationmay nest within the URL namespace of another. In this situation a request will match against the HTTP server object with the most deeply nested path.
filesrvr1 = netsvc.FileServer(os.path.join(os.getcwd(),"info")) filesrvr2 = netsvc.FileServer(os.path.join(os.getcwd(),"logs")) daemon.attach("/",filesrvr1) daemon.attach("/logs",filesrvr2)
Normally the port which the HTTP daemon is to listen on will be fixed. If you require a dynamically allocated port, you should use "0" as the port number. The actual port number which is allocated can then be queried using the "port()" member function. Obviously, this port number would then need to be displayed somewhere or otherwise accessible so it is known which port to connect to.
daemon = netsvc.HttpDaemon(0) port = daemon.port()
The File Server
The FileServer class is a predefined HTTP server object for serving up files from the file system in response to HTTP GET requests. This server object is suitable for providing access to documenta­tion related to an application, configuration files or application log files. A plugin mechanism for han­dling special file types is also included
In the case of files resident in the file system, the server is able to handle any size file, with the corre­sponding servlet only sending data back to the HTTP client as it is able to receive it. That is, transmis­sion of a large file will not blow out the size of the application nor will it causethe application to block if the client is slow at reading the contents of the file.
89
Servlet Framework
When an instance of the FileServer class is created, it must be supplied with the filesystem direc­tory from which files are to be served. The server object utilises the Python mimetypes module for determining file types. The file type associated with an extension can be overridden, or knowledge of additional file types can be added using the "map()" member function.
filesrvr = netsvc.FileServer("/home/httpd") filesrvr.map(".py","text/plain")
Note that it is expected that the HTTP client knows the name of the file it is trying to access as there is no builtin support included for directory browsing. It is however possible to define the namesof one or more index files to try when a request identifies a directory as opposed to a file. When more than one index file is specified, those which were declared later, take precedence.
filesrvr.index("index.htm") filesrvr.index("index.html")
Editor backup files, temporary files generatedby an application, or any other files which should not in any way be accessible from a HTTP client, can be hidden from view so long as they have a distinct extension.
filesrvr.hide(".bak") filesrvr.hide(".html~")
When it comes to the actual task of serving up a single file from the file system, the FileServlet class is used. This is a wrapper around the corresponding servlet class from the OSE C++ class library used to handle the request for a single file. The servlet may be used directly from a custom HTTP serv­er object.

Client Authorisation

If the HTTP servlet framework is being used to provide an administrative interface into an application, it may be desirable to block access from all but a few selected client hosts. This can be useful where the application is otherwise intentionally accessible overthe Internet, or may inadvertantly become ac­cessible from a broader range of hosts than intended. This may result from misconfigured firewalls, or the addition of additional subnets to a corporate network.
Ifyou wish to control who can access the application through the port monitored by the HTTP daemon, it is necessary to create a derived version of the HttpDaemon class and override the "author- ise()" member function. For each client connection, this member function will be calledwith the IP address of the client host. Your code can thereby block requests from any undesirable hosts.
class HttpDaemon(netsvc.HttpDaemon):
def __init__(self,port,hosts=[]):
netsvc.HttpDaemon.__init__(self,port) self._allow = hosts
def authorise(self,host):
return host in self._allow
90

User Authorisation

If access to a particular client is disallowed, the connection will be dropped immediately. The client will not receive any form of specific HTTP error response indicating why the connection has been closed. Note that this mechanism blocks a client from accessing any part of the URL namespace for that HTTP daemon. If you wish to only block client access to specific resources, you would need to customise each HTTPserver object or servlet, or use multiple HTTP daemon objects on separate ports.
User Authorisation
A further level of authorisation beyond that of blocking specific client hosts is to individually authen­ticate each user. The mechanism for user authentication isperformed against the HTTPserverobjects. That is, the URL namespace managed by each HTTP server component can be individually protected using different user databases.
To add user authentication to a particular HTTP server object, you should derive from the class and override the "authorise()" member function. If building your own HTTP serverobject, you could embed the member function directly in your class.
class FileServer(netsvc.FileServer):
def __init__(self,directory,users={}):
netsvc.FileServer.__init__(self,directory) self._allow = users
def authorise(self,login,password):
return self._allow.has_key(login) and \
self._allow[login] == password
If you need to controluser access at the level of individual URLs within the URL namespace managed by a particularHTTP server object, that functionality would need to be embedded into any servlets cre­ated by that HTTPserver object, or managed at the point that the servlets are createdby the HTTPserv­er object. Note that only the HTTP basic authentication mechanism is supported. There is no support for use of secure sockets and SSL.

HTTP Server Objects

When a HTTP request is received, it is a HTTP server object whichwill dictatethe type of HTTP serv­let created to handle the request. If you wish to implement a customised mapping between request URLs and the available HTTP servlets, or introduce a new type of HTTP servlet, you will need to de­fine your own HTTP server object by deriving from the HttpServer class and overriding the "servlet()" member function.
class HttpServer(netsvc.HttpServer):
def servlet(self,session):
servletPath = session.servletPath() if servletPath == "echo":
return netsvc.EchoServlet(session)
elif servletPath == "motd":
return netsvc.FileServlet(session,"/etc/motd","text/plain")
91
Servlet Framework
return netsvc.ErrorServlet(404)
daemon = netsvc.HttpDaemon(8000) server = HttpServer() daemon.attach("/test",server) daemon.start()
The job of the "servlet()" member function is to create an instance of a HTTP servlet capable of handling a request made against a specific URL. When the "servlet()" member function is called it is suppliedwith the HTTP session object. The session object provides accessto details of the request, including the server root and servlet path. The server root corresponds to the path under which the HTTP server object was registered. The servlet path is the remainder of the path expressed relative to that server root.
As an example, if the request used the path "/test/echo" and the HTTP server object was regis­teredwith the path "/test", the server root wouldbe "/test" and the servlet path wouldbe "echo". In the case that a HTTP server object i s registered with path "/", the server root will still be "/". This is the only case where the trailing "/" isn’t removed.
Under normal circumstances the HTTP server object would determine the type of HTTP servlet to cre­ate and the resource being referencedbased only on theservlet path.If necessary however, it canquery other information related to a request. Sucha circumstance might be to look for thepresenceof cookies used to implement a user session mechanism.
When a HTTP servlet is created, it will need to be passed the handle to the HTTP session object. All the predefined HTTP servlets accept this as the first argument when the servlet is created. If you are defining your own servlets, it is recommended you follow this convention.
If the HTTP server object isn’t able to map a request to a particular type of HTTP servlet, the "serv- let()" member function should return "None", or should indicate a specific type of HTTP error re­sponseusing the ErrorServlet class. A HTTP client canbe redirected to a different resource using the RedirectServlet class.

The Error Servlet

The error servlet as implemented by the ErrorServlet class is provided as a quick way for a cus­tom HTTP server object to return a HTTP error response. In addition to the the HTTP session object, theerror servlet needs to be supplied with an approriate HTTP error response code. Text to be included in the body of the response can also be provided if desired. Such text may include any relevant HTML markup, but should not include the opening and closing "body"tags.
class HttpServer(netsvc.HttpServer):
def servlet(self,session):
return netsvc.ErrorServlet(session,501,"Not implemented.")
92

The Redirect Servlet

The Redirect Servlet
The redirect servlet as implemented by the RedirectServlet class would be used when it is nec­essary to redirect a HTTP client to an alternate resource. In addition to the HTTP session object, it should be supplied the URI of the resource to which the HTTP client is to be directed. By default, the HTTP response code will be "302", indicating the resource has been temporarily moved. This can be explicitly indicated by using the value "REDIRECT_TEMPORARY". If the resource has been perma­nently moved, the value "REDIRECT_PERMANENT" can instead be used.
class HttpServer(netsvc.HttpServer):
def servlet(self,session):
url = "http://hostname/" + session.servletPath() type = netsvc.REDIRECT_PERMANENT return netsvc.RedirectServlet(session,url,type)
If the URI doesn’t start with "/", it is assumed to be a valid URI and will be passed as is. If the URI starts with "/", it will assumed to be a absolute URL against the current server host and will be auto­matically adjusted to include the details of the server host in the URL.

The Echo Servlet

The echo servlet as implemented by the EchoServlet class is useful for debugging. When used to service a HTTP request, it will generate a HTML document which provides details about the request.
class HttpServer(netsvc.HttpServer):
def servlet(self,session):
return netsvc.EchoServlet(session)

The File Servlet

The file servlet as implemented by the FileServlet class, is used to deliver up to a HTTP client the contents of a file stored in the operating system’s filesystem. This is the same servlet which is used internal to the FileServer class. When this servlet is being created it needs to be supplied with the name of the file and the file type. The latter corresponds to the MIME content type included in the HTTP response.
Ifthe path suppliedto the FileServlet classactuallydescribes a directory,the servlet willgenerate a response indicating that access is forbidden. If you wish to implement directory browsing you will need to implement a separate HTTP servlet to generate an appropriate response and map the request to it. If you want to redirect the request to an index file, your HTTP server should determine if such an index file exists and if it does, create the file servlet against it instead.
When the FileServlet class isused, any size file can be handled without the size of the application growing in size and without the application blocking as a resultof a slowHTTP client. Thisis achieved as a result of the file being sent in blocks, with the servlet waiting if the connection to the HTTP client
93
Servlet Framework
becomes congested. Although the servlet may be forced to wait beforeit can send more data,any other jobs in the event system will still be serviced, including other HTTP requests.

Logging of Requests

By default no information is logged about requests. If you wish to log what requests are being made against your application using the HTTP servlet framework, you need to set the environment variable "OTCLIB_HTTPLOGCHANNEL" to the name of the log channel to record the information on. The en­vironment variable needs to be set prior to the first request being received by the application through any instance of the HttpDaemon class.
dispatcher = netsvc.Dispatcher() dispatcher.monitor(signal.SIGINT)
netsvc.mergeEnviron("OTCLIB_HTTPLOGCHANNEL","")
daemon = netsvc.HttpDaemon(8000) filesrvr = netsvc.FileServer(os.getcwd()) daemon.attach("/",filesrvr) daemon.start()
dispatcher.run()
The format of the logged messages is the same as Apache web server common log file format except that no matter what version of HTTP is used, the url component of the request is always expanded to itscomplete form. Thatis, it will be prefixed with "http://hostname:port" as appropriate. Nor­mally this would only be the case if the request originated with a client supporting HTTP/1.1 protocol and a full url had been supplied by the client.
If you do not want information about requests appearingin the default log file, but want to split out the logged messages into a distinct log file, or otherwise treat them in a special way, use a hidden log chan­nel and create a user defined log channel to capture them.
94

Servlet Objects

To make the most of the HTTP servlet framework it will be necessary to create your own servlets for interacting with your application. Servlets can be written to handle basic requests against a resource, or requests where form data is supplied. Special purpose servlets which process arbitrary content as­sociated with a request may also be created. Having created a servlet, it can be integrated into an ap­plication by defining a custom HTTP server object, or by storing it as a file and using a plugin, in association with the file server object.
As the HTTP servlet framework is implemented on top of an event system and doesn’t rely upon threads, it is necessary to be mindful of how servlets are implemented to avoid a situation where the code blocks. If the code does block it will effectively stop the whole application. The event system should therefore be used as appropriate where concurrency is required. If communication with service objects in a remote process is required to obtain data to satisfy a request, this will be essential.

Processing a Request

In order to implement your own HTTP servlet, you need to create a new class which derives from the HttpServlet class. If the request your HTTP servlet is to handle does not have any content associ­ated with it, you will only need to override the "processRequest()" member function.
The "processRequest()" member func tion will be called immediately after the HTTP server ob­ject has returned a valid HTTP servlet. If the HTTP servlet doesn’t need to process any content asso­ciated with a request, it will typically be able to generate a response straight away and the servlet can then be destroyed.
class HttpServlet(netsvc.HttpServlet):
def processRequest(self):
95
Servlet Objects
if self.requestMethod() != "GET":
self.sendError(400)
else:
self.sendResponse(200) self.sendHeader("Content-Type","text/plain") self.endHeaders() self.sendContent("Hi there.") self.endContent()
The major member functions of the HTTP servlet class used to interrogate the details of the HTTP re­quest are "requestMethod()", "requestPath()" and "queryString()". It is these meth­ods you would use to determine what the HTTP servlet is to do. It may be the case however that it is only necessary to validate the type of request method. This would occur where the HTTP server object had already identified a resource against which a request was being made and supplied the handle for that resource when the HTTP servlet was created.
Having determined the validity or otherwise of a request, the HTTP servlet can do a number of things. In the event of an error the HTTP servlet can use the "sendError()" member function to generate an error response. The first argument to "sendError()" should be the appropriate HTTP response code. Anoptional second argumentmay also be supplied consisting of valid HTML text. This text will be included within the body of the HTML document generated by the "sendError()" member function.
If the request is valid, the HTTP servlet might instead generate its own response including any appro­priate content. To start the response the "sendResponse()" member function must be called. The first argument to "sendResponse()" would typically be be "200", indicating a successful re­sponse. A HTTP servlet may if it wishes supply any valid HTTP response code here. In fact, the "sendError()"member function is merely ashorthand method forgenerating an errorresponse and underneath actually uses the same functions as described here.
The HTTPservlet may now include any HTTP headers by calling "sendHeader()". The arguments to"sendHeader()" shouldbe the nameof the header and its string value.Whether or not any HTTP headers are included, the member function "endHeaders()" must now be called.
To include content in a response the "sendContent()" member function is used. This may be calledmultipletimes. When all content has been sent, the "endContent()"member function should be called. Calling the "endContent()" member function will have the affect of closing off the re­sponse and once the "processRequest()" member function returns, the servlet will able to be de­stroyed.

Persistent Connections

A persistent connection is one whereby the connection to the client canbe maintained after a response has been sent. This allows a HTTP client to submit additional requests without the need to create a new
96

Delaying a Response

connection. Negotiation of persistent connections between the HTTP client and server is managed by using special HTTP request and response headers.
Where possible the session manager will undertake to maintain persistent connections without you needing to take any special actions. This is done as a result of the session manager inserting on your behalf the special headers as appropriate when you call the "endHeaders()" member function.
If the client has requested a persistent connection and supplied a valid content length in the request headers, and you include a valid content length header in the response headers, the session manager will aim to maintain the connection. If you do not include a valid content length header in the response headers, or "sendError()" was used to generate a response, the connection will always be shut­down.
Note that when a HTTP client does send an additional request over the same connection, it will not be the same HTTP servlet instance that handles the request. Each request received will always be sepa­rately parsed, with the appropriate HTTP server object and servlet used each time.
Delaying a Response
The servlet framework is implemented on top of the event system. As a result, it is not mandatory that a complete response be generated by the "processRequest()" member function. Instead, the servlet could execute some action which would result in a callback at a later point in time. When that callback occurs, then it might complete the response.
class HttpServlet(netsvc.HttpServlet,netsvc.Agent):
def __init__(self,session):
netsvc.HttpServlet.__init__(self,session) netsvc.Agent.__init__(self)
def processRequest(self):
if self.requestMethod() != "GET":
self.sendError(400)
else:
self.sendResponse(200) self.sendHeader("Content-Type","text/plain") self.endHeaders() self.startTimer(self.completeResponse,10,"timeout")
def completeResponse(self,tag):
self.sendContent("Hi there.") self.endContent()
This is useful where the servlet needs to wait until data needed to formulate a response is available or where some form of time dependent server push mechanismis being implemented. Note however that special steps may be required in these situations to cope with a HTTP client prematurely closing the connection.
97
Servlet Objects

Destruction of Servlets

The destruction of a servlet can come about as a result of two situations. The first situation is where a servlet handles a requests and generates a response, whether that be successful or otherwise. The sec­ond situation is where the HTTP client closes the connection before the servlet has sent a complete response.
The fact that the actions of a servlet may need to be aborted before it has finished complicate the de­structionof aservlet. This is because anycallback whichmay have beenset up will result in a reference count against the servlet object. The existance of such references will actually prevent the immediate destruction of the servlet object. If that reference is never deleted, the servlet object may never be de­stroyed.
Allthis means that it isn’t sufficient for the servlet frameworkto delete its own reference to aninstance of a HTTP servlet and expect that it will be destroyed. Instead, it is necessary to introduce a special member function to t he HttpServlet class and require that any derived class extend it as appropri­ate to cancel any callbacks or otherwise cause external or circular references to the servlet to be delet­ed.
The name of this member function is "destroyServlet()". The member function will be called when a HTTP client prematurely closes the connection. So that only one mechanism is employed to ensure a servlet is destroyed, the member function is also called subsequent to a servlet generating a complete response.
class HttpServlet(netsvc.HttpServlet,netsvc.Agent):
def __init__(self,session):
netsvc.HttpServlet.__init__(self,session) netsvc.Agent.__init__(self)
def processRequest(self):
if self.requestMethod() != "GET":
self.sendError(400)
else:
self.sendResponse(200) self.sendHeader("Content-Type","text/plain") self.endHeaders() self.startTimer(self.completeResponse,10,"timeout")
def completeResponse(self,tag):
self.sendContent("Hi there.") self.endContent()
def destroyServlet(self):
netsvc.HttpServlet.destroyServlet(self) netsvc.Agent.destroyReferences(self)
The first action of the derived version of the member function "destroyServlet()" should be to call the base class version of the function in the HttpServlet class. The member function should then do what is ever necessary to ensure that references to the servlet are deleted. If the servlet had
98

Processing Content

been derived from the Agent class, this would include calling the "destroyReferences()" member function.
Processing Con tent
Ifthe function of a HTTP servlet entails that the content associated with a request be processed in some way, it will be necessary to override the "processContent()" member function. The"process- Content()" member function will only be called subsequent to "processRequest()"being called, and only provided that "processRequest()" hadn’t already dealt with the request and sent a complete response.
As the means to determine how much content to expect is dependent on the specifics of a request, no attempt is made to first accumulate the content into one block. Instead, the "processContent()" member function will be called multiple times if appropriate, once for each block of data which is read in. It is up to the "processContent()" member function to accumulate the data or otherwise proc­ess it, until it determines that all content has been received.
Typically, how much content is expected will be dictated by the presence of a HTTP content length header, or by a MIME multipart messageboundary string as specificedin a HTTP content type header. Either way, it is up to the specific implementation of a HTTP servlet to know what to expect and deal with it appropriately.
class FormServlet(netsvc.HttpServlet):
def processRequest(self):
if self.requestMethod() not in ["GET","POST"]:
self.sendError(501,"Request method type is not supported.")
elif self.requestMethod() == "POST" \
and self.contentLength() < 0:
self.sendError(400,"Content length required for POST.")
elif self.requestMethod() == "GET":
self._environ = {} self._content = [] self._contentLength = 0 self._headers = self.headers() self._environ["REQUEST_METHOD"] = self.requestMethod() self._environ["QUERY_STRING"] = self.queryString() self._headers["content-type"] = \
"application/x-www-form-urlencoded"
try:
form = cgi.FieldStorage(headers=self._headers, \
environ=self._environ,keep_blank_values=1)
self.processForm(form)
except:
netsvc.logException() self.shutdown()
elif self.contentLength() == 0:
self.processContent("")
99
Servlet Objects
def processContent(self,content):
self._content.append(content) self._contentLength = self._contentLength + len(content) if self._contentLength >= self.contentLength():
self._environ["REQUEST_METHOD"] = self.requestMethod() self._content = string.join(self._content,"") self._content = self._content[:self.contentLength()] fp = StringIO.StringIO(self._content) try:
form = cgi.FieldStorage(headers=self._headers, \
environ=self._environ,keep_blank_values=1,fp=fp)
self.processForm(form)
except:
netsvc.logException() self.shutdown()
def processForm(self,form):
self.sendResponse(501)
Member functions which a HTTP servlet may find useful here are "contentLength()"and "con- tentType()". The "contentLength()" member function returns an integer value correspond-
ing to that defined by the HTTP content length header, or "-1" if no such field was provided. The "contentType()" member function returns the HTTP content type header. Note that this will in­clude any supplied parameters so you will need to extract these yourself.
A HTTP servlet may also interrogate arbitrary headers using the member functions "contains- Header()" and "header()". These respectively indicate if a header existsand return its value. The name of a header should always be given as a lower case string. All headers may be obtained as a Py­thon dictionary using "headers()".
If a HTTP servlet encounters an internal error at any time, it may call the "shutdown()" member function to abort all processing of the request. This will cause the connection to the HTTP client to be closed immediately, discarding any data which hadn’t yet been sent. The instance of the HTTP servlet will then subsequently be destroyed.

The Form S ervlet

As processing of form data will be a common situation, an implementation of a form servlet is provid­ed.ThisiscalledFormServlet. The implementation of this servlet is similar to the previous exam­ple except that it does additional processing to translate data from the types used by the FieldStorage class into standard Python lists and dictionaries. The name of the member function which you need to override to process the form is "handleRequest()".
class LoginServlet(netsvc.FormServlet):
def handleRequest(self):
if self.containsField("user") and \
self.containsField("password"):
user = self.field("user")
100
Loading...