Persistente Objekte und Workflow-Managment mit Paos

von Carlos Maltzahn


Paos ist ein System zur netzwerkweiten und konsistenten Verwaltung von Python-Objekten. Carlos führt uns in der heutigen Folge der Python Tools in die Hintergründe dieses interessanten Werkzeugs ein. Als größere Anwendung wird zudem das damit entwickelte Workflow-Managment System Chautauqua vorgestellt und belegt die Einsatmöglichkeiten von Python in realen Anwendungen.


Der Python Modul shelve unterstützt das Abspeichern von Python Objekten in eine Datei. Nach dem Öffnen eines shelve files können beliebige Objekte mit einem Namen eingetragen werden:

>>> import shelve
>>> db = shelve.open('database')
>>> db['first object'] = [1, 2, 3]
>>> db['second object'] = ('hallo', [])
>>> db['first object']
[1, 2, 3]
>>> db.close()
Wenn nun diese Interpreter-Sitzung beendet wird, bleiben die so eingetragenen Objekte erhalten. Beim nächsten Aufruf von Python können diese Objekte mittels shelve.open('database') wieder geladen werden, d.h. diese Objekte sind persistent. Die Implementierung von shelve kann zur Compile-Zeit des Python Interpreters konfiguriert werden. Es stehen verschiedene Datenbank C-Bibliotheken zur Verfügung, z.B. dbm, gdbm und bsddb. Diese Bibliotheken implementieren Datenstrukturen, die einen schnellen Zugriff auf die abgespeicherten Daten ermöglichen. In zwei wichtigen Punkten bietet shelve jedoch keine Unterstützung: Es implementiert keine parallele Zugriffskontrolle, d.h. wenn mehrere Prozesse auf persistente Objekte der gleichen shelve Datei zugreifen, kann die Datei inkonsistent werden. Außerdem stellt shelve keine Anfragesprache zur Verfügung.

Paos (Python Active Object Server) baut auf shelve auf und implementiert eine Client/Server Architektur mit paralleler Zugriffskontrolle und einer einfachen Anfragesprache.

Darüberhinaus stellt Paos einen Notifikations-Service zu Verfügung, durch den sich Python Anwendungen von dem Server über bestimmte Bedingungen benachrichtigen lassen können. Diese Bedingungen werden von den Anwendungen in Form von Anfragen definiert und bei dem Notifikations Service im Server registriert. Jedesmal wenn eine Anwendung etwas bei dem Server abspeichert, wendet der Server die registrierten Anfragen auf die neu abgespeicherten Objekte an. Wenn eine Antwort auf eine Anfrage nicht leer ist, sendet der Server die Antwort zu der Anwendung, die die Anfrage registriert hat.

Ein Beispiel und etwas mehr (oder zuviele?) Details

Das folgende Beispiel illustriert wie eine Anwendung mit dem Server eine Verbindung aufbaut, eine Anfrage stellt und auf Attribute eines geladenen, persistenten Objektes zugreift. Wir gehen davon aus, daß ein Paos Server auf der Maschine cheesy.cs.colorado.edu läuft und auf Anfragen auf dem Port 5000 wartet. Das Beispiel erzeugt einige Objekte von der Klasse Person, speichert sie ab und führt eine Anfrage aus.

import Client
import ExampleSchema

# Baue Verbindung mit dem Paos Server auf
conn = Client.Connection('cheesy.cs.colorado.edu', 5000, 'example')
 
# Erzeuge Objekte 
john = ExampleSchema.Person()
john.name = 'John'
sue = ExampleSchema.Person()
sue.name = 'Sue'
john.loves = sue
bill = ExampleSchema.Person()
bill.name = 'Bill'
sue.loves = bill
bill.loves = sue

# Registriere Objekte bei dem Server
conn.register_objs([john, sue, bill])

# Speichere Objekte ab
conn.commit([john, sue, bill])

# Hole alle Instanzen von 'Person', die in Sue verliebt sind
answer = conn.get('r', 'Person', [('loves', '==', sue)])

# Für jedes Objekt in der Antwort drucke den Namen der Geliebten aus.
for obj in answer:
  if obj.hasattr('sibling'):
    print obj.name, obj.sibling.name
Zunächst importieren wir den Modul Client, um eine Verbindung mit dem Paos Server aufbauen zu können. Danach importieren wir den Modul ExampleSchema, welches die Klasse Person definiert (siehe weiter unten). Schließlich bauen wir eine Verbindung auf, indem wir die Klasse der Verbindungen instanzieren. Dabei geben wir den Host-Namen und den Port an, auf dem der Paos Server läuft. Im dritten Argument kann ein beliebiger Name für die Anwendung eingetragen werden. Dieser Name taucht dann in den entsprechenden Log-Einträgen des Servers auf.

