Napište si debugger PL/pgSQL aneb pokročilé techniky programování v PostgreSQL debugger PL/pgSQL

Z PostgreSQL
Přejít na: navigace, hledání

Schreiben Sie einen PL/pgSQL Debugger, oder Fortgeschrittene Programmiertechniken in PostgreSQL

Versuchen Sie mal, einen eigenen Debugger von gespeicherten Prozeduren zu schreiben. Sie werden bald feststellen, dass es keine einfache Sache ist. Warum? Ein Debugger ist eine typisch interaktive Applikation, und die Umgebung der gespeicherten Prozeduren lässt sich keinesfalls als interaktiv bezeichnen. Außerdem versuchen Sie einen Code abzustimmen, der entfernt durchgeführt wird. Freiheraus gesagt, ohne Anpassung der Ursprungscodes und Erweiterung des Kommunikationsprotokolls haben Sie keine Chance (ich habe einen fast tatsächlichen PL/pgSQL Debugger bereits geschrieben). Wenn Sie sich aber mit bestimmten Einschränkungen abfinden und die Bibliothek von Orafce verwenden, gibt es eine gewisse Hoffnung.

Diesmal begrenze ich die Funktionalität eines Debuggers auf Schrittbetrieb des Codes. Dann ist es die Funktion des Debuggers, die Durchführung des Codes in einem bestimmten Zeitpunkt zu stoppen, die Bedienungsperson über die Erreichung des Unterbrechungspunktes zu informieren und auf Anweisungen der Bedienungsperson zu warten. Es hängt vom Benutzer ab, ob er die abzustimmende Applikation beendet, oder sich den Inhalt der Variablen ausschreiben lässt, oder die Abstimmung fortsetzt. Wieder gibt es da ein kleines Problem. Mit Rücksicht auf die Implementierung von PL/pgSQL ist es nicht möglich, die Variablen zu bekommen. Das tut einem leid. Wir arbeiten aber mit einer Datenbasis, also wir können im Code den Inhalt der Variablen in eine Tabelle speichern und diese dann lesen. Diese Tabelle ist aber aus der Session der abzustimmenden Applikation zu lesen. PL/pgSQL Funktion wird in der Transaktion gestartet, und bis Beendigung (und Bestätigung der Transaktion) sind keine in den Tabellen durchgeführten Änderungen von Außen sichtbar. Es bestehen also zwei Hauptprobleme: a) Anhalten des Codes, b) wechselseitige Kommunikation in der Klient/Server-Umgebung. Falls Sie die Programmiersprache PL/pgSQL kennen, wissen Sie, dass die Lösung dieser Probleme über die Möglichkeiten dieser Sprache hinausgeht. Was sollen wir tun? Relativ kurz steht für PostgreSQL Orafce, Zusatz zur Verfügung, der unter anderem die Implementierung des Pakets enthält dbms_pipe RDBMS Oracle. Die Funktionen aus diesem Paket ermöglichen asynchronische Multisession-Kommunikation, und das brauchen wir. Aus der Sicht des Benutzers ermöglichen sie, benannte Rohre zu errichten und dadurch Kommunikation zwischen zwei Sessions zu realisieren. Die Rohre benehmen sich in RDBMS ähnlich wie Systemschwester. Ein Prozess, der aus einem leeren Rohr zu lesen versucht, wird gestoppt und wartet, bis sich ein anderer Prozess ins Rohr einschreibt. Durch diese Methode kann man die Durchführung der PL/pgSQL Funktion gesteuert stoppen.

Ich erlaube mich noch, die dbms_pipe Bibliothek kurz zu beschreiben. Ein Rohr ist eine Datenabstraktion (ähnlich wie eine Datei), die die Kommunikation zwischen den Klienten der Datenbasis ermöglicht. Grundsätzlich handelt es sich um einen gemeinsam benutzten Speicher, in den sich ein Klient einschreibt und ein anderer davon liest. Die durch das Rohr übertragenen Daten sind strukturiert, d.h. dass jeder Bericht, der im Rohr gespeichert wird, mehrere Posten von verschiedenen Typen haben kann. Der Gesamtprozess der Übertragung verläuft in mehreren Schritten:

  • im Lokalspeicher stellen wir durch Hinzufügen einzelner Posten einen Bericht zusammen,
  • den Inhalt des Lokalspeichers verschieben wir in den gemeinsam benutzten Speicher. Ab diesem Zeitpunkt hat ein beliebiger PostgreSQL Klient Zutritt zum Bericht,
  • den Inhalt des gemeinsam benutzten Speichers verschieben wir ins Lokalspeicher des Klienten und entfernen den Bericht aus dem gemeinsam benutzten Speicher
  • aus dem Lokalspeicher lesen wir den Posten des Berichts stufenweise ein

Sowohl das Rohr als auch der Bericht können als FIFO-Warteschlangen angesehen werden. Jedes Rohr kann von mehreren Klienten gemeinsam benutzt werden. Jeder Bericht wird jedoch nur einmal gelesen, - er wird von demjenigen Klienten gewonnen und aus dem gemeinsam benutzten Speicher entfernt, der ihn als Erster erreicht.

Ich möchte hier die Dokumentation zu dbms_pipe nicht abschreiben, da Sie diese im Internet problemlos herausgooglen können. Zu Beginn begnügen wir uns mit den Funktionen:

dbms_pipe.pack_message(Wert) legt den Wert nach buffer aufgebaute Bericht.
dbms_pipe.send_message(Name_Front) verlagert Nachricht nach benannt Nachrichtenwarteschlange.
dbms_pipe.receive_message(Name_Front) aus benannt Nachrichtenwarteschlange verlagert erste Nachricht nach buffer Lesen Bericht.
dbms_pipe.unpack_message_typ() rückkehrt den Wert gegeben Modell, welche ist erste in Pufferspeicher Lesen Bericht.

Zum Beispiel die Übertragung einer Nummer und eines Textes zwischen zwei angemeldeten Klienten wird durch folgende Reihenfolge der Funktionsaufrufe durchgeführt:

SELECT dbms_pipe.pack_message(0);
SELECT dbms_pipe.pack_message('tschüs');
SELECT dbms_pipe.send_message('mein Röhre');

SELECT dbms_pipe.receive_message('mein Röhre');
SELECT dbms_pipe.unpack_message_number();
SELECT dbms_pipe.unpack_message_text(); 

Jetzt haben wir genug Kenntnisse, um eine Trace-Funktion (...) zu schreiben, die ihre Argumente an den abstimmenden Klienten sendet und vor ihrer Beendigung auf ein Fremdsignal wartet. Die abzustimmende Funktion müssen wir mit einem Trace-Aufruf manuell ergänzen, z.B.:

CREATE OR REPLACE FUNCTION test_loop() 
RETURNS void AS $$
BEGIN
  FOR i IN 1..10 LOOP
    trace('test_loop', 3, i::text);
  END LOOP;
END;
$$ LANGUAGE plpgsql;

Ich brauche zwei Funktionen. Die bereits erwähnte Trace-Funktion() und die Cont-Funktion, die die Abstimmdaten darstellt und die Trace-Funktion vorschiebt (sie signalisiert ihr die Anforderung an ihre Beendigung).

CREATE OR REPLACE FUNCTION trace(_name varchar, _ln integer, _value varchar)
RETURNS void AS $$
BEGIN
  PERFORM dbms_pipe.pack_message(_name);
  PERFORM dbms_pipe.pack_message(_ln);
  PERFORM dbms_pipe.pack_message(_va);
  PERFORM dbms_pipe.send_message(dbms_pipe.unique_session_name()||'$DBG'); --schicken Daten
  PERFORM dbms_pipe.receive_message(dbms_pipe.unique_session_name()||'$DBG$CONT'); -- warte an Signal
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION cont(_pipe varchar, OUT _fn varchar, OUT _ln integer, OUT _value varchar)
RETURNS record AS $$
BEGIN
  -- sind zu Seite Datums?
  PERFORM 1 FROM dbms_pipe.db_pipes  WHERE name = _name AND items > 0;
  IF FOUND THEN
    PERFORM dbms_pipe.receive_message(_pipe||'$DBG');
    _fn := dbms_pipe.unpack_message_text();
    _ln := dbms_pipe.unpack_message_number();
    _value := dbms_pipe.unpack_message_text();
    RETURN;
  ELSE
    -- sie wartet jemand an Signal continue?
    PERFORM 1 FROM dbms_pipe.db_pipes WHERE name = _name || '$DBG$CONT' AND items = 0;
    IF FOUND THEN
      PERFORM dbms_pipe.send_message(_name||'$DBG$CONT');
    END IF;
    -- nochmals warte an Daten
    PERFORM dbms_pipe.receive_message(_name);
    _fn := dbms_pipe.unpack_message_text();
    _ln := dbms_pipe.unpack_message_number();
    _value := dbms_pipe.unpack_message_text();
    RETURN;
  END IF;
END;
$$ LANGUAGE plpgsql;

Der eigentliche Schrittbetrieb kann so erfolgen, dass wir in einem Fenster die abzustimmende Funktion starten, und in einem anderem durch einen Auszug aus der db_pipes Tabelle einen Session-Identifikator gewinnen, und den Aufruf der Cont-Funktion wiederholen. Der Nachteil dieser Version besteht darin, dass wir die Beendigung der Durchführung der abzustimmenden Funktion nicht detektieren können und die Cont-Funktion müssen wir nach dem letzten Zyklus manuell unterbrechen.

postgres=# select test_loop();

postgres=# select * from dbms_pipe.db_pipes;
          name           | items | size | limit | private | owner
-------------------------+-------+------+-------+---------+-------
 PG$PIPE$1$4652$DBG      |     1 |   32 |       | f       |
 PG$PIPE$1$4652$DBG$CONT |     0 |    0 |       | f       |
(2 rows)

postgres=# select cont('PG$PIPE$1$4652');
       cont
------------------
 (test_loop,40,1)
(1 row)

postgres=# select cont('PG$PIPE$1$4652');
       cont
------------------
 (test_loop,40,2)
(1 row)
  
    ...
    
postgres=# select cont('PG$PIPE$1$4652');
Cancel request sent
ERROR:  canceling statement due to user request
CONTEXT:  SQL function "receive_message" statement 1
SQL statement "SELECT  dbms_pipe.receive_message( $1 ||'$DBG')"
PL/pgSQL function "cont" line 3 at perform

Mit einem zustandslosen Kommunikationsprotokoll kommen wir nicht aus. Weitere Funktionen werden sehr kompliziert implementiert. Selbst ein Protokoll mit zwei Zuständen löst die Synchronisierung der Kommunikation. Im ersten Zustand meldet der abzustimmende Klient, dass er in den Unterbrechungszustand geraten ist und er wartet auf einen Befehl. Im zweiten Zustand sendet der abzustimmende Klient das Ergebnis des bearbeiteten Befehls ab. Die gegenseitige Kommunikation kann mit dem folgenden Schema beschrieben werden:

A, Ich geriet zu               B, Warte bis sich Klient sagt um Befehl 
   Zwischenstopp,                dann ihm ihn schicken.
   warte auf Anordnung           Warte an Ergebnis   

A, Hat verarbeitet bin Befehl, B, Zeige Ergebnis und beenden
   schicke Ergebnis und 
   beenden

Prozess A – der abzustimmende Klient, Prozess B – der Abstimmklient. Wenn ich nicht versuchen würde, den Debugger mit PL/pgSQL zu schreiben, würde ich Prozess B als eine Schleife implementieren, die die Darstellung der Abstimminformationen, Gewinnung der Reaktion vom Benutzer und Bearbeitung des Eintritts enthält. Da die gespeicherten Prozeduren grundsätzlich kein Mittel anbieten, um eine Interaktion mit dem Benutzer sicherzustellen (Vor kurzem wurde in einer Konferenz die Frage gestellt, wie man in gespeicherten Prozeduren Message Box und InputBox implementieren kann. Die Antwort: Es ist prinzipiell nicht möglich.), kann dieses Schema nicht verwendet werden. Ich muss alle Daten vom Benutzer bereits zur Zeit des Aufrufs der Funktion haben. Deshalb habe ich die Cont-, Exec- und Stop-Funktionen geschrieben.

CREATE OR REPLACE FUNCTION trace(_desc varchar, _ln integer, _value varchar)
RETURNS void AS $$
  DECLARE
    _request integer;
    _r record;
    _v varchar;
BEGIN
  PERFORM dbms_pipe.pack_message(0);
  -- schicken info Substitution Zwischenstopp und warte auf Anordnung 
  PERFORM dbms_pipe.send_message(dbms_pipe.unique_session_name()||'$DBG');
  PERFORM dbms_pipe.receive_message(dbms_pipe.unique_session_name()||'$DBG$CONT');
  _request := dbms_pipe.unpack_message_number();
  PERFORM dbms_pipe.pack_message(1);
  PERFORM dbms_pipe.pack_message(_desc);
  PERFORM dbms_pipe.pack_message(_ln);
  IF _request = 1 THEN -- zurückgeben Parametern
    PERFORM dbms_pipe.pack_message(_value);
  ELSIF _request = 2 THEN -- ausführen Frage
    -- für pretypovani record->varchar es ist notwendig benutzt PL/pgSQL Konversion
  EXECUTE dbms_pipe.unpack_message_text() INTO _r;
    _v := _r; PERFORM dbms_pipe.pack_message(_v);
  ELSIF _request = 3 THEN -- Abschluß Stimmung
    PERFORM dbms_pipe.pack_message('Stop debuging');
    PERFORM dbms_pipe.send_message(dbms_pipe.unique_session_name()||'$DBG');
    RAISE EXCEPTION 'Stop debuging';
  END IF;
  -- schicken Daten
  PERFORM dbms_pipe.send_message(dbms_pipe.unique_session_name()||'$DBG');
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION cont(_pipe varchar, OUT _desc varchar, OUT _ln integer, OUT _value varchar)
RETURNS record AS $$
 declare  i integer;
BEGIN
  PERFORM dbms_pipe.receive_message(_pipe||'$DBG');
  IF 0 <> dbms_pipe.unpack_message_number() THEN
    RAISE EXCEPTION 'Synchronisation error';
  END IF;
  PERFORM dbms_pipe.pack_message(1);
  PERFORM dbms_pipe.send_message(_pipe||'$DBG$CONT');
  PERFORM dbms_pipe.receive_message(_pipe||'$DBG');
  IF 1 <> dbms_pipe.unpack_message_number() THEN
    RAISE EXCEPTION 'Synchronisation error';
  END IF;
  _desc := dbms_pipe.unpack_message_text();
  _ln := dbms_pipe.unpack_message_number();
  _value := dbms_pipe.unpack_message_text();
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION stop(_pipe varchar, OUT _desc varchar, OUT _ln integer, OUT _value varchar)
RETURNS record AS $$
 declare  i integer;
BEGIN
  PERFORM dbms_pipe.receive_message(_pipe||'$DBG');
  IF 0 <> dbms_pipe.unpack_message_number() THEN
    RAISE EXCEPTION 'Synchronisation error';
  END IF;
  PERFORM dbms_pipe.pack_message(2);
  PERFORM dbms_pipe.send_message(_pipe||'$DBG$CONT');
  PERFORM dbms_pipe.receive_message(_pipe||'$DBG');
  IF 1 <> dbms_pipe.unpack_message_number() THEN
    RAISE EXCEPTION 'Synchronisation error';
  END IF;
  _desc := dbms_pipe.unpack_message_text();
  _ln := dbms_pipe.unpack_message_number();
  _value := dbms_pipe.unpack_message_text();
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION exec(_pipe varchar, _query varchar, OUT _desc varchar, OUT _ln integer, OUT _value varchar)
RETURNS record AS $$
 declare  i integer;
BEGIN
  PERFORM dbms_pipe.receive_message(_pipe||'$DBG');
  IF 0 <> dbms_pipe.unpack_message_number() THEN
    RAISE EXCEPTION 'Synchronisation error';
  END IF;
  PERFORM dbms_pipe.pack_message(5);
  PERFORM dbms_pipe.pack_message(_query);
  PERFORM dbms_pipe.send_message(_pipe||'$DBG$CONT');
  PERFORM dbms_pipe.receive_message(_pipe||'$DBG');
  IF 1 <> dbms_pipe.unpack_message_number() THEN
    RAISE EXCEPTION 'Synchronisation error';
  END IF;
  _desc := dbms_pipe.unpack_message_text();
  _ln := dbms_pipe.unpack_message_number();
  _value := dbms_pipe.unpack_message_text();
END;
$$ LANGUAGE plpgsql;

Die Abstimmung der gespeicherten Prozeduren ist nicht der einzige Bereich, wo Vložit sem neformátovaný textIntersession-Kommunikation eingesetzt werden kann. Oft wird sie zur gegenseitigen Synchronisierung von Prozeduren oder zur Realisierung der Klient-Server-Architektur verwendet (PostgreSQL unterstützt autonome Transaktionen leider nicht, ich kann mir also eine Klient-Server-Applikation über PL/pgSQL in der Praxis nicht vorstellen. Noch nicht.).

Ich mache mir keine Illusionen davon, dass ich einen verwendungsfähigen Debugger geschrieben habe. Das Kommunikationsprotokoll ist primitiv, ohne die Möglichkeit der Resynchronisierung, die Befehlmenge ist minimal. Ein hoch entwickeltes Protokoll bedeutet jedoch mehr Code und somit geringere Übersichtlichkeit und Deutlichkeit des Codes. Grundsätzlich wollte ich eher die Möglichkeiten der neuen Orafce-Bibliothek zeigen, als einen eigenen Debugger schaffen. Wobei jedoch nicht ausgeschlossen ist, dass jemand meinen Prototyp-Debugger verwenden wird. Ich selbst habe im Laufe der Jahre gelernt, ohne den Debugger zu leben und mich mit RAISE NOTICE zu begnügen.