AGB  ·  Datenschutz  ·  Impressum  







Anmelden
Nützliche Links
Registrieren
Zurück Delphi-PRAXiS Programmierung allgemein Datenbanken Hunderte Clients im Sekundentakt über gesperrte Datensätze informieren
Thema durchsuchen
Ansicht
Themen-Optionen

Hunderte Clients im Sekundentakt über gesperrte Datensätze informieren

Ein Thema von Papaschlumpf73 · begonnen am 19. Jun 2024 · letzter Beitrag vom 21. Jun 2024
Antwort Antwort
Seite 3 von 3     123   
Benutzerbild von Jasocul
Jasocul

Registriert seit: 22. Sep 2004
Ort: Delmenhorst
1.355 Beiträge
 
Delphi 11 Alexandria
 
#21

AW: Hunderte Clients im Sekundentakt über gesperrte Datensätze informieren

  Alt 20. Jun 2024, 15:31
Würde es nicht reichen, die Sperrung beim Versuch, den Datensatz zu bearbeiten, zu überprüfen?

Es mag vielleicht von dem konkreten Anwendungsfall abhängen, aber ich finde eine Live-Aktualisierung der aktuellen Sperrungen im Sekundentakt schon etwas übertrieben.
Finde ich nicht ganz so toll und würde ich so auch nicht anbieten wollen. Der Anwender würde eine lange Liste mit Datensätzen sehen. Jetzt klickt er einen an und merkt erst dann, dass dieser DS gesperrt ist, beim nächsten Datensatz dann ggf. das gleiche nochmal. Und beim Scrollen durch die Liste (Bleifinger auf CURSOR_DOWN) werden dann die Prüfungsanfragen an den SQL-Server im 100-Millisekundentakt abgefeuert.
Ich denke, dass das anders gemeint war. Wenn ein DS gesperrt ist, gibt es ein Refresh aller Datensätze. Dann muss nicht jeder einzelne geprüft werden.
Peter
  Mit Zitat antworten Zitat
Benutzerbild von Jasocul
Jasocul

Registriert seit: 22. Sep 2004
Ort: Delmenhorst
1.355 Beiträge
 
Delphi 11 Alexandria
 
#22

AW: Hunderte Clients im Sekundentakt über gesperrte Datensätze informieren

  Alt 20. Jun 2024, 15:47
Gerade viel mir ein, dass ich vor ca. 20 Jahren ein ähnliches Problem hatte.
Meine damalige Lösung:
Sobald ein Datensatz gespeichert wird, wird die gesamte Liste aktualisiert und bearbeitete Datensätze hatten eine Markierung. Die Anwender konnten über einen Schalter auch die erledigten Datensätze ausblenden. Natürlich war der zuletzt bearbeitete Datensatz immer noch selektiert, damit man nicht wieder alles durchblättern musste.
Der wesentliche Vorteil war, dass nach dem Refresh auf jeden Fall der aktuellste Stand verfügbar war und ein ständige Aktualisierung entfallen konnte.
Peter
  Mit Zitat antworten Zitat
johndoe049

Registriert seit: 22. Okt 2006
170 Beiträge
 
#23

AW: Hunderte Clients im Sekundentakt über gesperrte Datensätze informieren

  Alt 20. Jun 2024, 15:59
Das ist ein "Pick from the Rest" Verfahren.
Gibt es bei Maschinenzuordnungen, wo sich n Maschinen aus einem Stückpool sich was zur Bearbeitung picken und die Restmenge angezeigt wird.


Wie wäre es damit:


Neue Transaktionstabelle erstellen. Inhalt:
REC ID
In Process by User
Start Date/Time (zur Deadlock Erkennung)

HeaderEintrag, z.B. mit RecID -1 für den letzten Änderungseintrag. Das kann man mit wenig Zeitaufwend sekündlich abfragen (select ProcessingUser from Picklist where TargetID = -1). Ändert sich der Wert, kann man die Liste neu abrufen.

Bei neuem Änderungsdurchlauf alle Datensatz IDs in diese Tabelle laden.

Einer der Mitarbeiter pickt sich was raus. Alle anderen sehen den Rest (select * from picklist where ProcessingUser = NIL)
Ist die Bearbeitung abgeschlossen, wird der Eintag aus der Tabelle gelöscht.