Wir erzeugen dann drei Person -Instanzen und weisen ihnen Attributwerte zu. Bevor wir diese Objekte abspeichern können, müssen sie erst bei dem Server registriert werden. Die Registrierung weist den neuen Objekten eine eindeutige Datenbanknummer zu. Dies kommt uns in der folgenden Anfrage zugute, die der Abspeicherung folgt: "Gib mir alle Objekte aus der Klasse Person, die das Objekt sue lieben." Wenn sue nicht registriert worden wäre, könnte der Server dieses Objekt nicht mit den abgespeicherten Objekten vergleichen.

Das erste Argument 'r' in der Anfrage bedeutet, daß die Objekte in der Antwort von der Anwendung nur gelesen werden. Wenn wir Objekte modifizieren möchten, dann müssen wir entweder in der Anfrage statt 'r' das Argument 'rw' angeben, oder die Schreibrechte für die zu manipulierenden Objekte nachträglich mit der Methode conn.lock erwerben. Wir können die Schreibrechte nur dann erwerben, wenn kein anderer die Schreibrechte besitzt. Wenn wir die Schreibrechte besitzen, kann kein anderer die korrespondierenden Objekte modifizieren. Bei jedem conn.commit und bei Programmabbruch verlieren wir alle erworbenen Schreibrechte.

Die Anfrage liefert uns eine Liste mit zwei neuen Objekten, welche equivalent zu john und bill sind. In der darauf folgenden Schleife drucken wir den Namen der jeweiligen Geliebten aus (beidemal 'Sue'). Diese harmlos aussehende Schleife hat es allerdings in sich: Der Client-Modul stellt sicher, daß sue, john.loves und bill.loves auf das gleiche Objekt zeigen. Ermöglicht ist dies durch die Registrierung von sue und eines Resolutionsprozesses, der in den Attributzugriff von john.loves und bill.loves eingebaut ist. Implementiert ist dieser Resolutionsprozeß über die eingebaute Python Methode __getattr__, die die Umdefinierung von Attributzugriffen erlaubt. Dieser erweiterte Attributzugriff sorgt außerdem für das dynamische Laden von Objekten, die noch nicht in der Client-Anwendung existieren (die Implementierung von dem Attributzugriff ist in Schema.py in der Klasse DBobject definiert. Die Methode register_objs der Klasse Connection in Client.py installiert diesen Attributzugriff für jedes Object in der Argumentliste).

Es ist wichtig zu verstehen, daß dieser Resolutionsprozeß nur für die referentielle Konsistenz von registrierten Objekten untereinander sorgen kann. In dem obigen Beispiel zeigen die Variablen john und bill auf Objekte, welche nicht in der Antwort enthalten sind. Es liegt hier in der Verantwortung des Programmierers zu erkennen, wann Variablen auf veraltete Objekte zeigen. Mit einem einfachem Trick können Variablen "aufgefrischt" werden: john = conn.cache[john.db_id]. Dazu ist es nötig zu wissen, daß das Connection-Objekt einen Cache für geladene Objekte verwaltet und auf diesen Cache über die Datenbanknummern der registrierten Objekte zugreift. Jedes regestrierte Objekt besitzt das Attribut "db_id mit einer eindeutige Datenbanknummer. Der Cache enthält die jeweils zuletztgeladene Version von allen geladenen Objekten.

Paos interessanteste Eigenschaft ist jedoch der Notifikations-Service. Das folgende Beispiel zeigt, wie dieser Dienst verwendet wird:

import Client
import ExampleSchema
import Utilities
import os
import pickle

# Definiere eine Pipe für Notifikationen 
(read_pipe_fd, write_pipe_fd) = os.pipe()

# Baue Verbindung mit dem Server auf 
conn = Client.Connection('cheesy.cs.colorado.edu', 5000,
                         'example', (read_pipe_fd, write_pipe_fd))

# Registriere Anfrage bei dem Notifikations Service
request_id = conn.register('Person', [('name', '==', 'Sue')])

while 1:

  # Warte auf eine Notifikation und lese sie ein
  data = Utilities.READ(read_pipe_fd, 10000)

  # Packe Notifikation aus
  (req_id, obj_list, other_client) = pickle.loads(data)

  # Packe Identifikation von anderem Client aus
  (other_host, other_pid, other_uid, other_name) = other_client

  # Mache irgendwas damit
