Napište si debugger PL/pgSQL aneb pokročilé techniky programování v PostgreSQL
Napište si debugger PL/pgSQL aneb pokročilé techniky programování v PostgreSQL
Zkuste si napsat vlastní debugger uložených procedur. Brzo zjistíte, že to není žádná legrace. Proč? Jednak debugger je typicky interaktivní aplikace, a prostředí uložených procedur se ani náhodou nedá označit jako interaktivní. Navíc se snažíte ladit kód, který je prováděn vzdáleně. Řeknu to rovnou, bez úpravy zdrojových kódů a rozšíření komunikačního protokolu nemáte šanci (jeden skoro skutečný debugger PL/pgSQL jsem už napsal). Když se ale smíříte s určitými omezeními a pokud použijete knihovnu orafce, je tu jistá naděje.
Pro tuto chvíli zůžím funkcionalitu debuggerů na krokování kódu. Pak je funkcí debuggeru zastavit v určitém bodě provádění kódu, informovat obsluhu o dosažení bodu přerušení a čekat na pokyny obsluhy. Je na uživateli, zda-li ukončí laděnou aplikaci, nebo si nechá vypsat obsah proměnných, nebo bude pokračovat dál v ladění. Opět je tu menší zádrhel. Vzhledem k implementaci PL/pgSQL neexistuje způsob, jak se dostat k proměnným. To zamrzí. Pracujeme ale s databází, takže můžeme v kódu obsah proměnných uložit do tabulky a tu si přečíst. Tuto tabulku musíme ale číst z session laděné aplikace. PL/pgSQL funkce se spouští v transakci, a do ukončení (a potvrzení transakce) nejsou jakékoliv provedené změny v tabulkách zvenčí viditelné. Takže tu máme dva hlavní problémy: a) pozastavení kódu, b) obousměrná komunikace v prostředí klient/server. Pokud znáte programovací jazyk PL/pgSQL, tak víte, že řešení těchto problémů je nad možnosti tohoto jazyka. Takže jak? Relativně krátce je k dispozici pro PostgreSQL doplněk Orafce, který, kromě jiného, obsahuje implementaci balíčku dbms_pipe RDBMS Oracle. Funkce z tohoto balíčku umožňují asynchronní multisession komunikaci, což je přesně to, co potřebujeme. Z hlediska uživatele umožňují zakládat pojmenované roury a jejich prostřednictvím realizovat komunikaci mezi dvěma sessions. Roury v RDBMS se chovají stejně jako systémové sestřičky. Proces, který se snaží číst z prázné roury je zastaven a čeká až se do roury jiný proces zapíše. A to je způsob, jak dokážeme řízenně zastavit provádění PL/pgSQL funkce.
Ještě si dovolím stručně popsat knihovnu dbms_pipe. Roura je datová abstrakce (něco jako soubor) umožňující komunikaci mezi klienty databáze. V podstatě se jedná o sdílenou pamět, do které jeden klient zapisuje a druhý z ní čte. Data, která se prostřednictvím roury přenášejí, jsou strukturovaná, tj. každá zpráva, která se do roury uloží může mít víc položek různých typů. Celý proces přenosu probíhá v několika krocích:
- v lokální paměti sestavujeme zprávu přidáváním jednotlivých položek,
- obsah lokální paměti přesuneme do sdílené paměti. Od toho okamžiku má libovolný PostgreSQL klient přístup ke zprávě,
- obsah sdílené paměti přesuneme do lokální paměti klienta a zprávu odstraníme ze sdílené paměti
- z lokální paměti postupně načítáme položku zprávy
Jak na rouru, tak na zprávu se můžeme dívat jako na FIFO fronty. Každá roura může být sdílená několika klienty. Každá zpráva je ale přečtena pouze jednou,- získá ji a z sdílené paměti odstraní klient, který se k ní dostane jako první.
Nerad bych tu přepisoval dokumentaci k dbms_pipe, kterou si můžete bezproblémově vygooglovat na netu. Pro začátek si vystačíme s funkcemi:
dbms_pipe.pack_message(hodnota) | uloží hodnotu do bufferu sestavované zprávy. |
dbms_pipe.send_message(nazev_fronty) | přesune zprávu do pojmenované fronty zpráv. |
dbms_pipe.receive_message(název_fronty) | z pojmenované fronty zpráv přesune první zprávu do bufferu čtené zprávy. |
dbms_pipe.unpack_message_typ() | vrací hodnotu daného typu, která je první v bufferu čtené zprávy. |
Například přesun čísla a textu mezi dvěma přihlášenými klienty provede následující posloupnost volání funkcí:
SELECT dbms_pipe.pack_message(0); SELECT dbms_pipe.pack_message('Ahoj'); SELECT dbms_pipe.send_message('moje roura'); SELECT dbms_pipe.receive_message('moje roura'); SELECT dbms_pipe.unpack_message_number(); SELECT dbms_pipe.unpack_message_text();
Teď už máme dostatek znalostí, abychom napsali funkci trace(...), která pošle své argumenty ladícímu klientu a čeká před svým dokončením na vnější signál. Laděnou funkci musíme ručně doplnit o volání trace, např:
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;
Potřebuji dvě funkce. Zmíněnou funkci trace() a funkci cont, která zobrazí ladící data a popostrčí funkci trace (signalizuje jí požadavek na její ukončení).
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'); -- posli data PERFORM dbms_pipe.receive_message(dbms_pipe.unique_session_name()||'$DBG$CONT'); -- cekej na signal END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION cont(_pipe varchar, OUT _fn varchar, OUT _ln integer, OUT _value varchar) RETURNS record AS $$ BEGIN -- jsou k dispozici data? 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 -- ceka nekdo na 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; -- opet cekej na data 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;
Samotné krokování může probíhat tak, že v jednom okně si spustíme laděnou funkci, a v druhém okně výpisem z tabulky db_pipes získáme identifikátor session, a opakujeme volání funkce cont. Tato verze má tu nevýhodu, že nejsme schopni detekovat ukončení provádění laděné funkce a a funkci cont musíme po posledním cyklu ručně přerušit.
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
S bezestavovým komunikačním protokolem si nevystačíme. Jakékoliv další funkce se implementují dost komplikovaně. Dvou-stavový protokol sám o sobě řeší synchronizaci komunikace. V prvním stavu laděný klient oznamuje, že se dostal do stavu přerušení a očekává příkaz. V druhém stavu laděný klient odesílá výsledek zpracovaného příkazu. Vzájemnou komunikaci mohu popsat schématem:
A, Dostal jsem se k bodu B, Počkej až si klient řekne přerušení, čekám na o příkaz, pak mu jej pošli. příkaz Čekej na výsledek A, Zpracoval jsem příkaz, B, Zobraz výsledek a skonči posílám výsledek a skonči
Proces A - laděný klient, Proces B - ladící klient. Pokud bych nezkoušel napsat debugger v PL/pgSQL, pak bych proces B implementoval jako smyčku obsahující zobrazení ladících informací, získání reakce od uživatele a zpracování vstupu. Jelikož uložené procedury z principu nenabízejí žádný prostředek, jak zajistit interakci s uživatelem (Nedávno jsem narazil v konferenci na dotaz, jak v uložených procedurách implementovat MessageBox a InputBox. Odpověď: Principiálně to není možné.), nelze toto schéma použít. Veškerá data od uživatele musím mít už v době volání funkce. Proto jsem napsal funkce cont, exec a stop.
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); -- posli info dosazeni bodu preruseni a cekej na prikaz 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 -- vrat parametry PERFORM dbms_pipe.pack_message(_value); ELSIF _request = 2 THEN -- proved dotaz -- pro pretypovani record->varchar je nutne pouzit PL/pgSQL konverzi EXECUTE dbms_pipe.unpack_message_text() INTO _r; _v := _r; PERFORM dbms_pipe.pack_message(_v); ELSIF _request = 3 THEN -- ukonceni ladeni PERFORM dbms_pipe.pack_message('Stop debuging'); PERFORM dbms_pipe.send_message(dbms_pipe.unique_session_name()||'$DBG'); RAISE EXCEPTION 'Stop debuging'; END IF; -- posli data 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;
Ladění uložených procedur není jedinou oblastí, kde můžeme intersession komunikaci použít. Často se používá k vzájemné synchronizaci procedur nebo k realizaci architektury klient-server (Bohužel PostgreSQL nepodporuje autonomní transakce, takže si v praxi nedovedu představit klient-server aplikaci nad PL/pgSQL. Zatím.).
Nedělám si iluze, že bych napsal použitelný debugger. Komunikační protokol je primitivní bez možnosti resynchronizace, množina příkazů je minimální. Jenomže sofistikovanější protokol znamená více kódu a tak i menší přehlednost a zřetelnost kódu. Pravdou je, že mi šlo víc o ukázku možností nové knihovny orafce než o vytvoření vlastního debuggeru. Čímž ovšem nevylučuji, že někdo můj prototypový debugger použije. Já sám jsem se ale za těch pár let naučil žít bez debuggeru a vystačit si s RAISE NOTICE.