Dadurch wird die Liste immer kürzer und es muss immer weniger geprüft, übertrag, etc. werden.
  Mit Zitat antworten Zitat
Benutzerbild von IBExpert
IBExpert

Registriert seit: 15. Mär 2005
679 Beiträge
 
FreePascal / Lazarus
 
#24

AW: Hunderte Clients im Sekundentakt über gesperrte Datensätze informieren

  Alt 21. Jun 2024, 07:12
achtung, langer text, vorn ein paar Erfahrungen, weiter hinten ein paar konkrete Vorschläge

Basierend auf Erfahrungen von einem vor langer Zeit von uns erstellten Projekt
für einen sehr großen Konzern, bei dem 6 s2m isdn Karten mit je 30 isdn Kanälen
gesteuert werden mussten und die vorherige Lösung (die nicht von uns kam) eine
komplette Fehlkonstruktion war:

Es ging um die Umsetzung einer callbox, sozusagen ein Anrufbeantworter, nur umgekehrt,
der nahm keine Anrufe an, sondern musste einen freien der 180 isdn Kanäle auswählen
und diesem dann eine passende Sounddatei vorspielen. Im Protokoll sollte dann enthalten
sein, ob der Angerufene abgehoben hat, wie lange der Call aktiv war und noch ein paar
andere Metadaten. Wenn abgehoben, dann wird der nicht wieder angerufen. Wenn nicht
wird es je nach Auftrag nach x Minuten noch mal versucht.

Was war das Problem der vorherigen Lösung: Ein Programmierer des Konzerns hatte dafür
eine C++-Anwendung geschrieben, die Daten aus einer Postgres-Datenbank bekam und sich
dort per random einen Call aus der Call Tabelle holte, versuchte diesen zu sperren,
diesen auf dem isdn kanal zu verarbeiten und dann nach Durchführung die Metadaten zurück
in die Datenbank zu schreiben. Auch nicht erfolgreiche sperren wurden versucht, in die
datenbank zu schreiben.

Das ganze System sollte ausgelegt werden für 50000 ausgehende Anrufe pro Tag und als
Zeitraum war nur zwischen 08:00 und 20:00 wochentags erlaubt, also ein halber Tag.

Das der gesamte Tag 86400 Sekunden hat, wissen die meisten von uns, es bleiben also
nur 43200 Sekunden für 50000 Anrufe, also weniger als eine Sekunde pro Anruf, die
aber leider inkl. der tüdelüt Melodie am anfang und am ende alle zwischen 25 und 35
sekunden braucht. Typischer Inhalt: Wir haben ihren auftrag erhalten und bearbeiten
den so schnell wie möglich, ...

Die Auftragsdaten kamen in die Datenbank von einer Konzern SAP Schnittstelle, an die
der Inhalt der o.a. Metadaten dann später auch zurückgehen sollte.

Was war das Problem der gewählten Anwendung c++? Der client wurde auf dem master control
server in der anzahl der verfügbaren kanäle gestartet, also liefen ca 180 parallel laufende
executables auf einer linux maschine, die schon ziemlichen dampf hatte. Wenn die nix machen
wäre das auch kein problem.

Gleichzeitig lief dort aber auch die Postgres Datenbank, mit der sich alle
clients verbunden haben. Wenn ein call rausging, sorgte die executable dafür,
das der s2m controller server, auf dessen kanal der Client konfiguriert war, dieser das
dann erledigen sollte. Die s2m hardware einsteckkarte war auch wohl high end, dir hatten davon
eine im office, angeblicher wert ca 10000 € (wir hatten zwar keinen s2m anschluss, aber für die
programmierung ging das mit der karte in einem Simulationsmodus ganz gut und beim endkunden
hatten wir zugriff auf eine reale maschine mit realen s2m).

Um nicht zu weit auszuholen: die vorherige Anwendung war nach dem Serverstart innerhalb
von 30-45 minuten je nach anzahl der Aufträge als Datenbank nicht mehr erreichbar, weil
alle clients wie bekloppt immer die gesamten auftragsmenge runtergeladen habe, sich im
client per random einen ausgesucht haben und den dann versuchten zu sperren. Das führte
bei der Konstellation beim vorherigen Entwickler mit 50 Aufträgen und gerade mal 10 aktiven
isdn kanälen zu ganz guten ergebnissen im testbetrieb, so das man damit versuchte, das
System scharf zu schalten.

