Ponaučení z 15letého vývoje pluginu pro PL/pgSQL a pár poznámek k historii Postgresu

Dnes
Doba čtení: 17 minut

Sdílet

Tři sloni putují savanou
Autor: Root.cz s využitím Zoner AI
Rozšiřitelnost je základní a velmi důležitou součástí databáze PostgreSQL. V tomto článku bych chtěl popsat pár zajímavých technik, které používám při vývoji vlastního rozšíření plpgsql_check.

Rozšířitelná databáze PostgreSQL

Co se dozvíte v článku
  1. Rozšířitelná databáze PostgreSQL
  2. Zkušenosti s psaním vlastních rozšíření
  3. Jazyk PL/pgSQL
  4. PL/pgSQL plugin API
  5. fmgr hook
  6. MemoryContextRegisterResetCallback

Asi nejdůležitější vlastností databáze PostgreSQL (kromě provozní spolehlivosti) je její rozšiřitelnost. Skrze extenze lze do Postgresu přidávat další datové typy, nové operátory, rozšiřovat sadu typů indexů, přidávat operace executoru nebo přidávat metriky pro monitoring Postgresu. Samozřejmě, že drtivá většina uživatelů používá Postgres spokojeně bez extenzí. Pro určitou část uživatelů jsou tato rozšíření nutnost. Umožňují efektivně pracovat s daty aniž by data samotná musela opustit prostor databáze (to je v architektuře klient-server zásadní).

Rozšíření jsou důležitá i pro samotný vývoj Postgresu. Umožňují lidem se specifickou doménovou znalostí psát kód, který může efektivně pracovat s daty, aniž by museli znát kompletní kód Postgresu. Navíc, kód extenzí nezvyšuje komplexitu kódu samotné databáze (Postgresu). Komplexní extenze, jako je například PostGIS, Citus nebo Timescale (dnes TigerDB), jsou nezávislé projekty s vlastním týmem, s vlastním vývojovým cyklem (nemusí se synchronizovat s vývojovým cyklem Postgresu).

Není to tak, že by tyto týmy byly stoprocentně nezávislé (pár vychytávek v optimalizátoru Postgresu jsou díky PostGISu), ale určitě 99 % času si každý tým jede po své linii a neřeší ostatní. To je u velkých projektů, kde se setkávají vyšší desítky lidí, naprostý základ rozumného fungování (čím je větší tým, tím je větší problém ani ne tak s koordinací jako spíš s nalezením konsensu a s určením priorit).

Některé extenze do Postgresu jsou samy o sobě rozšiřitelné. Terminologie tady není úplně striktní nebo ustálená. Pro extenze do jiných extenzí se občas používá název plugin (v Postgresu). Dost často ale plugin sám o sobě má všechny náležitosti extenze Postgresu. Můžete mít plugin do PL/pgSQL  – pldebuger nebo plprofiler, můžeme mít output pluginy pro logickou replikaci ( pglogical, pgoutput, wal2json). To, co dělá nějaký kód Postgresovou extenzí jsou dvě základní vlastnosti:

  1. rozšiřují SQL interface,
  2. instalují se příkazemCREATE EXTENSION (a odinstalují příkazem  DROP EXTENSION).

Existují výjimky – důležitá extenzeauto_explain se neinstaluje příkazem CREATE EXTENSION, a to z toho důvodu, že nemá žádný SQL interface.

Zkušenosti s psaním vlastních rozšíření

V tomto článku bych chtěl popsat pár zajímavých technik, které používám v extenzi plpgsql_check . Základy této extenze (ještě pod jménem plpgsql_lint ) jsem napsal primárně pro sebe, protože jsem potřeboval řešit problémy s kvalitou kódu v PL/pgSQL  u komplexitou netriviálního projektu. PL/pgSQL je v Postgresu primárním jazykem pro psaní uložených procedur. Já sám mám uložené procedury jako technologii rád, a rád je používám (dnes jsem spíš výjimkou).