Im Vergleich zu dem ersten Beispiel müssen drei zusätzliche Module geladen werden: Utilities ist ein Modul mit Hilfsprozeduren, die in allen Paos Modulen verwendet werden. os und pickle sind eingebaute Python Module, die Betriebssystem Funktionen und Funktionen für die Umwandlung von Objekten in einen String (Serialisierung) zur Verfügung stellen.

Zunächst definieren wir eine Pipe, die uns später als Empfänger für Notifikationen dienen wird. Wir bauen dann eine Verbindung zu dem Paos Server auf. Der Aufruf der Connection-Funktion hat die Pipe als viertes Argument, so daß die Pipe mit dem Verbindungsobjekt assoziert werden kann. Dann registrieren wir eine Anfrage, die dafür sorgt, daß der Server uns alle neuen Person-Objekte mit dem Namen 'Sue' schickt, sobald diese Objekte in die Datenbank neu eingetragen werden. Der conn.register(...) Aufruf liefert eine Registrierungsnummer zurück.

Die Notifikattionen erhalten wir über die Pipe. Wir benutzen dafür eine Hilfsprozedur, die garantiert, daß die volle Länge der Notifikation von der Pipe gelesen wird. Die Notifikation wird als String über das Netz geschickt und muß bei der Empfängerseite wieder in Python Objekt umgewandelt werden. Dies geschieht mithilfe des Aufrufs pickle.loads(data). Eine Notifikation besteht aus einem Tripel, welches die

enhält. Diese Identifikation wiederum besteht aus

Chautauqua: Eine Größere Anwendung mit Paos

Paos ist ein "spin off" Produkt von einem Workflow Forschungsprojekt. Eines der Ergebnisse dieses Projekts ist das experimentelle Workflow System Chautauqua. Paos stellt mit dem Notifikations Service die Kommunikations-Infrastruktur für die verschiedenen Chautauqua Systemkomponenten zur Verfügung. Chautauqua Benutzer interagieren mit dem System über einen Graph-Editor und einen Web Browser. Der Web Browser zeigt dynamisch generierte "To-do" Listen für jeden Mitarbeiter an, und wird für das Ausfüllen von Formularen verwendet. Der Graph-Editor zeigt die Struktur eines Büroprozesses und den Zustand von verschieden Arbeitsvorgängen an. Speichert z.B. ein Büromitarbeiter den Inhalt einer Form in Paos ab, notifiziert Paos den Chautauqua Workflow Manager, welcher die Form zu dem nächsten Büromitarbeiter delegiert. Dieser Vorgang kann jeder Benutzer auf seinem/ihrem Graph Editor verfolgen, da jeder Editor die entsprechenden Notifikationen erhält und sofort in graphische Representationen umwandelt.

Das Besondere an Chautauqua ist, daß es den Benutzern ermöglicht, die Struktur der Büroprozesse während ablaufender Arbeitsvorgänge zu verändern. In dem folgenden Schnappschuß sehen wir die Struktur eines Büroprozesses:

Mitarbeiter werden durch Sterne, Bürorollen durch Quadrate, und Aktivitäten durch Kreise und Dreiecke dargestellt. Kleine Punkte rechts oberhalb von Aktivitäten stellen "Token" dar, die den Zustand der Arbeit repräsentieren, und die mit dem Fortschritt der Arbeit durch den Graphen wandern. Mit dem Editor ist es nun möglich, einen beliebigen Teil des Graphen zu verändern. Wenn Aktivitäten gelöscht werden, können Token ihren Standort verlieren. Chautauqua bietet Mechanismen an, diese verlorenen Token aufzusammeln und ihnen neue Standorte in dem veränderten Graphen zuzuweisen.

Infos

Paos und Chautauqua sind vollständig in Python programmiert und frei erhältlich unter:

ftp://ftp.cs.colorado.edu/users/carlosm/paos-1.4.tar.gz

ftp://ftp.cs.colorado.edu/users/carlosm/chautauqua-1.4.tar.gz (Chautauqua enthaelt Paos)

Detailliertere Dokumentation für Paos und Chautauqua ist zur Zeit in Vorbereitung und wird in der Newsgruppe comp.lang.python angekündigt.


Carlos Maltzahn ist zur Zeit Computer Science Student im Ph.D. Programm der University of Colorado in Boulder. Seine Forschungsinteressen konzentrieren sich momentan auf Internet Caches und Verteiltes Indizieren. In der Freizeit treibt er sich entweder irgendwo in den traumhaft schönen Rocky Mountains herum oder verbringt seine Zeit damit, mobile Roboter aus FischerTechnik zu bauen. Zu erreichen ist er unter carlosm@cs.colorado.edu

Copyright © Linux Magazin