Session D-WERK

Netzwerkprogrammierung nicht nur für Netzwerkeinsatz

Sebastian Flucke
ASCI CONSULTING GmbH


Vorbemerkung

Die Session D-WERK setzt die Kenntnis der Grundbegriffe von Netzwerkprogrammierung mit Visual FoxPro voraus (siehe insbesondere Session D-NETZ).

Ausgehend von den programmtechnischen Möglichkeiten mit Visual FoxPro werden Strategien für die direkt und indirekt mit der Netzwerkprogrammierung verbundenen Problemfelder aufgezeigt sowie Lösungen demonstriert.
Viele Aspekte sind dabei allerdings durchaus auch für Single-User-Standalone-Applikationen interessant!

Multi-user, multi-session, multi-tasking, multi-threading, multi-instancing, multi-tier, multi....

Keine Panik vor der "Multi"-Manie <s>!

Folgende "Multi"-Begriffe sind nicht Gegenstand dieser Session:

  • Multi-Tier-Development, dazu siehe Sessions D-TIER und E-TIER
  • Multi-Threading, dazu siehe Sessions D-MTS, D-COM, E-DCOM und E-TRANS

Dagegen werden die Begriffe Multi-User, Multi-Session / Multi-Tasking und Multi-Instancing im weiteren näher erläutert und untersetzt.

Multi-User-Applikationen

Multi-User-Applikationen sind Applikationen, die in einem Netzwerk betrieben werden und von mehreren Anwendern gleichzeitig genutzt werden können.

Bei der Programmierung solcher Applikationen sind die folgenden multi-user-spezifischen Probleme zu lösen:

  1. Umgang mit Konflikten, die aus gleichzeitiger Änderung eines Datensatzes von mehreren Arbeitsstationen aus herrühren
  2. Sicherung der Konsistenz von Auswertungen gegenüber Datenänderungen, die während der Auswertung von anderen Arbeitsstationen aus vorgenommen werden
  3. Erlaubnis / Verbot der Gleichzeitigkeit bestimmter einander ausschließender Aktivitäten von verschiedenen Arbeitsstationen aus (z.B. zeitgleiches Ermitteln der nächsten freien Kundennummer, weil sonst Doppelvergabe möglich wäre; Reorganisation während noch andere Arbeitsstationen "normal" arbeiten usw.)
  4. Trennung von Daten und Programm, Organisation des Zugriffs auf temporäre Verzeichnisse, Umgang mit temporären Dateien usw.

Diese vier Problemkreise bestehen unabhängig davon, ob die betreffende Applikation file-server-orientiert oder client/server-orientiert programmiert ist (siehe dazu die Sessions der Sektionen C/S und TIER).

Das Multi-Tasking- / Multi-Session-Problem

Alle aktuellen Windows-Betriebssysteme sind Multi-Tasking-Systeme, d.h. es können mehrere Tasks quasi parallel ablaufen, indem sie sich die Prozessorzeit in sehr kleinen Scheibchen ständig teilen.

Damit besteht allerdings auch die Möglichkeit, unter einem Windows-System die gleiche Applikation mehrfach als Sessions in verschiedenen Tasks zu starten - und schon hat man die Problemkreise (1) bis (4) auf dem Tisch, obwohl in dem konkreten PC vielleicht nicht einmal eine Netzwerk-Karte vorhanden ist.

Ergo: Die Multi-User-Probleme sind unter Windows auch gleichzeitig Multi-Tasking-Probleme.

Multi-Instancing von Masken

Jede echte Windows-Applikation bietet neben der normalen Arbeit mit nicht-modalen Masken außerdem die Möglichkeit der Mehrfach-Instanzierung von Datenerfassungs-Masken (Forms).

Dadurch ergeben sich unbestreitbare Vorteile bezüglich der Benutzeroberfläche. Z.B. muß die Erfassung eines neuen Kunden nicht abgebrochen werden, nur um die Telefonnummer eines anderen Kunden vom System abfragen zu können. Man startet die Kundenmaske einfach ein zweites Mal und schon kann die gewünschte Auskunft erlangt werden.

Mit der Mehrfach-Instanzierung von Masken hat man allerdings schlagartig die Problemkreise (1), (4) und teilweise auch (3) schon innerhalb einer einzigen Session einer Applikation auf dem Tisch! Lediglich der Problemkreis (2) spielt bei Multi-Instancing keine Rolle, da es innerhalb einer einzelnen VFP-Session keine Parallel-Verarbeitung gibt.

Was tun, sprach Zeus...

Bei der Entscheidung, welche Art von Applikation man programmieren sollte, spielen folgende Dinge eine entscheidende Rolle:

  • Da man als Berufs-Programmierer mit Sicherheit in der Lage sein muß, netzwerkfähige Applikationen zu erstellen, wäre es sozusagen zusätzlicher Programmier- und Pflege-Aufwand, auch noch eine "nicht-netzwerktaugliche" Variante vorzuhalten.
  • Auch ohne die echte Netzwerktauglichkeit finden sich diverse Probleme schon bei der Multi-Session- und Multi-Instancing-Programmierung wieder.
  • Ein windows-konformes Nutzer-Interface mit multi-instanzierbaren Masken ist ein nicht zu unterschätzendes Verkaufsargument für eine Applikation.

Also lautet nun der Schluß...

...daß das "Multi" sein muß!

Die folgenden Empfehlungen verhindern unnötigen Aufwand, der aus der Pflege mehrerer Varianten resultieren würde, und bieten ein leicht nutzbares Erweiterungspotential.