Der reale Dateneingang, der auch schon bescheiden programmiert war, sorgte aber
mit allen aktiven 180 Kanal executables dafür, das nicht mal mehr simple sql befehle vom
postgres server innerhalb akzeptabler zeit beantwortet wurden (auch nach 60 minuten kam
das zeitweise keine antwort). Das System lief komplett amok und wurde dafür dann wohl sogar in
der Presse bekannt, das es eine Arztpraxis 24 Stunden am Tag ca alle 3-5 Minuten angerufen
hat und egal, ob die abgenommen haben oder nicht, danach wieder angerufen wurde. Das ging
wohl mehrere Tage so, obwohl angeblich die Daten aus dem System entfernt wurden. Da es
auch ncoh die Notrufnummer dieser Arztpraxis war, konnte man die auch nicht so einfach
abschalten. Man entschied sich dann nach dem Prssebericht in dem Konzern, die Lösung
komplett abzuschalten, weil die auch nach mehreren reboots aller beteiligten Systeme
der unkontrollierbare Amoklauf erneut losging.

Die Technik (serverhardware) war auf jeden fall nicht schuld und als wir dann mit dem damaligen
Projektleiter, zu dem wir im Rahmen eines anderen Projekts schon einen sehr guten Kontakt hatten,
zu einer gemeinsamen Analyse des vorhandenen Systems eingeladen wurden, konnte ich mir mehrfach
das schmunzeln nicht verkneifen. Alle Clients holten sich im Sekundentakt immer alle Aufträge
in den eigenen Speicher von der Datenbank, es gab ein optimistisches locking, d.h. ob ein auftrag
evtl schon von jemand gesperrt war oder nicht war dem client nicht bekannt, er versuchte einfach
einen lock auf einen auftrag mit einem update. wenn der klappt, legte der los usw. Wenn der aber nicht klappt weil
schon von jemand anders gesperrt solte der client sich die auftragsliste noch mal neu und es ging
von vorne los (ohne in irgnedeiner art einen timeout zu haben).

Ihr ahnt es, wenn 10000 calls in der Auftragsliste sind, ist für alle genug frei, zufällig was
zu erwischen, was frei ist. wenn aber nur 20 oder 30 calls drin sind, laufen ca 150 von 180 clients
komplett amok und bringen jede datenbank an die Belastungsgrenze oder wie in diesem Fall deutlich
darüber hinaus.

Wir haben das dann so gemacht, das wir statt postgres firebird genommen haben, weil wir uns damit besser
auskennen und direkt mit einem eintrag eines neuen callauftrags von der sap schnittstelle sorgte
ein trigger auf dem server dafür, das dieser schon einem kanal zugeordnet wird, der den abarbeiten
soll.

python scripte auf den servern mit dem s2m karten, für die es dafür saubere apis gab, holten
sich immer nur den jeweils obersten eintrag aus der firebird db aus seiner kanal queue, vermerkt
diesen einen datensatz mit einem Insert in eine andere Tabelle als "in arbeit" (also kein update
auf dem record selbst), arbeitet den ab und verschiebt den nach ausführung in eine andere
tabelle und trägt die metadaten in der job ausführungstabelle per insert nach.

Wenn nix in seiner queue ist macht der client auch nix und wartet 10 sekunden. Wenn was gerade
abgearbeitet wurde, wartet der aber nicht, sondern holt sich den nächsten call sofort. Wenn
keiner da ist, dann eben wieder warten.

inserts sind immer pflegeleichter für multithreaded datenbanken, weil die nix sperren müssen im gegenatz
zu updates.

Eine Datensatzsperre muss nicht zwingend das sein, was die Datenbank dafür fordert
(so was wie in firebird zB mit "select ... for update" oder direkter update), weil das hässliche
nebeneffekte haben kann, wie zum Beispiel erforderliche langlaufende Transaktionen für die dauer
der sperre.

Eine extra lock tabelle, die von dir selber erstellt wird mit unique pk von der
datentabelle und timestamp/user/ip/connection id/was auch immer kannst du auch
extra selber erstellen und via trigger dafür sorgen, das echte updates auf der eigentlichen
Tabelle nur dann erlaubt sind, wenn zu den metadaten vom aktuellen client die in der lock tabelle
übereinstimmen.

