http://postgres.cz/index.php?title=Napi%C5%A1te_si_debugger_PL/pgSQL_aneb_pokro%C4%8Dil%C3%A9_techniky_programov%C3%A1n%C3%AD_v_PostgreSQL&feed=atom&action=historyNapište si debugger PL/pgSQL aneb pokročilé techniky programování v PostgreSQL - Historie editací2024-03-29T14:13:06ZHistorie editací této stránkyMediaWiki 1.36.0http://postgres.cz/index.php?title=Napi%C5%A1te_si_debugger_PL/pgSQL_aneb_pokro%C4%8Dil%C3%A9_techniky_programov%C3%A1n%C3%AD_v_PostgreSQL&diff=203&oldid=prev82.119.243.73 v 17. 7. 2006, 07:072006-07-17T07:07:13Z<p></p>
<p><b>Nová stránka</b></p><div>[[Category:Články]]<br />
Napište si debugger PL/pgSQL aneb pokročilé techniky programování v PostgreSQL <br />
<br />
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.<br />
<br />
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 [http://pgfoundry.org/projects/orafce/ Orafce], který, kromě jiného, obsahuje implementaci balíčku [http://www.utexas.edu/its/unix/reference/oracledocs/v92/B10501_01/appdev.920/a96612/d_pipe.htm 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.<br />
<br />
Ještě si dovolím stručně popsat knihovnu [[Oracle functionality#DBMS_PIPE|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:<br />
* v lokální paměti sestavujeme zprávu přidáváním jednotlivých položek,<br />
* 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ě,<br />
* obsah sdílené paměti přesuneme do lokální paměti klienta a zprávu odstraníme ze sdílené paměti<br />
* z lokální paměti postupně načítáme položku zprávy<br />
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í.<br />
<br />
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:<br />
<br />
{|<br />
|-<br />
| dbms_pipe.pack_message(hodnota) || uloží hodnotu do bufferu sestavované zprávy.<br />
|-<br />
| dbms_pipe.send_message(nazev_fronty) || přesune zprávu do pojmenované fronty zpráv.<br />
|-<br />
| dbms_pipe.receive_message(název_fronty) || z pojmenované fronty zpráv přesune první zprávu do bufferu čtené zprávy.<br />
|-<br />
|dbms_pipe.unpack_message_typ() || vrací hodnotu daného typu, která je první v bufferu čtené zprávy.<br />
|-<br />
|}<br />
Například přesun čísla a textu mezi dvěma přihlášenými klienty provede následující posloupnost volání funkcí:<br />
<pre><br />
SELECT dbms_pipe.pack_message(0);<br />
SELECT dbms_pipe.pack_message('Ahoj');<br />
SELECT dbms_pipe.send_message('moje roura');<br />
<br />
SELECT dbms_pipe.receive_message('moje roura');<br />
SELECT dbms_pipe.unpack_message_number();<br />
SELECT dbms_pipe.unpack_message_text(); <br />
</pre><br />
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ř:<br />
<pre><br />
CREATE OR REPLACE FUNCTION test_loop() <br />
RETURNS void AS $$<br />
BEGIN<br />
FOR i IN 1..10 LOOP<br />
trace('test_loop', 3, i::text);<br />
END LOOP;<br />
END;<br />
$$ LANGUAGE plpgsql;<br />
</pre><br />
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í).<br />
<pre><br />
CREATE OR REPLACE FUNCTION trace(_name varchar, _ln integer, _value varchar)<br />
RETURNS void AS $$<br />
BEGIN<br />
PERFORM dbms_pipe.pack_message(_name);<br />
PERFORM dbms_pipe.pack_message(_ln);<br />
PERFORM dbms_pipe.pack_message(_va);<br />
PERFORM dbms_pipe.send_message(dbms_pipe.unique_session_name()||'$DBG'); -- posli data<br />
PERFORM dbms_pipe.receive_message(dbms_pipe.unique_session_name()||'$DBG$CONT'); -- cekej na signal<br />
END;<br />
$$ LANGUAGE plpgsql;<br />
<br />
CREATE OR REPLACE FUNCTION cont(_pipe varchar, OUT _fn varchar, OUT _ln integer, OUT _value varchar)<br />
RETURNS record AS $$<br />
BEGIN<br />
-- jsou k dispozici data?<br />
PERFORM 1 FROM dbms_pipe.db_pipes WHERE name = _name AND items > 0;<br />
IF FOUND THEN<br />
PERFORM dbms_pipe.receive_message(_pipe||'$DBG');<br />
_fn := dbms_pipe.unpack_message_text();<br />
_ln := dbms_pipe.unpack_message_number();<br />
_value := dbms_pipe.unpack_message_text();<br />
RETURN;<br />
ELSE<br />
-- ceka nekdo na signal continue?<br />
PERFORM 1 FROM dbms_pipe.db_pipes WHERE name = _name || '$DBG$CONT' AND items = 0;<br />
IF FOUND THEN<br />
PERFORM dbms_pipe.send_message(_name||'$DBG$CONT');<br />
END IF;<br />
-- opet cekej na data<br />
PERFORM dbms_pipe.receive_message(_name);<br />
_fn := dbms_pipe.unpack_message_text();<br />
_ln := dbms_pipe.unpack_message_number();<br />
_value := dbms_pipe.unpack_message_text();<br />
RETURN;<br />
END IF;<br />
END;<br />
$$ LANGUAGE plpgsql;<br />
</pre><br />
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.<br />
<pre><br />
postgres=# select test_loop();<br />
<br />
postgres=# select * from dbms_pipe.db_pipes;<br />
name | items | size | limit | private | owner<br />
-------------------------+-------+------+-------+---------+-------<br />
PG$PIPE$1$4652$DBG | 1 | 32 | | f |<br />
PG$PIPE$1$4652$DBG$CONT | 0 | 0 | | f |<br />
(2 rows)<br />
<br />
postgres=# select cont('PG$PIPE$1$4652');<br />
cont<br />
------------------<br />
(test_loop,40,1)<br />
(1 row)<br />
<br />
postgres=# select cont('PG$PIPE$1$4652');<br />
cont<br />
------------------<br />
(test_loop,40,2)<br />
(1 row)<br />
<br />
...<br />
<br />
postgres=# select cont('PG$PIPE$1$4652');<br />
Cancel request sent<br />
ERROR: canceling statement due to user request<br />
CONTEXT: SQL function "receive_message" statement 1<br />
SQL statement "SELECT dbms_pipe.receive_message( $1 ||'$DBG')"<br />
PL/pgSQL function "cont" line 3 at perform<br />
</pre><br />
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:<br />
<pre><br />
A, Dostal jsem se k bodu B, Počkej až si klient řekne<br />
přerušení, čekám na o příkaz, pak mu jej pošli.<br />
příkaz Čekej na výsledek<br />
<br />
A, Zpracoval jsem příkaz, B, Zobraz výsledek a skonči<br />
posílám výsledek a <br />
skonči<br />
</pre><br />
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.<br />
<pre><br />
CREATE OR REPLACE FUNCTION trace(_desc varchar, _ln integer, _value varchar)<br />
RETURNS void AS $$<br />
DECLARE<br />
_request integer;<br />
_r record;<br />
_v varchar;<br />
BEGIN<br />
PERFORM dbms_pipe.pack_message(0);<br />
-- posli info dosazeni bodu preruseni a cekej na prikaz<br />
PERFORM dbms_pipe.send_message(dbms_pipe.unique_session_name()||'$DBG');<br />
PERFORM dbms_pipe.receive_message(dbms_pipe.unique_session_name()||'$DBG$CONT');<br />
_request := dbms_pipe.unpack_message_number();<br />
PERFORM dbms_pipe.pack_message(1);<br />
PERFORM dbms_pipe.pack_message(_desc);<br />
PERFORM dbms_pipe.pack_message(_ln);<br />
IF _request = 1 THEN -- vrat parametry<br />
PERFORM dbms_pipe.pack_message(_value);<br />
ELSIF _request = 2 THEN -- proved dotaz<br />
-- pro pretypovani record->varchar je nutne pouzit PL/pgSQL konverzi<br />
EXECUTE dbms_pipe.unpack_message_text() INTO _r;<br />
_v := _r; PERFORM dbms_pipe.pack_message(_v);<br />
ELSIF _request = 3 THEN -- ukonceni ladeni<br />
PERFORM dbms_pipe.pack_message('Stop debuging');<br />
PERFORM dbms_pipe.send_message(dbms_pipe.unique_session_name()||'$DBG');<br />
RAISE EXCEPTION 'Stop debuging';<br />
END IF;<br />
-- posli data<br />
PERFORM dbms_pipe.send_message(dbms_pipe.unique_session_name()||'$DBG');<br />
END;<br />
$$ LANGUAGE plpgsql;<br />
<br />
CREATE OR REPLACE FUNCTION cont(_pipe varchar, OUT _desc varchar, OUT _ln integer, OUT _value varchar)<br />
RETURNS record AS $$<br />
declare i integer;<br />
BEGIN<br />
PERFORM dbms_pipe.receive_message(_pipe||'$DBG');<br />
IF 0 <> dbms_pipe.unpack_message_number() THEN<br />
RAISE EXCEPTION 'Synchronisation error';<br />
END IF;<br />
PERFORM dbms_pipe.pack_message(1);<br />
PERFORM dbms_pipe.send_message(_pipe||'$DBG$CONT');<br />
PERFORM dbms_pipe.receive_message(_pipe||'$DBG');<br />
IF 1 <> dbms_pipe.unpack_message_number() THEN<br />
RAISE EXCEPTION 'Synchronisation error';<br />
END IF;<br />
_desc := dbms_pipe.unpack_message_text();<br />
_ln := dbms_pipe.unpack_message_number();<br />
_value := dbms_pipe.unpack_message_text();<br />
END;<br />
$$ LANGUAGE plpgsql;<br />
<br />
CREATE OR REPLACE FUNCTION stop(_pipe varchar, OUT _desc varchar, OUT _ln integer, OUT _value varchar)<br />
RETURNS record AS $$<br />
declare i integer;<br />
BEGIN<br />
PERFORM dbms_pipe.receive_message(_pipe||'$DBG');<br />
IF 0 <> dbms_pipe.unpack_message_number() THEN<br />
RAISE EXCEPTION 'Synchronisation error';<br />
END IF;<br />
PERFORM dbms_pipe.pack_message(2);<br />
PERFORM dbms_pipe.send_message(_pipe||'$DBG$CONT');<br />
PERFORM dbms_pipe.receive_message(_pipe||'$DBG');<br />
IF 1 <> dbms_pipe.unpack_message_number() THEN<br />
RAISE EXCEPTION 'Synchronisation error';<br />
END IF;<br />
_desc := dbms_pipe.unpack_message_text();<br />
_ln := dbms_pipe.unpack_message_number();<br />
_value := dbms_pipe.unpack_message_text();<br />
END;<br />
$$ LANGUAGE plpgsql;<br />
<br />
CREATE OR REPLACE FUNCTION exec(_pipe varchar, _query varchar, OUT _desc varchar, OUT _ln integer, OUT _value varchar)<br />
RETURNS record AS $$<br />
declare i integer;<br />
BEGIN<br />
PERFORM dbms_pipe.receive_message(_pipe||'$DBG');<br />
IF 0 <> dbms_pipe.unpack_message_number() THEN<br />
RAISE EXCEPTION 'Synchronisation error';<br />
END IF;<br />
PERFORM dbms_pipe.pack_message(5);<br />
PERFORM dbms_pipe.pack_message(_query);<br />
PERFORM dbms_pipe.send_message(_pipe||'$DBG$CONT');<br />
PERFORM dbms_pipe.receive_message(_pipe||'$DBG');<br />
IF 1 <> dbms_pipe.unpack_message_number() THEN<br />
RAISE EXCEPTION 'Synchronisation error';<br />
END IF;<br />
_desc := dbms_pipe.unpack_message_text();<br />
_ln := dbms_pipe.unpack_message_number();<br />
_value := dbms_pipe.unpack_message_text();<br />
END;<br />
$$ LANGUAGE plpgsql;<br />
</pre><br />
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.). <br />
<br />
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.</div>82.119.243.73