Denn irgendwann kommt der Kunde ja doch und will mindestens eines der "Multis" mal eben schnell umgesetzt haben - und wenn man dann klug vorgebaut hat...:

  • Dateneingabe-Masken immer so programmieren, daß sie multi-instanzierbar sind (die Mehrfachinstanzierung im Einzelfall verhindern kann man dann immer noch)
  • Applikationen immer so konzipieren, daß sie multi-user- und damit auch multi-session-fähig sind (den Mehrfachstart im Einzelfall verhindern kann man dann immer noch)

Voraussetzung ist natürlich, den zweifellos höheren einmaligen Aufwand für die netzwerktaugliche Programmierung einmal umfassend in einem eigenen Framework erarbeitet oder mit einem fremden Framework eingekauft zu haben (zu Frameworks siehe die Sessions der Sektion FWK).
TIP: Immer multi-user-, multi-session- und multi-instancing-tauglich programmieren. Irgendwann ereilt es einen doch!

Netzwerkfähigkeit durch multi-instanzierbare Masken

Die Multi-Instanzierbarkeit von Masken ist im wesentlichen für Dateneingabe- und Datenpräsentations-Masken von großer Bedeutung. Für ablaufsteuernde Dialoge dagegen spielt Multi-Instanzierbarkeit nur eine sehr geringe Rolle.

Für die Programmierung multi-instanzierbarer Masken sind folgende Aspekte relevant, die im weiteren näher erläutert werden:

  • Forms als Objekte
  • Private DataSession inklusive Buffering
  • Dateien immer im Modus SHARED öffnen
  • weitere Überschneidungs-Fallen beachten
  • Koordinierung der existierenden Instanzen

Forms als Objekte

Diese erste Voraussetzung ist fest in die Programmiersprache von Visual FoxPro integriert.

Jede mit dem Formdesigner erstellte Maske ist ein Objekt mit einem eigenen Namensraum für Property- und Methoden-Bezeichner. Dadurch kann es bei der Mehrfach-Instanzierung einer Maske keine gegenseitigen Beeinflussungen oder Überschneidungen beim Zugriff auf Methoden oder Properties geben!

Private DataSession

Die Private DataSession einer Form beinhaltet mehrere wichtige Aspekte:

  • Jede Private DataSession umfaßt einen abgeschotteten Namensraum für Arbeitsbereiche (in jeder Private DataSession kann erneut eine Tabelle mit dem Arbeitsbereich "Kunde" geöffnet sein).
  • Die durch die Grenzen einer DataSession abgeschotteten Arbeitsbereiche sind bezüglicher diverser Eigenschaften völlig voneinander unabhängig (selbst wenn sie sich auf die gleiche Tabelle beziehen):
    • Satz-Zeiger
    • Filter-Einstellungen
    • Index-Einstellungen usw.
  • Außerdem gibt es diverse arbeitsbereichs-übergreifende Einstellungen, die für jede DataSession unterschiedlich eingestellt sein können (z.B. SET DELETED, SET DATE, SET POINT usw).
TIP: Eine vollständige Liste der datasession-abhängigen SET-Einstellungen findet man in der VFP-Hilfe unter dem Stichwort "SET DATASESSION"!

TIP: In jeder neu eröffneten Private DataSession befinden sich diese Set-Einstellungen leider in der amerikanischen Grundeinstellung, deshalb muß man sich einen Mechanismus schaffen, der diese Einstellungen auf die selbst definierten Standards umstellt!

Dateien immer im Modus SHARED öffnen. Die Notwendigkeit dieser Verfahrensweise ist leicht einzusehen:

  • Würde die Kundentabelle in der Datenumgebung der Kundenmaske mit EXCLUSIVE eingetragen sein, dann wird die Kundentabelle beim Starten der ersten Instanz der Maske exklusiv geöffnet.
  • Das Starten einer weiteren Instanz würde die Kundentabelle wiederum exklusiv öffnen wollen, was verständlicherweise fehlschlagen muß.

Weitere Überschneidungs-Fallen

Neben den schon angeführten Dingen gibt es noch einige weitere Aspekte, bei denen es zu Überschneidungen kommen kann, die eine Mehrfach-Instanzierung von Masken verhindern:

  • Wenn man in unzulässiger Weise globale Variablen verwendet, kommt es bei Mehrfach-Instanzierung dazu, daß die Variablen wechselseitig überschrieben werden.
TIP: Vermeiden Sie konsequent die Benutzung globaler Variablen, sie sind nur in ganz extremen Ausnahmen notwendig!
  • Auch das Anlegen temporärer Dateien mit festen Namen führt dazu, daß sich die Dateiinhalte gegenseitig verfälschen.
TIP: Benutzen Sie zum Erstellen temporärer Tabellen am besten Views, SELECT…INTO CURSOR bzw. CREATE CURSOR. Für diese Dateien verwaltet Visual FoxPro selbst die temporären Dateinamen und löscht diese Dateien auch automatisch, wenn sie geschlossen werden (das gilt übrigens auch für Indizies, die man für einen View oder Cursor anlegt).

TIP: Sollten dennoch eigene temporäre Dateien angelegt werden, benutzten Sie die Funktionen SYS(3) oder SYS(2015) zum Bilden von temporären Dateinamen. Vergessen Sie aber niemals die Prüfung, ob eine Datei mit dem entsprechenden Namen nicht doch schon existiert!

Und außerdem müssen Sie selbst für das Löschen solcher Dateien sorgen!

  • Ein weiterer Punkt ist die Namensgebung für maskenspezifische Menüs und Popups, da es in Visual FoxPro nur einen einzigen gemeinsamen Namensraum für Menü- und Popup-Namen gibt (die komplette Menü-Verwaltung von Visual FoxPro ist leider immer noch nicht objektorientiert).