So kann ein Lock auch mal minuten, stunden oder sogar tage gültig sein, ohne
irgendwas blockierendes machen zu müssen wie transaktionen lang offen halten. Und ganz nebenbei
kannst du darüber dann auch gleich anzeigen, wer was gerade gelockt hat und seit wann und wenn
das gewünscht ist kann ein andere client einen fremden lock übernehmen oder löschen.

Wenn eine Queue größer wird und warum auch immer nicht sauber abgearbeitet wird (s2m server reboot oder kaputt zB)
dann wird der inhalt von seiner queue von einem server prozess, der als sp implementiert war, auf andere
aktive queues verschieben, sofern die noch keine exec job log eintrag haben.

Die Architektur was also eigentlich zentral von der firebird db gesteuert und nicht zufallsbedingt
von hunderten Clients, die sich alle gegenseitig dauernd blockieren.

Mich erinnerte die Anforderung deines Kunden sehr stark an die Programmierung, die unser Vorgänger
da verbrochen hat. Hundert Clients wühlen sich durch die Daten, sucht sich irgendwas nach Lust und
Laune zufällig aus, sperren das dann mit updates usw.

Die Realität selbst in sehr großen Callcentern, die sich solche Forderungen gerne mal ausdenken,
kann man oft in entsprechende Bahnen lenken, in dem man ähnlich wie den o.a. isdn kanälen
automatische serverbasierende regeln hinterlegt, weil es auch keinerlei sinn ergibt, das sich 100
call center agents auf dem Screen alle die ersten 50 aktiven datensätze anglotzen, um dann irgendwas
anzuklicken, was jemand anders aber vielleicht auch schon gemacht hat. 50 records im sekundentakt
sind im durchschnitt 20ms zeit pro record. Es wäre kompletter Unsinn, die Arbeitszeit da von
hunderten Mitarbeitern zu verplempern, in dem hunderte den selben kram auf dem screen sehen, von denen
mit sehr hoher wahrscheinlichkeit schon alles gesperrt ist, wenn das dann auch ncoh wild herumflackert
wird es noch verrückter. Clientbasierte filter, die tausende records vom server holen und erst lokal
ausfiltern sind da auch ein NoGo in Enterpriseumgebungen.

Mein tip also: versuch erst mal dem Kunden die Probleme seiner Idee zu verdeutlichen und versuch
es gar nicht erst, das mit hunderten client executables zu implementieren, die das selber
unkoordiniert für sich entscheiden.

In der Firebird Welt wäre in meiner Umsetzung ein zentrales Regelwerk für die Queue Zuordnung automatisiert
per trigger o.ä. zuständig und wenn ein client dann was für ihn relevantes neues sehen soll, wird der per
event alterter benachrichtigt (zB weil sein username als eventname benutzt wird) und nur der, für den es was
relevantes neues gibt, holt sich dann die notwendigen daten neu (zB nur die sperrmerkmale infos
der für ihn relvanten records mit pk der zugehörigen daten,wie zum Beispiel adressen).

alle schon bekannte daten kann man lokal vorhalten und muss nicht jedes mal den gesamten kram neu laden.

Lade die die anzuzeigenden Daten nur dann neu wenn das erforderlich ist und trenne das ganz sauber vom
laden der statusinfos, um zu wissen welchen status 10000 records haben brauchst du nichts anderes als
die pks der daten mit dem zugehörigen pk des zustands. wenn da ein pk kommt, den dein client noch nicht
kennt kann der die details dazu ja noch mal extra holen.

und ganz wichtig: wenn der client keine lebenszeichen vom anwender erkennt, weil weder mousemove noch keypress
erkannt wurde, muss man auch nichts in echtzeit aktualisieren, dann reicht auch refresh im minutentakt.

wenn man das gut designt bekommt man mit brauchbarer Firebird Server Hardware ziemlich problemlos 5000
sqls pro Sekunde vom server beantwortet, auch weil der meiste kram im Rahmen von readonly Transaktionen
geholt werden kann.

wenn das nicht reicht kann man das über replikation auf mehrere Server verteilen, die dann je 5000 sqls pro sekunde
können.