Jsem si vědom, že jsou uložené procedury kontroverzní technologii. Dá se v tom hodně věcí dělat ošklivě (a viděl jsem hrozná zvěrstva), některé prostředí pro uložené procedury jsou na dnešní dobu primitivní ( T-SQL) nebo pomalé a zabugované ( SQL/PSM v MySQL). PL/SQL v Oraclu má kvalitní implementaci, kvalitní základ ( ADA) nicméně z historických důvodů umožňuje hodně prasit. SQL/PSM (standard) se v praxi neujal (čistou a úplnou implementaci SQL/PSM můžeme vidět v DB2 – minimálně v ČR se DB2 už moc nevidí).

Na druhou stranu dost problémů s latencemi a výkonem se dají perfektně řešit právě uloženými procedurami. V každém případě je v PL/SQL (případně v T-SQL) napsáno tolik kritického kódu, že nepochybuji o tom, že se s kódem napsaným v tomto jazyce, budeme setkávat ještě 50 let.

Jazyk PL/pgSQL

V Postgresu se pro psaní uložených procedur primárně používá programovací jazykPL/pgSQL (je možné použít i Perl  nebo Python). PL/pgSQL vznikl zkopírováním výrazně zredukované syntaxe oraclovského PL/SQL. Původní implementace PL/pgSQL byla jednoduchá a primitivní, nicméně funkční a umožňovala migrace z Oracle (což ve své době dost možná zachránilo Postgres jako projekt). Pozor a v praxi se na to občas narazí, přes veškerou vnější podobnost PL/SQL a PL/pgSQL interně jde o absolutně rozdílné implementace a to prakticky ve všech ohledech.

Implementace byla původně hodně jednoduchá (a relativně stále je). Parser vytvořený generátorembison vytvoří syntaktický strom, kde každý uzel je příkazem PL/pgSQL (jako IF, FOR, …). Každý příkaz má svou implementaci, která se volá při iteraci napříč syntaktickým stromem. Problém je, že PL/pgSQL může obsahovat SQL příkazy a výrazy. bison neumí kombinovat víc gramatik (říká se, že parser, který používá Oracle to umí). SQL příkazy a výrazy se proto v PL/pgSQL  identifikují dost nehezky a přeskakují.

Poté, při exekuci, se zavolá interní API (SPI ), které umožňuje provést SQL příkaz zadaný jako řetězec. Není to hezké, ale je to jednoduché a funguje to. Problém je, že se na chybu v SQL přijde až v runtimu. Poměrně brzo se přidala alespoň kontrola, zda je SQL syntakticky validní. Nikdy se ale neprovádí kompletní kontrola SQL příkazů (při validaci PL/pgSQL funkcí). Nekontroluje se, jestli referencované objekty (tabulky, sloupce, funkce) skutečně existují.

Důvody jsou dva. Jednak v PL/pgSQL  chybí koncept modulu a není jak zadat dopředu signaturu funkce. Pokud by byl validátor příliš agresivní, tak by nebylo možné vložit funkce A a B, kde A volá B a B volá A. Druhým důvodem je implementace dočasných tabulek. V Postgresu jsou pouze tzv lokální dočasné tabulky. V době validace funkce (registrace), tyto tabulky ještě neexistují, a tudíž validátor nemůže být příliš agresivní – jinak by se s nimi nedalo pracovat, a muselo by se vždy použít dynamické SQL.

Obsluha (implementace) příkazu WHILE:

/* ----------
 * exec_stmt_while          Loop over statements as long
 *                  as an expression evaluates to
 *                  true or an exit occurs.
 * ----------
 */
static int
exec_stmt_while(PLpgSQL_execstate *estate, PLpgSQL_stmt_while *stmt)
{
    int         rc = PLPGSQL_RC_OK;

    for (;;)
    {
        bool        value;
        bool        isnull;

        value = exec_eval_boolean(estate, stmt->cond, &isnull);
        exec_eval_cleanup(estate);

        if (isnull || !value)
            break;

        rc = exec_stmts(estate, stmt->body);

        LOOP_RC_PROCESSING(stmt->label, break);
    }

    return rc;
}

Na velkých projektech je slabá kontrola vložených SQL příkazů (v PL/pgSQL) problém. Při zápisu SQL se nikdy nevyhnete překlepům. Někdy je překlep přímo v názvu referencovaného objektu. Zažil jsem situace, kdy jsme na některé chyby narazili po několika měsících provozu. Dříve se daná větev kódu nevykonala.

S prostředím PL/pgSQL jsem měl pár zkušeností, takže bez větší námahy jsem napsal jednoduchou utilitku, která iteruje napříč syntaktickým stromem funkce, a pro SQL příkazy, které najde, vyvolá generování prováděcího plánu. Tím se vynutí úplná kontrola vložených SQL příkazů a výrazů. Práce na dva dny, a základ plpgsql_lintu byl na světě. Tehdy mi to dost pomohlo, dnes další generace této extenze se relativně masivně (tak jak se používá PL/pgSQL) používá napříč světem.

Když se dívám do zdrojáků plpgsql_lintu , tak mohu říct, že je to čistý plugin pro PL/pgSQL (využívající tzv plpgsql plugin API), který nemá žádný SQL interface. Jeho funkcionalita se aktivuje načtením knihovny příkazem load (a z pohledu pozdějšího plpgsql_checku kontroly běží v tzv pasivním režimu).

Jelikož jsem v této extenzi ještě nepotřeboval volat žádnou funkci z knihovny plpgsql, fungovala bez problémů na všech platformách. V další inkarnaci této extenze (v plpgsql_checku) už volám funkce z plpgsql.so (což je knihovna mimo backend – mimochodem plpgsql je také extenzí). Měl jsem štěstí, že jsem byl na Linuxu, a tam není problém přímo zavolat jakoukoliv funkci z již načtené knihovny. Na MacOS jsem ale měl problém. Tam takové chování není defaultem, ale dá se vynutit

ifeq ($(PORTNAME), darwin)
override CFLAGS += -undefined dynamic_lookup
endif

Dynamický lookup je ale trochu na prasáka. Lepší je použít interní funkci Postgresu load_external_function  – případně obalené makrem (tak jak je použitá interně v Postgresu):

#define LOAD_EXTERNAL_FUNCTION(file, funcname)  ((void *) (load_external_function(file, funcname, true, NULL)))

plpgsql_check__compile_p = (plpgsql_check__compile_t)
        LOAD_EXTERNAL_FUNCTION("$libdir/plpgsql", "plpgsql_compile");

Dynamický lookup nemusí být stoprocentně spolehlivý. V okamžiku volání požadovaná knihovna nemusí být načtena a volání funkce selže (na což jsem narazil ve chvíli, kdy jsem plpgsql_check přepsal do čistého C. Pokud se použije explicitně získaná reference na funkci (s použitím load_external_function), tak máme garantováno, že požadovaná knihovna je načtena a připravena. Trvalo mi možná 10 let, než jsem se k této verzi dostal.

PL/pgSQL plugin API

S tím jak rostla popularita PostgreSQL (včetně PL/pgSQL), tak rostla potřeba uživatelů mít nějaké nástroje na ladění a profilování kódu. V roce 2007 Korry Douglas (pod patronací EDB) navrhl a realizoval koncept debuggeru pro PL/pgSQL, který se skládal ze dvou (tří) částí. První částí byla rozšíření interpretu PL/pgSQL o sadu callbacků, které umožňují provést zadaný kód při startu funkce, po dokončení funkce, při startu příkazu, po dokončení příkazu.

Druhou částí byla původně neotevřená extenze pldebugger, která umožňovala dozorovat (implementace breakpointů) interpretaci kódu (pomocí výše zmíněných callbacků) z druhého spojení. Třetí částí byl plugin do pgadminu (také jej bylo možné provozovat samostatně), který umožňoval krokovat funkci a zobrazit si obsah proměnných.

O pár let později bylo plugin API použité v PL profileru (což byl a je neskutečně užitečný program), a ještě o něco málo později jsem jej použil v plpgsql_lintu (a posléze samozřejmě v plpgsql_checku). Nevím o žádných dalších rozšířených extenzích, které by používaly plugin API (přeci jen je to dost specializovaná záležitost).

typedef struct PLpgSQL_plugin
{
    /* Function pointers set up by the plugin */
    void        (*func_setup) (PLpgSQL_execstate *estate, PLpgSQL_function *func);
    void        (*func_beg) (PLpgSQL_execstate *estate, PLpgSQL_function *func);
    void        (*func_end) (PLpgSQL_execstate *estate, PLpgSQL_function *func);
    void        (*stmt_beg) (PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt);
    void        (*stmt_end) (PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt);

    /* Function pointers set by PL/pgSQL itself */
    void        (*error_callback) (void *arg);
    void        (*assign_expr) (PLpgSQL_execstate *estate,
                                PLpgSQL_datum *target,
                                PLpgSQL_expr *expr);
    void        (*assign_value) (PLpgSQL_execstate *estate,
                                 PLpgSQL_datum *target,
                                 Datum value, bool isNull,
                                 Oid valtype, int32 valtypmod);
    void        (*eval_datum) (PLpgSQL_execstate *estate, PLpgSQL_datum *datum,
                               Oid *typeId, int32 *typetypmod,
                               Datum *value, bool *isnull);
    Datum       (*cast_value) (PLpgSQL_execstate *estate,
                               Datum value, bool *isnull,
                               Oid valtype, int32 valtypmod,
                               Oid reqtype, int32 reqtypmod);
} PLpgSQL_plugin;

Plugin API je api jednoduché a relativně silné. Zpřístupňuje i dynamickou paměť, která se alokuje při startu každé PL/pgSQL funkce. V této paměti je primárně obsah proměnných funkce. Naopak docela zásadním omezením tohoto API je skutečnost, že jej v jednu chvíli může používat pouze jedna extenze. Většina zpětných volání je navržena tak, že je může používat víc extenzí. Zrovna toto ne. Důvodem je komplikovanější memory management – je potřeba někam uložit data per volání funkce per extenze. U běžných extenzí je to per extenze per spojení (session).

Popsaná vlastnost tohoto API je důvodem, proč jsem do plpgsql_checku přidal profiler (a posléze tracking). Historicky nebylo možné v jednom připojení používat plpgsql_check a plprofiler. Nakonec ale stejně uživatele přišli s prosbou o podporu debuggingu, a rozhodně jsem nechtěl pldebugger integrovat do plpgsql_checku. Takže jsem trochu improvizoval, a našel jsem řešení, jak bezpečně provozovat plpgsql_check s jinou extenzí, která také používá plugin API. Požadavkem je, aby extenze, která podporuje sdílení plugin API se inicializovala jako poslední.

Dalším omezením plugin API je, že nemá ošetřené situace, kdy dojde k chybě. Po chybě se už nezavolá odpovídající stmt_end a func_end (Asi by to bylo možné relativně jednoduše doimplementovat, a na dnešních CPU by to nemělo extra znatelnou režii. V každém případě by to ale mělo režii a to i v případě, že to 99% uživatelů nevyužije. To je něco, co se v komunitě hodně špatně prosazuje). V případě profileru jsem musel relativně složitě dohledávat, kdy a kde mám odečíst režii obsluhy výjimky, a pokud výjimka nebyla zachycena, tak jsem nedokázal profil funkce uložit. Bylo to nepříjemné, ale dalo se s tím nějak žít. Ve chvíli, kdy jsem začal podporovat metriky pokrytí (coverage metrics) už to bylo hodně omezující. Potřebuji aktivovat můj kód i poté, co nějaké funkce selže, a potřebuji, aby se můj kód provedl dříve, než se řízení vrátí volající funkci.

fmgr hook

V Postgresu existuje jiné API, které umožňuje zachytit výjimku – fmgr hook. Každá SQL funkce se volá skrz fmgr API, a tudíž je možné napsat callback, který se spustí při startu, při ukončení nebo po chybě. Problém je, že toto api není koordinované s plugin API, a není tam jednoduchý přístup k paměti, kterou si alokuje volání funkce. Navíc, po výjimce, se callback spustí až poté, co je paměť pl pluginu uvolněna (a teoreticky už může být přepsána).

Dalším problémem je, že se nemůžeme spolehnout, že před každým voláním FHET_END nebo FHET_ABORT bude volání FHET_START (jelikož callbacky se mohu inicializovat uvnitř nějaké funkce). Nicméně s využitím fmgr hook api (a s několika sty řádky místy trochu vachrlatého kódu) jsem byl schopný zachytit i výjimky v PL/pgSQL, a uživatelé byli spokojení.

/* function manager hook */
if (fmgr_hook)
    (*fmgr_hook) (FHET_START, &fcache->flinfo, &fcache->arg);

/*
 * We don't need to restore GUC or userid settings on error, because the
 * ensuing xact or subxact abort will do that.  The PG_TRY block is only
 * needed to clean up the flinfo link.
 */
save_flinfo = fcinfo->flinfo;

PG_TRY();
{
    fcinfo->flinfo = &fcache->flinfo;

    /* See notes in fmgr_info_cxt_security */
    pgstat_init_function_usage(fcinfo, &fcusage);

    result = FunctionCallInvoke(fcinfo);

    /*
     * We could be calling either a regular or a set-returning function,
     * so we have to test to see what finalize flag to use.
     */
    pgstat_end_function_usage(&fcusage,
                              (fcinfo->resultinfo == NULL ||
                               !IsA(fcinfo->resultinfo, ReturnSetInfo) ||
                               ((ReturnSetInfo *) fcinfo->resultinfo)->isDone != ExprMultipleResult));
}
PG_CATCH();
{
    fcinfo->flinfo = save_flinfo;
    if (fmgr_hook)
        (*fmgr_hook) (FHET_ABORT, &fcache->flinfo, &fcache->arg);
    PG_RE_THROW();
}
PG_END_TRY();

fcinfo->flinfo = save_flinfo;

if (fcache->configNames != NIL)
    AtEOXact_GUC(true, save_nestlevel);
if (OidIsValid(fcache->userid))
    SetUserIdAndSecContext(save_userid, save_sec_context);
if (fmgr_hook)
    (*fmgr_hook) (FHET_END, &fcache->flinfo, &fcache->arg);

return result;

Až do doby, kdy v Supabase opravili svoji extenzi způsobem, který bych neoznačil jako šťastný (určitě to není čisté, ale dost možná není jiný způsob), a který ve výsledku (pro specifické situace) rozbíjí fmgr API (může se stát, že se zavolá callback FHET_START, ale pak vlastní funkce a plugin API se nezavolá). Extenzi supautils to umožňuje filtrovat (blokovat) podezřelá volání (triggerů nebo funkcí), aniž by došlo k výjimce (detailně jsem to nestudoval, může tam docházet k něčemu jinému).

Přes fmgr hook máte možnost přesměrovat volání potenciálně riskantní funkce na stoprocentně neškodnou funkci, ale je to absolutní prasárna – takhle to rozhodně nebylo navržené. Nekorektní použití fmgr API mělo samozřejmě dopad i na  plpgsql_check

Mimochodem, tento bugreport je napsaný s využitím AI, takže vypadá úhledně a propracovaně, ale jinak je celý úplně celý špatně. V podstatě jediné, čemu tam lze věřit je, že uživatelům padaly nějaké funkce z důvodu výjimky vyhozené  plpgsql_checkem.

Supabase má určitou reputaci. Rozhodně je to produkt, který nemohu ignorovat, a který nemohu přetlačit. Takže jsem se podíval po dalším řešení. A dost možná jsem našel ideální řešení. Určitě jsem našel řešení, které je lepší než nad fmgr hookem.

MemoryContextRegisterResetCallback

Postgres je napsaný v jazyce C už od svého vzniku, kdy ještě bylo Cčko novým jazykem, až do současnosti. Pokud vím, tak se nikdy neuvažovalo o přepisu do jiného programovacího jazyka. Není to úplně v klasickém Cčkovém stylu. Část tehdejšího INGRESu byla napsaná v Lispu (po přepisu do Cčka je tam ten Lisp stále vidět), občas se používá pseudo dědičnost, a pseudo reflexe struktur, atd. O Microsoft NT se kdysi vyprávělo, že je psán v Cčku objektovým způsobem. Možná to bude podobné tomu, jak je psaný Postgres.

Správa dynamické paměti v Cčku je bezpochyby primitivní. Také proto se nebere Cčko jako bezpečný jazyk. Postgres měl hromadu problémů s memory leaky. Vzpomínám, že když jsem v naší učebně na K128 testoval nějakou pozdní šestkovou verzi Postgresu (muselo to být někdy v 99), tak jsem nedokázal naimportovat data s miliónem řádků. Pak následovala vojna a dva roky, kdy jsem jen na MS technologiích. S Postgresem jsem pro produkci začal dělat až v roce 2004 (7.4), a to už byl jiný systém.

Změna přišla ve verzi 7.1 (vydání na jaře 2001), kdy se v Postgresu implementovala správa paměti založena na hierarchických paměťových kontextech. Paměť se alokuje (většinou) z aktuálního paměťového kontextu příkazem palloc. Paměť lze uvolnit příkazem pfree (analogicky k free). Paměť ale lze také uvolnit resetem nebo zrušením kontextu, z kterého byla paměť alokovaná. Při resetu nebo zrušení se zároveň ruší všechny podřízené paměťové kontexty

 Konečně při jakékoliv výjimce dojde ke zrušení všech kontextů, které už dále nebudou potřeba. Přechod na paměťové kontexty měl dramatický (pozitivní) dopad na kvalitu Postgresu. Najednou opominutí pfree většinou skoro nic neznamenalo. Také paměť nemusela být čištěna až tak agresivně, takže se na některých místech mohl zjednodušit kód (Co jsem se bavil s lidmi, kteří si tady v ČR psali vlastní databázový systém (602 SQL, PC-FAND), všichni si po x iteracích napsali vlastní memory management).

/*
 * MemoryContextCallResetCallbacks
 *      Internal function to call all registered callbacks for context.
 */
static void
MemoryContextCallResetCallbacks(MemoryContext context)
{
    MemoryContextCallback *cb;

    /*
     * We pop each callback from the list before calling.  That way, if an
     * error occurs inside the callback, we won't try to call it a second time
     * in the likely event that we reset or delete the context later.
     */
    while ((cb = context->reset_cbs) != NULL)
    {
        context->reset_cbs = cb->next;
        cb->func(cb->arg);
    }
}

Pro můj záměr je důležité, že paměťové kontexty mají callback MemoryContextRegisterResetCallback, který se zavolá těsně před zrušením kontextu (v době, kdy je obsah paměti ještě validní), a že pro každé volání funkce se vytváří nový krátkodobý paměťový kontext. V tomto kontextu je typicky uložený obsah proměnných PL/pgSQL, může tam být ale i něco jiného – například stavová proměnná plugin API.

Pokud bych na zmíněný callback pověsil svoji funkci, tak mám garantováno, že se tato funkce provede vždy po ukončení PL/pgSQL funkce (ať úspěšném nebo neúspěšném), a co víc, provede se ještě v době, kdy ještě existuje kontext funkce. S tímto „novým“ callbackem jsem mohl přepsat (a hlavně zjednodušit) kód v plpgsql_checku, a zbavit se fmgr hooku. Napsal jsem si své vlastní api pldbgapi3 , které nemá omezení plugin API (podporuje sdílení, a garantuje volání funkcí stmt_abort a func_abort po výjimce.

Školení Kubernetes

static void
plugin_info_reset(void *arg)
{
    plpgsql_plugin_info *plugin_info = (plpgsql_plugin_info*) arg;
    PLpgSQL_execstate loc_estate;
    PLpgSQL_execstate *old_cur_estate;
    MemoryContext exec_mcxt = CurrentMemoryContext;
    int         stmts_stack_size;
    int         i;

    /* The memory should not be corrupted! */
    Assert(plugin_info->magic == PLUGIN_INFO_MAGIC);

    /*
     * PostgreSQL 19 can remove this callback. But we need to support
     * previous releases, so when fextra is already released, then
     * do just nothing here.
     */
    if (!plugin_info->fextra)
        return;

    /*
     * In this moment, the estate content can be corrupted, because
     * estate is from stack of already ended function.
     *
     * Inside abort methods, the estate fields should not be referenced!
     */
    memset(&loc_estate, 0, sizeof(PLpgSQL_execstate));

#if PG_VERSION_NUM >= 180000

    Assert(plugin_info->fextra->use_count > 0);
    Assert(plugin_info->fextra->func->cfunc.use_count > 0);

#else

    Assert(plugin_info->fextra->use_count > 0);
    Assert(plugin_info->fextra->func->use_count > 0);

#endif

    loc_estate.func = plugin_info->fextra->func;

    old_cur_estate = loc_estate.func->cur_estate;
    loc_estate.func->cur_estate = &loc_estate;
    plugin_info->estate = &loc_estate;

    stmts_stack_size = plugin_info->stmts_stack_size;
    plugin_info->stmts_stack_size = 0;

    PG_TRY();
    {
        abort_statements(plugin_info->stmts_stack,
                         stmts_stack_size,
                         plugin_info, true);

        for (i = 0; i < nplugins; i++)
        {
            if (plugin_info->is_active[i] && plugins[i]->func_abort)
            {
                plugin_info->estate->plugin_info = plugin_info->plugin_info[i];

                MemoryContextSwitchTo(exec_mcxt);
                plugins[i]->func_abort(plugin_info->estate,
                                       plugin_info->estate->func,
                                       plugin_info->fextra);
            }
        }
    }

    PG_CATCH();
    {
        plugin_info->fextra->func->cur_estate = old_cur_estate;
        plch_release_fextra(plugin_info->fextra);
        plugin_info->fextra = NULL;
        plugin_info->estate = NULL;

        PG_RE_THROW();
    }
    PG_END_TRY();

    plugin_info->fextra->func->cur_estate = old_cur_estate;
    plch_release_fextra(plugin_info->fextra);
    plugin_info->fextra = NULL;
    plugin_info->estate = NULL;
}

Kód s fmgr hookem dělal, co jsem chtěl, ale rozhodně to nebylo nijak elegantní řešení. Kód s MemoryContextRegisterResetCallbackem  také dělá, to co chci, a navíc je to výrazně jednodušší, přehlednější a robustnější kód. Obě api jsou v Postgresu po celou dobu, co dělám s Postgresem. Na snad správné řešení jsem přišel až po 13 letech, a dvou předchozích pokusech, které jsem musel opustit, protože z nějakého důvodu přestaly fungovat.

Někdy některá dobrá řešení jsou po ruce, ale prostě nejsou vidět. Navíc je někdy potřeba udělat mentální úkrok stranou. Správné řešení jsem našel, teprve tehdy, až jsem přestal hledat, jak zachytit výjimku, a podíval jsem se na správu paměti.

Autor článku

Pavel Stěhule je odborníkem na relační databázový systém PostgreSQL, pracuje jako školitel a konzultant.