TIP: Benutzen Sie auch für Menüs und Popups die Funktion SYS(2015) in Verbindung mit einer Prüfung, ob der Name nicht doch schon benutzt wird!

 

 

  • Außerdem existieren einige indirekte Beeinflussungen zwischen Arbeitsbereichen in verschiedenen DataSessions, die sich auf die gleiche Tabelle beziehen:
    • wird eine Datei das allererste Mal im Modus READ ONLY geöffnet, kann sie in keiner anderen DataSession mehr im Modus READ WRITE geöffnet werden (dies ist ein Bug!)
    • ist eine Tabelle in irgendeiner DataSession mit Table Buffering geöffnet, können keine neuen Indizies für diese Tabelle erzeugt werden
  • Es ist zu beachten, daß es in einigen anderen Fällen noch zu weiteren Beinflussungen kommen kann (speziell beim Sperren von Datensätzen), was weiter unten detailliert erläutert wird.

Koordinierung existierender Instanzen von Masken

Die Koordinierung existierender Instanzen von Masken wird weiter unten behandelt.

Die Multi-User-Fähigkeit von Masken

Die Multiuser-Fähigkeit einer Maske muß immer dann gegeben sein, wenn diese Maske zu einem Zeitpunkt mehrfach geöffnet sein kann:

  • die Maske ist mehrfach innerhalb einer Applikation geöffnet
  • eine Applikation mit der Maske ist mehrfach innerhalb eines Windows-Systems aktiv
  • eine Applikation mit der Maske läuft zeitgleich auf verschiedenen Arbeitsstationen einer Netzwerk-Installation.

Locking-Strategien für Masken

Die wichtigste Aufgabe in der Multi-User-Programmierung besteht in der Koordinierung und Konfliktlösung bei quasi gleichzeitiger Bearbeitung von Datensätzen. Für diese Aufgabe stehen in Visual FoxPro verschiedene Lockingtypen zur Verfügung:

  • pessimistisches Locking
  • optimistisches Locking

Bei pessimistischem Locking wird der entsprechende Datensatz gesperrt, sobald in der Maske die erste Veränderung vorgenommen wird:

  • In dieser Variante können mehrere Nutzer den gleichen Datensatz angezeigt haben, aber nur der "erste" Benutzer kann die Daten ändern.
  • Will ein weiterer Benutzer die Daten ändern, so führt das zu dem Fehler "Record is in use by another user (Error 109)".
  • Bewertung:
    • Vorteil: Wer begonnen hat, den Satz zu bearbeiten, kann ihn mit Sicherheit auch speichern.
    • Nachteil: Wer den Satz begonnen hat zu bearbeiten und stehen läßt, behindert alle anderen.

Im optimistischen Locking sperrt Visual FoxPro den Datensatz nur kurzeitig in dem Moment, wenn der Satz gespeichert wird:

  • Mehrere Nutzer können deshalb den Datensatz gleichzeitig ändern.
  • Wenn ein Benutzer den Datensatz speichert, und danach ein anderer auch seine Änderungen speichern will, wird die zweite Änderung zunächst abgewiesen, da sich die Ausgangsdaten zwischenzeitlich geändert haben.

Für diesen Fall kann man mit den Befehlen und Funktionen GETNEXTMODIFIED(), GETFLDSTATE(), OLDVAL(), CURVAL(), TABLEREVERT() und TABLEUPDATE() sowie unter Einsatz von Transaktionen eine beliebig differenzierte automatische oder auch durch den Nutzer beeinflußbare Konfliktlösung programmieren.

  • Bewertung:
    • Vorteil: Keine gegenseitige Behinderung beim Bearbeiten.
    • Nachteil: Eventuell können Änderungen nicht gespeichert werden, ohne den Satz nochmals zu überarbeiten.

Angesichts des Gleichgewichts in der Bewertung der beiden Locking-Verfahren gibt ein weiterer Aspekt den Ausschlag, nämlich die Verfahrensweise bei Views: Views können generell nur mit dem optimistischen Verfahren bearbeitet werden, da die meisten Backend-Datenbanken keine pessimistische Sperrung unterstützen.
TIP: Arbeiten Sie aus Gründen der Einheitlichkeit in Programmierung und Bedienung immer mit optimistischem Locking!

Die Auswahl eines bestimmten Locking-Typs ist in Visual FoxPro immer mit der Entscheidung für einen der zwei möglichen Buffermodes verbunden:

  • Row Buffering puffert immer den aktuellen Datensatz, wobei der TableUpdate-Befehl oder irgendeine Satzzeigerbewegung zum automatischen Update der Tabelle führen.
  • Bei Table Buffering werden alle veränderten Datensätze solange gespeichert, bis sie explizit mit TableUpdate() aus dem Puffer in die DBF-Datei zurückübertragen werden.

Damit können durch optimistisches Table-Buffering alle relevanten Fälle gut abgedeckt werden:

  • Dort, wo sowieso Table Buffering benötigt wird, steht es damit zur Verfügung.
  • Wo eigentlich Row Buffering möglich wäre, schadet Table Buffering auch nichts, wenn man beim Speichern oder Verwerfen eines Einzelsatzes immer ein TableUpdate() oder TabelRevert() ausführt, denn dann wird trotzdem jeder Satz einzeln gepuffert und gespeichert.
    Außerdem hat es den zusätzlichen positiven Effekt, das man auf einer schon geänderten Tabelle problemlos Satzzeigerbewegungen ausführen kann, ohne daß der Pufferinhalt unbeabsichtigt implizit gespeichert wird.
  • Wenn man eigentlich kein Buffering benötigt (bei reinen Lookup-Tabellen o.ä.), schadet das Buffering nichts, sondern hat im Gegenteil noch einen wichtigen Vorzug. Eventuell versehentlich durchgeführte Veränderungen in solchen Dateien werden bei Table-Buffering ohne ein explizites TableUpdate() automatisch ignoriert (beim Schließen der Datei wird sozusagen ein implizites TableRevert() ausgeführt).
TIP: Öffnen Sie alle Ihre Tabellen und Views immer mit optimistischem Table-Buffering!

ACHTUNG! Der SELECT-SQL-Befehl ist der einzige Befehl, der nicht auf die Daten aus dem ggf. schon veränderten Puffer zurückgreift, sondern die Ergebnisse immer direkt aus den Datei-Inhalten auf der Festplatte ermittelt.

Dieses Verhalten kann man gezielt ausnutzen, wenn man z.B. bei in einer Eingabe-Validierung nach Doubletten sucht, da der neu eingebene Wert nur im Puffer steht und damit in das Ergebnis eines entsprechenden SQL-Statements nicht mit einbezogen wird.

ACHTUNG! Die mit entsprechendem Locking und Buffering festgelegte Verfahrensweise dient nicht nur zur Koordinierung zwischen mehreren Instanzen einer speziellen Maske.

Vielmehr ist dies ein tabellenbezogener Mechanismus, der auch Zugriffskonflikte bei Überschneidungen von verschiedenen Masken oder Programmteilen bezüglich einer Tabelle verabeiten können muß.

Voraussetzung dafür ist allerdings, daß die Zugriffsmechanismen an allen Stellen im Programm bzgl. einer Tabelle nach der gleichen Strategie vorgehen (siehe auch weiter unten).

Konfliktlösung beim TableUpdate()

Beim TableUpdate() können in der Konstellation "optimistic Table-Buffering" folgende Arten von Konflikten auftreten:

  • Updatekonflikte
  • Locking-Konflikte
  • Rules- und Trigger-Fehler
  • sonstige Fehler

Diese Konflikte äußern sich in dem Rückgabewert .F. der TableUpdate()-Funktion und/oder dem Auslösen eines Fehlers, wobei die detaillierte Ursache in beiden Fällen aus dem mit AError() zu füllenden Error-Array ermittelt werden kann.

Updatekonflikte treten dann auf, wenn der bearbeitete Datensatz zwischenzeitlich durch jemand anderen geändert wurde:

  • In diesem Fall bricht der Update-Prozeß ab und die TableUpdate()-Funktion liefert ein .F. zurück (allerdings nur, wenn als zweiter Parameter bei TableUpdate() nicht .T. angegeben war).
    Das Fehler-Array liefert den Fehler "Update conflict (Error 1585)".
  • Falls diese Konstellation nicht gleich bei dem ersten geänderten Datensatz auftritt, sind die Änderungen der vorangegangenen Datensätze allerdings schon gespeichert!
  • Um ein derart mißlungenes Update aus Konsisitenzgründen wieder rückgängig zu machen, muß eine Transaktion verwendet werden.
  • Bei Auftreten eines solchen Konfliktes können die Funktionen GetNextModified(), GetFldState(), CurVal(), OldVal(), TableRevert() und TableUpdate() benutzt werden, um den vorliegenden Konflikt zu erkennen und lösen zu können.

Locking-Konflikte entstehen, wenn einer der zu speichernden Datensätze nicht gesperrt werden kann:

Ursache dafür kann darin liegen, daß der betreffende Satz in diesem Moment durch einen zeitgleichen Update-Prozeß einer anderen Arbeitsstation oder einer anderen Instanz der Applikation oder im Extremfall durch eine andere Maske der gleichen Instanz der Applikation gesperrt ist (wenn diese andere Maske mit pessimistischem Locking arbeiten sollte).

  • Eine andere Ursache ist eine Datei- oder Datensatzsperre unabhängig von einem Update-Prozeß, entweder explizit durch die Befehle RLOCK() / FLOCK() oder implizit durch die Einstellung des SET LOCK Befehls in Verbindung mit den damit zusammenhängenden Befehlen.
  • In diesen Fällen entscheidet die Einstellung von SET REPROCESS darüber, wie oft bzw. wie lange versucht wird, eine Sperrung zu erreichen. Ist diese Zeit bzw. Anzahl überschritten, dann wird der Fehler "Record is in use by another user (Error 109)" ausgelöst!
  • Der Fehler " File is in use by another user (Error 108)" kann beim TableUpdate() nicht auftreten, da diese Funktion immer satzweise arbeitet und deshalb niemals versucht, die komplette Datei zu sperren.
  • Dagegen kann es durchaus zu dem Fehler " Illegal to attempt a file lock in a transaction after taking prior record locks (Error 1594)" kommen (siehe VFP-Dokumentation).

Rules- und Triggerfehler entstehen, wenn eine der zuständigen Rules oder Trigger feststellt, daß die betreffende Operation nicht ausgeführt werden darf:

  • In diesem Fall liefert die jeweilige Prüf-Routine (Rule oder Trigger) ein .F. zurück und die entsprechende Änderung wird abgewiesen.
  • Die Verletzung einer Rule führt zu dem Fehler " Field 'name' validation rule is violated (Error 1582)" bzw. " Record validation rule is violated (Error 1583)".

TIP: Beachten Sie, daß die Field- und Record-Validierung nicht erst beim TableUpdate() verarbeitet werden, sondern unabhängig vom BufferMode immer bereits beim Verlassen des Feldes bzw. des Datensatzes.

Beim TableUpdate() kann deshalb nur die Record-Validation fehlschlagen und auch nur, wenn der Satzzeiger noch nicht bewegt wurde!

  • Ein Triggerfehler wird an der Meldung " Trigger failed (Error 1539)" erkannt und kann durch die Spalte 5 des Fehler-Arrays näher spezifiziert werden.
  • Die Operation wird auch abgewiesen, wenn eine verschachtelte persistente Relation ihrerseits feststellt, daß nicht gespeichert werden darf!

Sonstige Fehler resultieren aus der Verletzung von fest in Visual FoxPro integrierten Regeln:

  • Wenn die Eindeutigkeit eines Primary- oder Candidate-Keys verletzt wird, entsteht der Fehler "Uniqueness of index 'name' is violated (Error 1884)".
  • Wird einem Feld ein .NULL. Wert zugewiesen und dies ist auf Grund der Felddefinition nicht gestattet, dann wird der Fehler " Field 'name' does not accept null values (Error 1581)" ausgelöst!
TIP: Beachten Sie bitte, daß die in diesem Abschnitt beschriebenen Fehler-Konstellationen speziell im Zusammenhang mit optimistischem Table-Buffering beschrieben sind!

Weitere Aspekte

Einige weitere Aspekte sind im nachfolgenden Abschnitt beschrieben, haben allerdings für die Multi-User-Fähigkeit von Masken ebenso Gültigkeit!

Multi-user- / multi-session-fähige Applikationen

Die im vorigen Abschnitt "Multi-User-Fähigkeit von Masken" diskutierten Aspekte sind eigentlich nicht masken-spezifisch, sondern können ebenso auftreten, wenn die Inhalte von Datenfeldern und Datensätzen programmatisch durch REPLACE, INSERT und verwandte Befehle verändert wurden, und dabei wie empfohlen optimistisches Table-Buffering aktiv ist.

In den folgenden Punkten werden darüberhinausgehende Aspekte multi-user- und multi-session-fähiger Applikationen behandelt, die gleichermaßen auch für Masken relevant sind.

Semaphoren durch Sperrung auf übergeordneter Ebene

In bestimmten Fällen ist es wünschenswert, daß eine gewisse Menge von Datensätzen ungestört bearbeitet werden kann, z.B. alle Positionen einer Rechnung.

In diesem Fall muß man zu einer besonderen Strategie greifen, denn man könnte zwar alle existierenden Positionssätze speichern, aber dadurch nicht die Neuanlage von Rechnungspositionen in der betreffenden Rechnung verhindern.

Deshalb verwendet man die RLOCK()-Funktion für eine Satzsperre auf dem zugehörigen Satz in einer übergeordneten Datei (z.B. in der Rechnungskopf-Datei) als Semaphore, und der Sperrstatus gibt Auskunft darüber, ob die betreffende Rechnung überhaupt bearbeitet werden darf.

Wenn sich nun alle rechnungsbearbeitenden Programmteile (Masken und andere Programmsegmente) an diese Regelung halten, kann niemals der Fall eintreten, daß bestimmte Rechnungspositionen von zwei Stationen gleichzeitig verändert werden, d.h. es bedarf in diesem Fall keiner speziellen Locking-Vorkehrungen bzgl. der Rechnungspositionen.

Locking für konsistente Auswertungen

Für die Konsistenz von Auswertungen kann es notwendig sein, daß während des Ablaufs einer Auswertung gewährleistet ist, daß keine Änderungen in den einbezogenen Daten erfolgen können: Eine Soll-Haben-Aufstellung würde z.B. verfälscht, wenn zeitlich gesehen zwischen der Ermittlung der Soll-Summe und der Ermittlung der Haben-Summe eine neue Buchung eingetragen würde, die dann zwar in der Haben- aber nicht in der Soll-Summe auftauchen würde.

Auch in diesem Fall kommt am günstigsten eine Sperr-Semaphore durch einen RLOCK()-Befehl auf einer hinreichend übergeordneten Tabelle als Lösung in Frage.

Locking bei Such- und Prüf-Prozessen

Auch Such- und Prüfprozesse müssen ggf. mit einer Locking-Semaphore vor einer zeitlichen Überschneidung geschützt werden.

Beispiele für derart zu schützende Prozess sind:

  • Doublettenprüfung der Artikelbezeichnung beim Speichern eines Datensatzes in der Artikelmaske
  • Ermitteln der nächsten freien Rechnungsnummer beim Erstellen einer neuen Rechung

In solchen Fällen wird eine Satzsperre gesetzt, die entsprechende Prüfung oder Ermittlung durchgeführt und danach die Sperre wieder freigegeben, wodurch eine zeitliche Überschneidung dieser Aktivitäten verhindert wird, die zu Verfälschungen führen würde.

Das Qualifizieren von Lockings

Wenn ein Record- oder File-Lock als Semaphore dafür verwendet wird, daß eine bestimmte Operation im Moment nicht ausgeführt werden darf, ist die Kennzeichnung völlig ausreichend.

Andererseits ist es aber sehr wünschenswert, dem Anwender der Arbeitsstation, die auf eine Freigabe warten muß, diverse Einzelheiten über den Grund und die Dauer der Sperrung mitzuteilen.

Deshalb sollte man Dateien, in denen Semaphoren hinterlegt werden, mit zusätzlichen Feldern versehen:

SPERRE_WER C 10
   SPERRE_WANN T
   SPERRE_WARUM M oder C

Wird nun ein RLOCK() als Semaphore gesetzt, wird in diese Felder eingetragen, durch welchen Benutzer oder durch welche Arbeitsstation zu welchem Zeitpunkt und aus welchem Grund dieser Satz gesperrt ist.

Diese Informationen können dann im Falle einer fehlgeschlagenen Sperre den wartenden Nutzern in einer Maske angezeigt werden.

Semaphoren im Client-Server-Bereich

Bei Client-Server-Lösungen hat man auf Grund der Eigenschaften der Backend-Datenbank u.U. keine Möglichkeiten, eine explizite Satzsperre zu setzen, wie sie für Semaphoren notwendig wäre.

In diesem Fall kann man sich an Stelle von Locks mit TimeStamp-Semaphoren behelfen:

  • In den betroffenen Tabellen wird ein zusätzliches DateTime-Feld angelegt, welches normalerweise leer ist.
  • Soll jetzt eine Semaphore gesetzt werden, so ersetzt man das DateTime-Feld durch einen Zeitpunkt, zu dem die Semaphore als abgelaufen betrachtet werden kann (wie eine Parkuhr).
  • Ist die betreffende Aktivität dann beendet, wird das DateTime-Feld wieder leergeräumt.
  • Will jetzt eine andere Station die gleiche Aktivität ausführen, erkennt sie das u.U. belegte DateTime-Feld und prüft, ob die "Parkuhr" schon abgelaufen ist.
  • Wenn die Zeit schon abgelaufen war, dann wird die Semaphore als ungültig betrachtet (z.B. verursacht durch einen Absturz der Arbeitsstation, die die Semaphore gesetzt hat).
  • Ist die Zeit noch nicht abgelaufen, muß die zweite Station solange warten, bis die Zeit abgelaufen ist oder das DateTime-Feld wieder leer ist (weil die erste Station ihre Aktivitäten mittlerweile beendet hat).
  • Für das Funktionieren dieses Mechanismus kann man den einzutragenden Zeitvorlauf relativ groß halten (z.B. 15 min.), da man ja im normalen Betrieb sowieso davon ausgehen kann, das das Feld nach sehr kurzer Zeit wieder gelöscht wird.

Die im TimeStamp hinterlegte Zeit ist also eher eine Art WatchDog-TimeOut, bei dessen Ablaufen eine noch gesetzte Semaphore für ungültig erklärt wird.

Die Stimmigkeit von Sperr-Strategien

Die Verwendung von Sperrstrategien an verschiedenen Stellen einer Applikation erfordert einen konsistenten Einsatz der Sperrmechanismen.

Besondere Aufmerksamkeit verdient dabei die Benutzung übereinstimmender Ebenen des Semaphoren-Lockings.

Beispiel:

  • Es hilft z.B. wenig, wenn zur Auswertung der Rechnungsdaten eines Kunden der betreffende Kundensatz in der Kundenstammdatei als Semaphore gesperrt wird, die Rechungsbearbeitungsmaske dagegen Rechnungen satzweise in der Rechnungskopfdatei sperrt.
  • Eine sinnvoll abgestimmte Verfahrensweise in diesem Fall wäre es dann, vor der Auswertung alle Rechungsköpfe des betreffenden Kunden zu sperren.
  • Dann müßte aber immer noch verhindert werden, daß in diesem Zeitraum für den Kunden neue Rechnungen angelegt werden können. Dies könnte man dadurch bewerkstelligen, daß man vor dem Speichern einer neuerfaßten Rechnung prüft, ob der Kundendatensatz des zugehörigen Kunden gesperrt ist.

Dieses relativ einfache Beispiel zeigt sehr anschaulich die Komplexität, die hinter dieser Problematik steckt. Deshalb bedarf dieses Thema bei Konzipierung einer Applikation im Rahmen der Datenmodellierung großer Aufmerksamkeit!
TIP: Ein System aufeinander abgestimmter applikationsinterner Sperrmechanismen hat natürlich keine oder nicht die gewünschte Wirkung, wenn auf die Daten von außerhalb der Applikation zugegriffen wird.

Der Umgang mit SET REPROCESS

Von besonderer Bedeutung für netzwerkrelevante Porgrammteile ist die Einstellung von SET REPROCESS.

Die SET REPROCESS Einstellung legt gemeinsam mit dem aktiven Error-Handler fest, wie Visual FoxPro mit der Tatsache umgeht, daß ein Satz nicht gesperrt werden kann (siehe VFP-Hilfe zum Befehl SET REPROCESS bzw. Session D-NETZ). Zu dieser Thematik sind die folgenden Aspekte abzuwägen:

  • Einerseits sollte man REPROCESS auf einen genügend großen Wert setzen, der etwas mehr als die übliche Zeitdauer einer Sperre betragen sollte.
  • Desweiteren muß bei der Progammierung immer damit gerechnet werden, daß die Zeit trotzdem überschritten wird. Für diesen Fall muß das Programm eine definierte Reaktionsmöglichkeit beinhalten!
  • Außerdem muß man den Anwender darüber informieren, daß der Programmablauf darauf wartet, eine Satz-Sperre setzen zu können.

Die beiden zuletzt genannten Punkte führen zu einer programmtechnischen Umsetzung dieser Problematik, die einen hohen Allgemeinheitsgrad hat:

  • Wenn man konsequent mit optimistischem Tablebuffering arbeitet, kann es nur bei den Befehlen RLOCK(), FLOCK() und TABLEUPDATE() zu Locking-Konflikten kommen, denn alle anderen Befehle arbeiten auf den Tabellenpuffern und verursachen deshalb keine Locks.
  • Diese drei Befehle sollte man niemals direkt aufrufen, sondern sich entsprechende kapselnde Funktionen oder Methoden dafür programmieren.
  • In diesen Methoden wird generell mit einem Schleifen-Konstrukt und SET REPROCESS TO 1 gearbeitet, wie der folgende Pseudo-Code am Beispiel des RLOCK-Befehls zeigt:
SET REPROCESS TO 1
   llSuccess = .F.
   DO WHILE .T.
   IF RLOCK()
   * Sperre erfolgreich
   llSuccess = .T.
   EXIT
   ELSE
   * Sperre nicht erfolgreich
   IF schon diverse Wartezeit verstrichen
   * Nachricht anzeigen
   ...
   ENDIF
   IF INKEY(,"H") = -7
   * Nutzer will mit der F8-Taste abbrechen
   EXIT
   ENDIF
   ENDIF
   ENDDO
   RETURN m.llSuccess
  • Dadurch hat man die Möglichkeit, längere Wartezeiten mit einer Bildschirmausgabe zu versehen und dem Nutzer eine programmtechnisch unkritische Abbruch-Möglichkeit zur Verfügung zu stellen.

Voraussetzung dafür ist allerdings, daß die aufrufenden Funktionen den weiteren Programmablauf dann entsprechend des Rückgabewertes organisieren.
TIP: Programmtechnisch kann man zwar den numerischen Wert der Einstellung von SET REPROCESS abfragen, nicht aber, ob dieser Wert als "Anzahl" oder als "Sekunden" gemeint ist. In diesem Fall hilft nur eine applikationsweite Vereinbarung weiter.

TIP: Beachten Sie im Zusammenhang mit SET REPROCESS auch die Funktion SYS(3052), die das Lockingverhalten bezüglich Index- und Memodateien regelt!

Transaktionen

Transaktionen spielen für die Sicherung der Konsistenz von Datenbeständen eine wichtige Rolle. Diese Thematik ist dabei nicht so sehr eine Spezifik der Netzwerkprogrammierung, sondern hat mehr mit der inhaltlichen Konsistenz zusammengehöriger Daten zu tun.

Im Rahmen der Konzipierung netzwerkfähiger Applikationen muß unbedingt beachtet werden, daß für die Schachtelung von Transaktionen lediglich 5 Ebenen zur Verfügung stehen, wovon noch eine Ebene für die Trigger-Mechanismen des Datenbankcontainers von Visual FoxPro benötigt wird. Daraus ergibt sich eine Menge von maximal 4 frei verfügbaren Schachtelungsebenen für Transaktionen.

Diese Begrenzung kann problematisch werden, wenn man ineinander geschachtelte Funktionen programmiert, die jede für sich sozusagen "der Ordnung halber" mit einer Transaktion abgesichert sind. Übersteigt diese Schachtelungstiefe die Anzahl der zur Verfügung stehenden Transaktionsebenen, wird der Fehler "BEGIN command failed. Nesting level is too deep (Error 1590)" ausgelöst!

Auch vor dieser Problematik kann man sich durch geeignete programmorganisatorische Maßnahmen schützen:

  • Man arbeitet nach der sogenannten "alles oder keiner" Methode. Entweder sind alle ineinander geschachtelten Aktivitäten in ihrer Gesamtheit erfolgreich oder es wird alles wieder rückgängig gemacht.
  • Für die technische Umsetzung dieser Variante benötigt man dann nur noch genau eine Transaktionsebene.
  • Jede Funktion, die ein Transaktion benötigen würde, prüft durch Abfragen der Systemvariablen _TXNLEVEL, ob schon eine Transaktion läuft.
  • Nur im Fall _TXNLEVEL = 0 wird eine Transaktion gestartet, und auch nur für diesen Fall wird die Transaktion am Ende der Funktion wieder abgeschlossen.
  • In allen anderen Fällen geht die Funktion davon aus, daß die schon aktive Transaktion ein eventuell notwendiges ROLLBACK sichert.
  • Unbedingte Voraussetzung dafür ist allerdings, daß jede dieser geschachtelten Funktionen ihrer übergeordneten Funktion mitteilt, ob sie erfolgreich war oder nicht. Der Mißerfolg einer solchen Funktion muß dann an die höheren Porgrammebenen immer weiter hochgereicht werden bis auf die Ebene, die die Transaktion gestartet hat und dadurch veranlaßt wird, ein ROLLBACK auszuführen.

Eine dritte Transaktion neben der eben beschriebenen inhaltlichen Transaktionsebene und der Ebene für den Datenbank-Container wird für die Konsistenzsicherung des TableUpdate()-Befehls benötigt, wenn man wie empfohlen mit Table-Buffering arbeitet:

  • Wie weiter oben beschrieben kann ein TableUpdate()-Befehl im Table-Buffering-Mode mitten in der Menge zu speichernder Sätze abbrechen, wenn ein Updatekonflikt vorliegt.
  • Auch in diesem Fall sollte man nach der "alles oder keiner" Methode vorgehen, weswegen ein solches TableUpdate() in eine Transaktion eingeschlossen wird, um es ggf. komplett rückgängig machen zu können.

ACHTUNG! Beachten Sie bitte, das eine Transaktion nur bezogen auf ein DataSession gilt!

Die weiteren allgemeinen technischen Aspekte der Arbeit mit Transaktionen in Visual FoxPro entnehmen Sie bitte der Session D-NETZ.

Weitere Aspekte multi-user- / multi-session-fähiger Applikationen

Zur Programmierung multi-user- und multi-session-fähiger Applikationen gehören noch einige weitere Aspekte, die im Rahmen dieser Session nur erwähnt werden können:

  • Es wird normalerweise ein Nutzerverwaltung benötigt.
  • Man muß sich für eine Strategie bzgl. der Anmeldeprozedur entscheiden (soll es z.B. möglich sein, daß ein Nutzer zur gleichen Zeit mehrfach angemeldet sein darf?).
  • Auch der Umgang mit temporären Dateien und Verzeichnissen muß konfliktfrei organisiert werden.
  • Weiterhin muß die Applikation so konfigurierbar sein, daß bei fileserver-basierten Lösungen der Zugriffspfad auf den zentralen Datenbestand über ein beliebig benennbares Netzwerklaufwerk erfolgen kann.
  • Für Reorganisationszwecke wird ein exklusiver Administrationsmodus benötigt.

Koordinierung existierender Instanzen von Masken

Wenn mehrere Instanzen einer Maske zugelassen sind, gibt es häufig den Bedarf, diese Instanzen miteinander zu koordinieren.

Ermitteln existierender Instanzen

Zur Koordinierung existierender Instanzen ist es notwendig, alle diese Instanzen zu ermitteln.

Dabei ist zu unterscheiden zwischen dem nachträglichen Ermitteln der existierenden Instanzen einerseits bzw. dem Mitprotokollieren des Instanzierungsprozesses andererseits.

Das nachträgliche Ermitteln existierender Instanzen einer Maske kann über mehrere Wege erfolgen:

  • Wurde die Maske aus einer VCX-Datei per CREATEOBJECT() instanziert, können die existierenden Instanzen mit der Funktion AINSTANCE() ermittelt werden.
  • Wenn die Maske dagegen per DO FORM aus einer SCX-Datei erzeugt wurde, hilft das Durchsuchen der _Screen.Forms Collection auf Masken, bei denen das Ergebnis der SYS(1271)-Funktion das gesuchte Ergebnis liefert.

Das Mitprotokollieren der Instanzierung erfolgt über einen sogenannten FormLoader:

  • Ein FormLoader ist ein selbst programmiertes Programm-Modul, welches dem Zweck dient, Masken zu instanzieren.
  • Der Formloader bekommt den Klassen- oder SCX-Datei-Namen der zu instanzierenden Form sowie ggf. weitere Parameter mitgeteilt und sorgt für die ordnungsgemäße Instanzierung
  • Wichtige Funktion dabei ist die Protokoll-Funktion, die üblicherweise die Objektreferenzen auf die erstellten Masken in einem Array zusammen mit weiteren Informationen aufbewahrt und für diverse Zwecke benutzen kann.

Instanzen-Handling

Die Überwachung der existierenden Instanzen einer Maske kann aus verschiedenen Gründen wichtig sein:

  • zur prinizipiellen Verhinderung des Mehrfachstarts einer nicht-modalen Maske
  • zur Begrenzung der Anzahl der Instanzen einer nicht-modalen Maske
  • zum Ermitteln der Instanzen-Anzahl einer Maske, um die aktuelle Instanz-Nummer im Fenstertitel anzuzeigen

Für die Ermittlung der existierenden Instanzen einer Maske gibt es verschiedene Möglichkeiten:

  • Prüfung der Existenz weiterer Instanzen der Maske aus dem Init der Maske heraus
  • Prüfung der Existenz weiterer Instanzen der Maske vor dem Instanzieren der Maske

Beide Varianten können sowohl in die Start-Events von Masken (Init und Load) eingebaut werden als auch im Vorfeld vor dem Instanzieren zum Einsatz kommen:

  • Vorteil in den Start-Events ist die Tatsache, daß sozusagen die Maske selbst ihre eigenen Instanzen kontrolliert. Der Nachteil besteht darin, daß dies erst geschehen kann, nachdem die neue Instanz schon existiert und ggf. erst wieder vernichtet werden muß.
  • Der Vorteil der Kontrolle vor dem Instanzieren liegt in der Vermeidung des Instanzierens von Objekten, die dann wegen überschrittener Anzahl erst wieder zerstört werden müßten. Der Nachteil ist allerdings, daß ein den Formularen übergeordnetes Objekt (ein sogenannter FormLoader) existieren muß, der diese Verwaltungsaufgaben übernimmt.

Außerdem arbeitet diese Variante nur zuverlässig, wenn durch entsprechende Programmier-Disziplin sichergestellt ist, daß keine Maske ohne Benutzung des Formloaders instanziert wird.

Zusammenfassung der Empfehlungen

  • Programmieren Sie immer multi-user-, multi-session- und multi-instancing-tauglich, denn irgendwann ereilt es einen doch!
  • Arbeiten Sie bei temporären Tabellen sooft es geht mit Views, SELECT…INTO CURSOR bzw. CREATE CURSOR, denn für diese Dateien verwaltet Visual FoxPro selbst die temporären Dateinamen und löscht diese Dateien auch automatisch, wenn sie geschlossen werden (inklusive möglicher Index- und Memodateien)!
  • Öffnen Sie alle Ihre Tabellen und Views immer mit optimistischem Table-Buffering!
  • Bauen Sie um RLOCK()-, FLOCK()- und TableUpdate()-Befehle eine Schleifenkonstruktion, die auf eine längere Wartzeit mit einer Anzeige reagieren kann und außerdem durch den Nutzer per Funktionstaste abbrechbar ist.
  • Begrenzen Sie sich gezielt auf den Einsatz von drei Transaktions-Ebenen:
    • die erste Transaktionsebene zur Klammerung inhaltlich zusammengehöriger Daten-Veränderungen
    • die zweite Transaktionsebene zum Absichern des TableUpdate()-Befehls
    • eine dritte Transaktionsebene benutzt das Visual FoxPro automatisch zur Absicherung der referentiellen Integrität

Über den Autor

Bei weiterführendem Interesse kann zum Autor gern Kontakt aufgenommen werden (E-Mail-Adresse:
SFlucke@asci-consulting.com).