dafür einen chatserver oder ähnliches dazwischen zu packen wäre für mich keinesfalls ein vorteil, die machen
oft am ende noch dinge, die du dann gar nicht mehr unter kontrolle hast. versuch die relevanten infos vom
datenbankserver auf das minimum einzuschränken,also mit sqls das zu holen oder zu senden was notwendig ist.
Verlass dich nicht auf Komponenten, das die schon nur das machen was notwendig ist, das ist keineswegs der
Fall (beispiel dataset recordcount, db lookup comboboxen und fields usw).

über umsetzungen auf basis von ado/datasource/dataset/dbgrid usw wird dir das Projekt unter vollast
sicherlich früher als später um die Ohren fliegen. Da sorgt oft der client serveseitig für deutlich
mehr last als man denkt.

da macht das auch keine unterschied ob es mssql oder firebird ist.

Vielleicht sind in meinem langen post aber ja schon ein paar Anregungen für dich dabei,
wenn dir der ganze text nicht zu lang war.

p.s.: in dem o.a. Projekt, bei dem eigentlich 50000 calls als maximum pro Tag vorgesehen waren, fanden
wir ca. 9 monate, nachdem das projekt live ging, aufgrund eines Streiks der Konzernmitarbeiter an einem Tag
sogar 500000 calls, die da erfolgreich abgewickelt wurden. Da war man auf beiden seiten sehr froh, das
selbst damit das von uns implementierte Gesamtsystem nicht überfordert war.
Holger Klemt
www.ibexpert.com - IBExpert GmbH
Oldenburger Str 233 - 26203 Wardenburg - Germany
IBExpert and Firebird Power Workshops jederzeit auch als Firmenschulung
  Mit Zitat antworten Zitat
Papaschlumpf73

Registriert seit: 3. Mär 2014
Ort: Berlin
442 Beiträge
 
Delphi 12 Athens
 
#25

AW: Hunderte Clients im Sekundentakt über gesperrte Datensätze informieren

  Alt 21. Jun 2024, 09:43
Ich versuche mal eine kurze Zusammenfassung:
  • Nur beim Start der Clientanwendung wird die Liste mit den 10.000 Datensätzen in ein 2D-Array geladen und dann in einer ListView angezeigt
  • Auf dem SQL-Server wird eine separate Sperrliste inkl. RecID, Sperrstatus (gesperrt/aufgehoben) und Timestamp geführt.
  • Jede Clientanwendung trägt neue Sperrungen oder Aufhebungen in die separate Sperrliste per StoredProcedure ein
  • Alle Clientanwendungen fragen die Sperrliste regelmäßig oder bei Bedarf über eine StoredProcedure ab und ermitteln alle Änderungen (Sperrungen/Aufhebungen) seit dem letzten Abruf der Sperrliste (Letzter Timestamp als Parameter).
Ein Problem habe ich aber immer noch:
Stellen wir uns das mal praktisch vor: In einer Kiste liegen viele Teile, die ziemlich zügig und von vielen Mitarbeitern bearbeitet werden müssen. Ein Mitarbeiter nimmt sich ein Teil aus der Kiste um es zu bearbeiten. Und schon in diesem Augenblick muss feststehen (er sieht es ja auch), dass kein anderer Mitarbeiter dieses Teil in der Hand hat. Würde er es erst merken, wenn er das Teil in der Hand hält und müsste es dann zurücklegen, wäre wertvolle Arbeitszeit verschwendet worden. Daher müsste unsere Clientanwendung immer sehr zeitnah über Sperrungen informiert werden.

Und das habe ich euch verschwiegen: Unsere Datenbanken laufen auf SQL-Servern, auf denen noch zahlreiche andere Datenbanken der Kunden laufen. Wenn jede Clientanwendung 1 x pro Sekunde die Sperr-Procedure abfragen würde, wären das locker 100 Abfragen pro Sekunden oder 6.000 pro Minute. Selbst wenn das hyper-performant wäre, würde der SQL-Admin schon bei der kleinsten Störung irgendeiner DB bei mir anrufen, weil auf unserer DB so viel Betrieb ist.

Daher hatte mir die UDP-Broadcast-Idee ziemlich gut gefallen - wenn sie denn zuverlässig funktionieren würde. Damit gäbe es dann eine aktive Sperrbenachrichtigung und im Netzwerk würde sich nur etwas tun, wenn jemand tatsächlich arbeitet. Ansonsten wäre Funkstelle.

