[Allegro] update.job + optsget.inc überarbeitet

Thomas Berger ThB at Gymel.com
Fr Jan 13 12:21:33 CET 2012


Lieber Herr Eversberg,

> Aufruf:   acon -jupdate -f...   statt   update -f...
> Mehr dazu in Kürze!
> 
> Noch nicht berücks. Optionen:  I,R,m,y,z
> (Unseres wissens wurden diese kaum je benutzt)

Der Schalter -i duerfte ebenfalls schwer zu implementieren sein:
acon kann (derzeit) ja keine Datenbank anlegen, sondern setzt
bereit fuer den Start das Vorhandensein der angegebenen voraus.

-R (Steuerung der vollstaendigkeit der Protokollierung, insbesondere
-R3) ist tendenziell recht nuetzlich (paranoid wie ich bin, habe ich
das auch bereits mehrfach genutzt, etwa um nach dem Update
auszuwerten, ob auch alle zu loeschenden Saetze vorher vorhanden
gewesen sind), ich bin allerdings der Ansicht, dass hier keine
Kompatibilitaet der Ausgabe erreicht werden muss.

-m kann m.E. ebenfalls unabhaengig vom Verhalten von UPDATE.EXE
implementiert werden: Ich hatte ja zu bedenken gegeben, dass es
keine korrekte Strategie gibt, mit Sperren umzugehen. -m kann
genutzt werden, dem Job klarzumachen, ob es sich um ein
"beaufsichtigtes" (-m1: Benutzer ist praesent und koennte auf erkannte
Sperren waehrend der Laufzeit reagieren => Update versucht es
endlos, eine keycheck-Funktionalitaet in acon waere langfristig
allerdings wuenschenswert) oder "unbeaufsichtigtes" (-m0: Update
muss alle Probleme selber loesen, d.h. gesperrte Saetze oder
Datenbanken nach hinreichend langer Zeit ignorieren oder mit
Abbruch quittieren) Update handelt.

-I / -y: Vermutlich wirklich niemals genutzt worden

-z: Das war kurze Zeit bei aufgebohrten Datenbanken noetig, der Schalter
    sollte sogar ignoriert werden (die Zeiten sind ja schon etwas laenger
    vorbei, wo aus auf-Diskette-pass-Gruenden eine .cLD-Datei nicht
    die vollen 15,x MB gross werden durfte)


> ABER:
> Erforderlich ist die neue Version 32.0 von acon, die ein paar
> kleinere Verbesserungen bietet in den Lock- und if-Funktionen.
> Diese braucht update.job.

diese Verbesserungen muessten vermutlich deutlich dokumentiert
werden.


> Hinsichtlich des Locking ist die von Berger monierte "völlig vergurkte"
> Variante noch drin. Seine eigene war nicht besser, nebenbei gesagt,
> und konnte es auch nicht sein aufgrund noch bestehender interner
> kleinerer Probleme, die wir nun ausgeräumt zu haben hoffen. Sobald
> Berger in dem Punkt ein besseres Codestück liefert, bauen wir das ein;
> sobald er nachvollziehbare Probleme vorlegt, können wir sie lösen.

Ich war Ihnen ja noch eine Differenzierung meines Entsetzensschreis
neulich schuldig geblieben und will das nun nachholen. Vorab allerdings
die Anmerkung, dass mein Wissen um die Datensatzssperren von allegro
so gering ist, dass es mir gar nicht moeglich ist, viel ueber die
Korrektheit auszusagen. Demzufolge ist es eigentlich auch gar nicht
moeglich, damit zu arbeiten.

Folgendes ist mein Wissen um die Sperren:

* PRESTO etc. haben einen Satz gesperrt, wenn sie ihn "in die Bearbeitung
  holten", bei ORDER war dies auch das Verhalten bei Systemsaetzen,
  die im Hintergrund automatisch aktualisiert wurden.

* acon erlaubt (???, erfordert!) das Speichern nur von vorher gesperrten
  Saetzen [xput.rtf bedarf der Interpretation: "put" eines beliebigen
  Satzes ist zwar moeglich, empfiehlt sich aber nur, wenn man weiss,
  dass die Aenderung keine Auswirkung auf den Index hat. Legt man
  Wert auf einen konsistenten Index (und wer taete das nicht), muss
  man vorher sperren. "put unlock" wird beschrieben als Mechanismus,
  fremde Sperren zu durchbrechen, im folgenden wird aber herausgestellt,
  dass es auch noetig ist, um selbst gesperrte Saetze speichern zu
  koennen]
  Wissen darum, ob ein Satz vom aktuellen Job selbst gesperrt wurde,
  scheint acon nicht zu haben, ebenso scheint es keine Moeglichkeit
  zu geben, den Satz beim Speichern gesperrt zu lassen.
  FIXME: in xput.rtf fehlt die "avanti"-Kennzeichnung bei put unlock
  und put free

* anders als PRESTO erlaubt acon das Sperren / Freigeben auf
  zwei Arten: get edit ... put unlock  (Emulation eines Editier-Vorgangs)
  und "set lock = set rec lock ... set unlock = set rec free" (von Lesen
  / Schreiben losgeloeste Aktion)

* a99 kennt ebenfalls "put unlock" (nicht: "get edit") und "set rec fre/loc",
  der Normalmodus ist allerdings Bearbeiten / Speichern ohne vorherige
  Datensatzssperren (Konflikterkennung ueber intern gecachtem Zeitstempel /
  Signatur der Version des Datensatzes, die der Bearbeitung zugrunde lag).
  Da allerdings gesperrte Saetze erkannt werden, erfolgt wohl eine
  Nutzung des Sperr-Mechanismus intern im normalen put-Befehl.

* Das Verhalten beim put von a99 duerfte analog auch fuer Massen-
  aktionen wie Globale Ersetzungen, Globale Manipulationen, flex-Kommando
  "update", Rueckspeichern von Ergebnismengen etc. gelten.

Unklar ist hingegen folgendes:

* Wie verhaelt sich das "update" von acon.

* (Ich finde die Dokumentation dazu nicht mehr, erinnere mich aber
  ungefaehr an folgendes): acon kennt keine sitzungsuebergreifenden
  Datensatzsperren, und gibt daher alle im Job getaetigten Sperren
  am Ende des Jobs notfalls selbsttaetig wieder frei (also weiss es
  doch um "eigene" Sperren?)


Vor allem aber und ganz zentral, sobald fuer eine Datenbank mehr als ein
Lock genutzt wird: Was ist die Semantik, auf welchen Wegen muessen die
Locks erlangt werden?

Wie hier schon oefter dargelegt, sind "lesende" Tests auf Locks Aktionen
ohne jegliche Aussagekraft, da das Ergebnis dieses Tests moeglicherweise
bereits nicht mehr wahr ist, wenn ich an seine Interpretation gehe (und
in diesem Bereich ist bereits die Moeglichkeit fatal, denn beim Locking
geht es um hart beweisbare, absolute Sicherheit um Inkonsistenzen
*ausszuschliessen"). Das korrekte Paradigma ist folgendes: Ich kann mich
um ein Lock bemuehen und bekomme rueckgemeldet, ob ich es nun habe
oder nicht habe. Wenn ich es bekommen habe, darf ich davon ausgehen,
dass es nun niemand anders zugeteilt bekommen kann, allerdings muss ich es
auch irgendwann wieder freigeben. Ein Lock freizugeben, das ich nicht habe,
ist nicht erlaubt (bzw. administrativen Notmassnahmen vorbehalten),
umgekehrt ist es ein absolut gravierender Fehlerzustand, wenn anlaesslich
der Freigabe eines von mir gehaltenen Locks festgestellt wird, dass
es gar nicht mehr bestand.

Locking zu implementieren ist schwer, "tief" unten werden stets Features
des Betriebssystems genutzt, noch tiefer unten solche des Dateisystems
und/oder eigenstaendige, auf Atomizitaet getrimmte Kommandos des
Mikroprozessors. Oder aber man nutzt eine bereits vorhandene Locking-
Funktionalitaet und setzt darauf auf.

Das Datenbank-Lock (.TBL-Sperre) nutzt z.B. ein Feature der Win32-
Betriebssysteme / Dateisystemabstraktion, genannt Byterange-Locks:
Mit Hilfe von Locking-Funktionalitaet Betriebssystems wird also zunaechst
exklusiver Schreibzugriff auf einen bestimmten Speicherbereich der .TBL-
Datei erlangt, damit wird dann das allegro-interne Lock realisiert
(wenn das Betriebssystem mir das Lock zugeteilt hat, *darf* ich im
C-Programm der Anwendung recht gemaechlich und nicht atomar auslesen,
Entscheidungen treffen, aendern, zurueckschreiben, ohne dass mir
jemand dazwischenfunken kann). Wichtig zu beachten ist, dass Sperren
nicht "vereinfacht" aufgehoben werden koennen, fuer die Freigabe
des Datenbank-Locks ist der fuer die Sperrung genutzte Mechanismus
haargenau gleich zu nutzen (nur anschliessend ist die Verarbeitungs-
logik weniger komplex, weil ich keine Schleife fuer den Fall "Operation
wurde verweigert" benoetige, nur eine Aktion fuer die Fehlermeldung
"Datenbank ist inkonsistent, bitte kuemmern")!

Die Datensatz-Sperren von allegro muessen fuer eine sichere Implementation
ebenfalls auf irgendeinen vorhandene Funktionalitaet zurueckgreifen,
da gibt es nun aber mehrere Moeglichkeiten:
- 1. Rueckgriff auf Betriebssystemfunktionalitaet wie fuers Datenbank-Lock
  beschrieben, nur wird dabei ein spezifischer Speicherbereich der .cLD-
  Datei requiriert (ermoeglicht grosse Parallelitaet)
- 2. Rueckgriff aufs Betriebssystem, allerdings wird stets derselbe
  Speicherort in der .TBL-Datei requiriert (durchaus legitim)
- 3. Nutzung des allegro-Datenbanklocks: Um ein Datensatz-Lock zu
  bekommen, muss ich vorher (ggfls. implizit) den exklusiven Schreib-
  zugriff auf "die Datenbank" besitzen.


Weil damit fuer allegro mehr als ein Lock definiert ist, kann es in allen
drei im vorigen Absatz skizzierten Implementierungen sogenannte Deadlocks
geben:

- Prozess A sperrt Datensatz X
- Prozess B sperrt die Satztabelle
- Prozess A benotigt ein Lock auf die Datenbank, um Satz X
  wegzuschreiben (wg. eventueller Umspeicherung und Indexaktualisierung
  ist exklusiver Zugriff in dieser Situation zwingend), kann aber
  nicht, weil Prozess B bereits gesperrt hat
- Prozess B benoetigt ein Lock auf Datensatz X um eine Aenderung
  vorzunehmen, kann aber nicht, weil Prozess A dieses Lock bereits
  haelt.

Die Anwendung muss nun Strategien enwickeln, solche Deadlocks zu
vermeiden:
- Wichtig z.B., dass eine Reihenfolge vorgegeben wird: Werden
  z.B. Satzsperren (das Aufrechterhalten, nicht nur das Erlangen)
  nur erlaubt, wenn gleichzeitig die Datenbank gesperrt ist,
  besteht das Problem nicht (die Loesung ist aber so drastisch,
  dass man gar keine Datensatzsperren mehr benoetigt;-).
  Es sollte aber z.B. festgelegt werden, in welcher Reihenfolge
  man die fuer einen Speichervorgang kurzfristig benoetigten
  zwei Locks auf Datenbank und Datensatz erlangen soll:
  Da eine Datensatzsperre im acon-Kontext durchaus etwas laenger
  erlaubt sein sollte, muss also fuer alle Aktionen von allegro
  die Reihenfolge so sein, bei "put" und vergleichbarem zuerst
  die Datensatzsperre zu erlangen und dann die Datenbanksperre.
  (Bei Methode 3 oben laesst sich das allerdings optimieren,
  aber da ist die Deadlock-Gefahr auch hoeher)

- Ebenfalls wichtig ist, dass bereits zugeteilte Locks zurueck-
  gegeben werden, wenn die Erlangung weiterer benoetigter Locks
  scheitert: Um das Deadlock im obigen Szenario aufzuloesen
  muss entweder Prozess A die Sperre auf den Datensatz wieder
  aufheben, oder Prozess B seine Sperre auf die Datenbank.
  (Da die Datenbanksperre ein Flaschenhals ist, ist das evtl.
  vorzuziehen. Da aber die oben als sinnvoll dargestellte Reihenfolge
  "zuerst lange Sperre auf den Datensatz, danach kurze Sperre auf die
  Satztabelle" ist, muss entweder sichergestellt werden, dass
  niemand bei selbst gesperrter Satztabelle einen Satz sperren
  will (Methode 3 oben scheidet dann aus), oder aber bereits
  begonnene Aktion abgeblasen werden muss ("geht nicht, Satz bereits
  in Bearbeitung")


Soweit die Theorie, nun meine Anmerkungen zum am 10.1. skizzierten
update.job:


    :rloop
    // naechsten Satz einlesen

wirkt harmlos, Abarbeitung Datensatz fuer Datensatz ist der Ansatz
von Update.EXE, der hier emuliert werden soll

    ...

schon weniger harmlos: hier wurde ja irgendwo anhand des
Primaerschluessels ein eventuell vorhandener Satz in der
Datenbank ausfindig gemacht und ebenfalls eingelesen.
Wurde der dabei gesperrt? Wenn nein, werden Bearbeitungs-
konflikte moeglich sein, die spaeter beim Speichern zu
beruecksichtigen sind.


    // Zuerst Satzsperre pruefen, dreimal, erst 2, dann 8 Sek. Pause
    if not Lock jump settbl
    sleep 2000
    if not Lock jump settbl
    sleep 8000
    if not Lock jump settbl
    jump wasLocked

In mehrfacher Hinsicht problematisch: Wir haben den Datensatz also beim
Einlesen offensichtlich nicht gesperrt und versuchen es auch jetzt nicht,
die scheinbar positiven Tests des "if not Lock" geben uns keine
sichere Grundlage fuer die Entscheidung "weiter im Text".

Die Timeouts scheinen mir extrem zu hoch gewaehlt, oder extrem zu niedrig:
Entweder wir haben PRESTO-Clients und muessten eine typische Datensatz-
Bearbeitung plus Mittagspause abwarten, also mindestens einee Stunde
lang nicht aufgeben (und waehrenddessen haeufig testen), oder aber wir
haben keine PRESTO-Clients, dann gibt es Satzsperren nur waehrend
Speichervorgaengen, die Gesamtdauer von 10 Sek. ist da evtl. noch in
Ordnung, aber es sollte wesentlich hochfrequenter getestet werden.
Es empfiehlt sich also der Einsatz einer Schleifenkonstruktion mit
Zaehler.

Aber wie gesagt, so wie es da steht, haben die Tests wenig Aussagekraft
(es koennten ja auch drei verschiedene Bearbeitungen dieses Datensatzes
sein, die wir hier sehen, man denke an datensatzgebundene Order-
Generatoren, in die staendig hineingeschrieben wird).



    // Satz ist nicht gesperrt

Wie oben dargelegt, nur wishful thinking als Arbeitsgrundlage


    :settbl
    // TBL sperren
    set tbl lock
    // zweiter Versuch (nochmal 10 Sek.)
    if no set tbl lock
    // dritter Versuch (nochmal 10 Sek.)
    if no set tbl lock
    // wieder nix, dann aufgeben
    if no jump tblTrouble

Vom Ansatz her korrekt: Wir bemuehen uns um die Sperre, das
genutzte Kommando "set tbl lock" fuehrt dafuer hoffentich
viele Unter-Tests in vernuenftiger Frequenz durch, das braucht
uns also nicht zu kuemmern. 30 Sekunden insgesamt scheinen
mir aber zu knapp (wenn parallel eine Massenaktion laeuft,
wo pro Satz hunderte von Schluesseln geaendert werden, kann
alles lange dauern und ausserdem ist ja keine Queue implementiert,
d.h. es gibt keine Garantie, dass wir die winzige Luecke zwischen
zwei Datensaetzen dieses hypothetischen Parallelprozesses
erwischen: Vorschlag: Mindestens 240 Sekunden, per Zaehler in
Schleife realisiert.



    // Nun den Satz NOCHMAL pruefen
    // - evt. gerade eben anderweitig gesperrt worden!
    // (sog. "race condition") Dann aufgeben

    if Lock jump wasLocked

Wieder nur ein Test.

    // aber nun kann das nicht mehr passieren, weil immer zuerst
    // die TBL gesperrt ist, und das haben wir ja selber gemacht

Dieses "immer zuerst" ist dann korrekt, wenn Datenbanken stets
nur von Update.job's bearbeitet werden oder alle allegro-
Module dieselbe Strategie fahren. Fuer PRESTO ist das nachweislich
falsch.



    // Satz sperren UND Schluessel merken
    // (fuer "put" wichtig, sonst muesste man set lock nicht machen!)
    set lock

Hier ist die entscheidende Aktion. Wenn es ueberhaupt Sinn macht,
auf etwas zu testen, dann HIER.

if yes: Wir (und nicht jemand anders) *haben* nun das Lock wirklich, Hurra!

if no: Entweder jemand anderes haelt das Lock (ein Denkfehler in der obigen
Grundannahme "immer zuerst" ist damit nachgewiesen) oder das Lock wurde
uns aus anderen Gruenden verweigert (.cLD-Datei oder Datenbank schreib-
geschuetzt?)



    // Nun Satz ändern oder was auch immer
     ....

Schwer zu sagen, was hier korrekt ist. Ich gehe davon aus,
dass das eigentliche Merge der beiden Datensaetze bereits
vorher stattgefunden hat und die teure Zeit, in der wir die
Datenbank im Exklusivzugriff haben, nur fuer die optionale
GM-Zusatzparameterdatei genutzt wird: Die greift u.U. auf
den Index zu und da brauchen wir konsistente Resultate.

Andererseits koennte es Bearbeitungskonflikte geben (s.o.)
und wir muessen hier den Satz (erneut?) aus der Datenbank
einlesen und mit dem Satz aus der Update-Datei mergen. Dafuer
genuegt aber ein "einfaches" Lock auf den Datensatz, die
.TBL-Datei dafuer die ganze Zeit in Beschlag zu nehmen, ist
ziemlich viehig.



    // dann speichern
    set tbl free
    put

kein "put unlock" wie es lt. xput.rtf zwingend fuer die
Aktualisierung des Index ist?

Fatal allerdings folgendes: Wir geben die Satztabelle frei
und versuchen sofort danach ein "put", stuerzen uns also
ins Getuemmel um die Satztabelle.

"put free" hingegen wuerde den Satz speichern (und die .TBL
freigeben? Die Dokumentation ist uneindeutig, und vom
Namen des Kommandos soll man sich bekanntlich nicht zu
voreiligen Schluessen verleiten lassen)

Zusammen benoetigt man eigentlich ein "put free unlock", wobei
acon's "put" ohne Zusatzparameter eigentlich selber wissen sollte,
dass sowohl Datenbank- als auch Datensatzsperre in seinem Besitz
sind und nicht aquiriert werden brauchen (und das eigentliche
Freigeben dieser beiden Sperren kann unerwuenscht sein, und
erfolgt dann im Normalfall nicht, ausser eben "free" oder
"unlock" sind angegeben. Das Durchbrechen fremder Sperren
sollte im put-Kontext gar nicht erst angeboten werden).



    if ok jump putok

Das "put" hat mit einer gewissen Hartnaeckigkeit (30 Sek?) versucht,
die Satztabelle zu sperren, denkbar sind aber auch harte Fehler wie
"Platte voll" etc., die nicht unbedingt einen Neuversuch nahelegen.


    sleep 5000
    // zweiter Versuch, nach 5 Sek.
    put
    if ok jump putok

Wie oben: Hektische Aktivitaet des "put" und dann gigantische
5 Sekunden Zwangspause sind kein ueberzeugendes Muster. Die
Tests des "put" sind hoffentlich so wenig aggressiv, dass sie
auch dauernd stattfinden duerfen, analog dem (hier impliziten)
"set tbl lock" bietet sich kontinuierliches Wiederversuchen
fuer mindestens 240 Sekunden an, bevor das Handtuch geworfen wird.

    jump putErr

    :putok

Der Erfolg des put wird nicht protokolliert (Satznr., Schluessel
veraendert, umgepeichert, ...)?
Ist hier der Ort fuer die typischerweise erwuenschte / Konfigurier-
bare Pause zwischen den Datensaetzen?

    jump rloop


    :wasLocked
    //Satz in die Protokolldatei o.a. mit write kn
    wri "Speichern misslungen, Satz gesperrt: " n kn n
    // weiter zum naechsten Satz

Hierhin wird gesprungen von Tests sowohl vor als auch nach dem
Sperren der .TBL-Datei. Dementsprechend darf sie hier nicht
bzw. *muss* sie nun freigegeben werden. Fatal...


    jump rloop

    :putErr
    //Satz in die Protokolldatei o.a. mit write kn
    wri "Speichern misslungen: " n kn n

Die Datensatzsperre besteht allerdings weiter, wohl mindestens
bis zum Ende dieses Jobs in einigen Stunden...


    // weiter zum naechsten Satz
    jump rloop



Fazit: Ohne Wissen um die *vorgeschriebene* (= von allen Anwendungen
einzuhaltene und auch eingehaltene) Reihenfolge bei der Erlangung von
Satz- und Datenbanklocks gibt es keine wirkliche Sicherheit. Mir
scheint aber ein hier nicht implementierter Ansatz mit fruehem
"get edit", moeglichst vielen Manipulationen und anschliessendem
"set tbl lock" nur im Fall einer zugeschalteten Parameterdatei,
ansonsten einfachem "put" (mit implizitem "set rec fre") der
Ressourcenschonendste Weg, auch wenn dafuer mehrfach in der .cLD-
Datei herumgeschrieben wird.

viele Gruesse
Thomas Berger



Mehr Informationen über die Mailingliste Allegro