Comment obtenir un contexte d'exception pour une exception déclenchée manuellement dans PL / pgSQL?

11

Dans Postgres, nous obtenons la "trace de pile" des exceptions en utilisant ce code:

EXCEPTION WHEN others THEN
    GET STACKED DIAGNOSTICS v_error_stack = PG_EXCEPTION_CONTEXT;

Cela fonctionne bien pour les exceptions "naturelles", mais si nous levons une exception en utilisant

RAISE EXCEPTION 'This is an error!';

... alors il n'y a pas de trace de pile. Selon une entrée de la liste de diffusion , cela pourrait être intentionnel, bien que je ne puisse pas comprendre pourquoi. Cela me donne envie de trouver une autre façon de lever une exception autre que l'utilisation RAISE. Suis-je juste en train de manquer quelque chose d'évident? Quelqu'un at-il une astuce pour cela? Existe-t-il une exception que je peux demander à Postgres de lancer qui contiendrait une chaîne de mon choix, de sorte que j'obtienne non seulement ma chaîne dans le message d'erreur, mais également la trace complète de la pile?

Voici un exemple complet:

CREATE OR REPLACE FUNCTION error_test() RETURNS json AS $$
DECLARE
    v_error_stack text;
BEGIN

    -- Comment this out to see how a "normal" exception will give you the stack trace
    RAISE EXCEPTION 'This exception will not get a stack trace';

    -- This will give a divide by zero error, complete with stack trace
    SELECT 1/0;

-- In case of any exception, wrap it in error object and send it back as json
EXCEPTION WHEN others THEN

    -- If the exception we're catching is one that Postgres threw,
    -- like a divide by zero error, then this will get the full
    -- stack trace of the place where the exception was thrown.
    -- However, since we are catching an exception we raised manually
    -- using RAISE EXCEPTION, there is no context/stack trace!
    GET STACKED DIAGNOSTICS v_error_stack = PG_EXCEPTION_CONTEXT;

    RAISE WARNING 'The stack trace of the error is: "%"', v_error_stack;

    return to_json(v_error_stack);
END;
$$ LANGUAGE plpgsql;
Taytay
la source
Ce pourrait être une bonne idée de montrer un exemple simple ici.
Craig Ringer
Bon point @CraigRinger. Terminé!
Taytay
Ce n'est pas autonome. Quoi error_info? Ressemble à un type personnalisé.
Craig Ringer
Désolé - je pensais que vous vouliez juste un contexte général. J'ai supprimé les trucs superflus.
Taytay

Réponses:

9

Ce comportement semble être voulu par la conception même du produit.

Dans src/pl/plpgsql/src/pl_exec.cle contexte d'erreur, le rappel vérifie explicitement s'il est appelé dans le contexte d'une RAISEinstruction PL / PgSQL et, dans l'affirmative, ignore l'émission du contexte d'erreur:

/*
 * error context callback to let us supply a call-stack traceback
 */
static void
plpgsql_exec_error_callback(void *arg)
{
        PLpgSQL_execstate *estate = (PLpgSQL_execstate *) arg;

        /* if we are doing RAISE, don't report its location */
        if (estate->err_text == raise_skip_msg)
                return;

Je ne trouve aucune référence précise pour expliquer pourquoi c'est le cas.

En interne dans le serveur, la pile de contexte est générée par le traitement de error_context_stack, qui est un rappel en chaîne qui ajoute des informations à une liste lors de son appel.

Lorsque PL / PgSQL entre dans une fonction, il ajoute un élément à la pile de rappel de contexte d'erreur. Lorsqu'il quitte une fonction, il supprime un élément de cette pile.

Si les fonctions de rapport d'erreurs du serveur PostgreSQL, comme ereportou elogsont appelées, il appelle le rappel de contexte d'erreur. Mais en PL / PgSQL s'il remarque qu'il est appelé à partir d'un, RAISEses rappels ne font rien intentionnellement.

Compte tenu de cela, je ne vois aucun moyen d'atteindre ce que vous voulez sans patcher PostgreSQL. Je suggère de poster du courrier à pgsql-general demandant pourquoi RAISEne fournit pas le contexte d'erreur maintenant que PL / PgSQL doit l' GET STACKED DIAGNOSTICSutiliser.

(BTW, le contexte d'exception n'est pas une trace de pile en tant que telle. Il ressemble un peu à cela car PL / PgSQL ajoute chaque appel de fonction à la pile, mais il est également utilisé pour d'autres détails sur le serveur.)

Craig Ringer
la source
Merci beaucoup Craig pour la réponse rapide et complète. Cela me semble étrange et va certainement à l'encontre de mes attentes. L'utilité de RAISEest diminuée par ce contrôle. Je vais leur écrire.
Taytay
@Taytay Veuillez inclure un lien vers votre question ici, mais assurez-vous que votre courrier est complet et peut être compris sans suivre le lien; de nombreuses personnes ignorent les publications contenant uniquement des liens ou contenant principalement des liens. Si vous avez la chance de faire apparaître un lien vers votre message dans les commentaires ici, via archives.postgresql.org, ce serait vraiment génial d'aider d'autres personnes plus tard.
Craig Ringer
Merci Craig. Bon conseil. J'ai créé un fil ici: postgresql.org/message-id/… Pour l'instant, ils recherchent une bonne solution au problème.
Taytay
6

Vous pouvez contourner cette restriction et faire plpgsql émettre le contexte d'erreur comme vous le souhaitez en appelant une autre fonction qui déclenche (avertissement, avis, ...) l'erreur pour vous.

J'ai posté une solution pour cela il y a quelques années - dans l'un de mes premiers messages ici sur dba.SE :

-- helper function to raise an exception with CONTEXT
CREATE OR REPLACE FUNCTION f_raise(_lvl text = 'EXCEPTION'
                                  ,_msg text = 'Default error msg.')
  RETURNS void AS
$func$
BEGIN
   CASE upper(_lvl)
      WHEN 'EXCEPTION' THEN RAISE EXCEPTION '%', _msg;
      WHEN 'WARNING'   THEN RAISE WARNING   '%', _msg;
      WHEN 'NOTICE'    THEN RAISE NOTICE    '%', _msg;
      WHEN 'DEBUG'     THEN RAISE DEBUG     '%', _msg;
      WHEN 'LOG'       THEN RAISE LOG       '%', _msg;
      WHEN 'INFO'      THEN RAISE INFO      '%', _msg;
      ELSE RAISE EXCEPTION 'f_raise(): unexpected raise-level: "%"', _lvl;
   END CASE;
END
$func$  LANGUAGE plpgsql STRICT;

Détails:

J'ai développé votre cas de test publié pour démontrer qu'il fonctionne dans Postgres 9.3:

SQL Fiddle.

Erwin Brandstetter
la source
Merci beaucoup Erwin! Curieusement, j'ai expérimenté votre solution avant de poster, mais j'ai dû faire quelque chose de mal et je n'ai pas eu le contexte que j'attendais. Maintenant que j'ai vu le violon (merci de me le montrer aussi), je vais lui donner un autre coup!
Taytay
Bien fait; ça ne devrait pas être nécessaire, mais on dirait que ça ferait l'affaire.
Craig Ringer
@CraigRinger: Étant donné que les exceptions devraient être, eh bien, l' exception , l'impact minimal sur les performances ne devrait pas non plus avoir d'importance. Nous avons toutes les options de cette façon.
Erwin Brandstetter
Tout à fait d'accord, j'aimerais juste voir la nécessité de la solution de contournement disparaître à un moment donné.
Craig Ringer
@CraigRinger: Vrai. Si cela ne se produit pas de si tôt, nous pouvons suggérer cette solution de contournement dans le manuel ...
Erwin Brandstetter