Sieht so aus, als müsste man dann doch einen separaten TCP-Server oder so basteln, an dem sich alle Clients anmelden und aktiv über die Sperrungen benachrichtigt werden. Das scheint mir dann aber doch etwas zu aufwändig für so verrückte Kundenideen.

Nochmals vielen Dank für eure Vorschläge.
  Mit Zitat antworten Zitat
Benutzerbild von gubbe
gubbe

Registriert seit: 8. Okt 2005
Ort: Schleswig-Holstein
137 Beiträge
 
Delphi 11 Alexandria
 
#26

AW: Hunderte Clients im Sekundentakt über gesperrte Datensätze informieren

  Alt 21. Jun 2024, 10:11
Ein Problem habe ich aber immer noch:
Stellen wir uns das mal praktisch vor: In einer Kiste liegen viele Teile, die ziemlich zügig und von vielen Mitarbeitern bearbeitet werden müssen. Ein Mitarbeiter nimmt sich ein Teil aus der Kiste um es zu bearbeiten. Und schon in diesem Augenblick muss feststehen (er sieht es ja auch), dass kein anderer Mitarbeiter dieses Teil in der Hand hat. Würde er es erst merken, wenn er das Teil in der Hand hält und müsste es dann zurücklegen, wäre wertvolle Arbeitszeit verschwendet worden. Daher müsste unsere Clientanwendung immer sehr zeitnah über Sperrungen informiert werden.
Genau deshalb ist die Vorgabe des Kunden nicht durchdacht. Nehmen wir mal an, es gibt nur noch wenige zu bearbeitende Datensätze, dann steigt die Wahrscheinlichkeit für diese Kollisionen stark an. Aber auch schon allein, dass jeder Mitarbeiter die komplette Liste von 10.000 Datensätzen sieht ist blödsinnig. Nach welchen Kriterien sucht er sich denn jetzt einen Datensatz aus und was hat es für einen Vorteil gegenüber einer einfachen Zuteilung? Selbst wenn die Aktualisierung zeitnah passiert, verschwendet ein Mitarbeiter schon allein mit dem Aussuchen eines Datensatzes Arbeitszeit. Wenn er sich dann einen ausgesucht hat und er in diesem Momentan als gesperrt gekennzeichnet wird, muss er sich wieder einen neuen Suchen usw.

Wir machen es jetzt mal ganz anders, ist schnell gesagt.. Vielleicht sollte der Auftraggeber sich das noch mal in der Praxis vorstellen, bevor es ein teures Experiment wird.
  Mit Zitat antworten Zitat
Benutzerbild von gubbe
gubbe

Registriert seit: 8. Okt 2005
Ort: Schleswig-Holstein
137 Beiträge
 
Delphi 11 Alexandria
 
#27

AW: Hunderte Clients im Sekundentakt über gesperrte Datensätze informieren

  Alt 21. Jun 2024, 10:20
Mal so als Vorschlag ein Kompromiss:
Jeder Mitarbeiter bekommt eine eingeschränkte Zahl an Datensätzen angezeigt, die für ihn reserviert werden. Aus denen sucht er sich einen aus und in diesem Moment werden die restlichen wieder freigegeben. Ist er fertig, bekommt er wieder welche zur Auswahl. Das würde den Kundenwunsch berücksichtigen, verschwendet aber nicht so viel Arbeitszeit und Ressourcen, weil er nur z.B. 10 statt 10.000 Datensätzen zur Auswahl hat und ist technisch einfacher umzusetzen.
  Mit Zitat antworten Zitat
Antwort Antwort
Seite 3 von 3     123   


Forumregeln

Es ist dir nicht erlaubt, neue Themen zu verfassen.
Es ist dir nicht erlaubt, auf Beiträge zu antworten.
Es ist dir nicht erlaubt, Anhänge hochzuladen.
Es ist dir nicht erlaubt, deine Beiträge zu bearbeiten.

BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus.
Trackbacks are an
Pingbacks are an
Refbacks are aus

Gehe zu:

Impressum · AGB · Datenschutz · Nach oben
Alle Zeitangaben in WEZ +1. Es ist jetzt 03:03 Uhr.
Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz