Create infrastructure for "soft" error reporting.
authorTom Lane <[email protected]>
Fri, 9 Dec 2022 14:58:38 +0000 (09:58 -0500)
committerTom Lane <[email protected]>
Fri, 9 Dec 2022 14:58:38 +0000 (09:58 -0500)
Postgres' standard mechanism for reporting errors (ereport() or elog())
is used for all sorts of error conditions.  This means that throwing
an exception via ereport(ERROR) requires an expensive transaction or
subtransaction abort and cleanup, since the exception catcher dare not
make many assumptions about what has gone wrong.  There are situations
where we would rather have a lighter-weight mechanism for dealing
with errors that are known to be safe to recover from without a full
transaction cleanup.  This commit creates infrastructure to let us
adapt existing error-reporting code for that purpose.  See the
included documentation changes for details.  Follow-on commits will
provide test code and usage examples.

The near-term plan is to convert most if not all datatype input
functions to report invalid input "softly".  This will enable
implementing some SQL/JSON features cleanly and without the cost
of subtransactions, and it will also allow creating COPY options
to deal with bad input without cancelling the whole COPY.

This patch is mostly by me, but it owes very substantial debt to
earlier work by Nikita Glukhov, Andrew Dunstan, and Amul Sul.
Thanks also to Andres Freund for review.

Discussion: https://postgr.es/m/3bbbb0df-7382-bf87-9737-340ba096e034@postgrespro.ru

doc/src/sgml/ref/create_type.sgml
src/backend/nodes/Makefile
src/backend/nodes/gen_node_support.pl
src/backend/utils/error/elog.c
src/backend/utils/fmgr/README
src/backend/utils/fmgr/fmgr.c
src/include/fmgr.h
src/include/nodes/meson.build
src/include/nodes/miscnodes.h [new file with mode: 0644]
src/include/utils/elog.h
src/tools/pgindent/typedefs.list

index 693423e5243508222907eb1e4ac290cd99aea512..994dfc65268bd1585c10da503eb64c699baa7a20 100644 (file)
@@ -900,6 +900,17 @@ CREATE TYPE <replaceable class="parameter">name</replaceable>
    function is written in C.
   </para>
 
+  <para>
+   In <productname>PostgreSQL</productname> version 16 and later,
+   it is desirable for base types' input functions to
+   return <quote>soft</quote> errors using the
+   new <function>errsave()</function>/<function>ereturn()</function>
+   mechanism, rather than throwing <function>ereport()</function>
+   exceptions as in previous versions.
+   See <filename>src/backend/utils/fmgr/README</filename> for more
+   information.
+  </para>
+
  </refsect1>
 
  <refsect1>
index 4368c30fdbb03e55b86372afb4093f3673ab4eea..7c594be5837e3a526dc20985be871c0e1685c6bc 100644 (file)
@@ -56,6 +56,7 @@ node_headers = \
        nodes/bitmapset.h \
        nodes/extensible.h \
        nodes/lockoptions.h \
+       nodes/miscnodes.h \
        nodes/replnodes.h \
        nodes/supportnodes.h \
        nodes/value.h \
index 7212bc486f3da17212a76fe114f03c087bc48467..08992dfd4761b88a1db0d36d2728c8e38f1f8c38 100644 (file)
@@ -68,6 +68,7 @@ my @all_input_files = qw(
   nodes/bitmapset.h
   nodes/extensible.h
   nodes/lockoptions.h
+  nodes/miscnodes.h
   nodes/replnodes.h
   nodes/supportnodes.h
   nodes/value.h
@@ -89,6 +90,7 @@ my @nodetag_only_files = qw(
   executor/tuptable.h
   foreign/fdwapi.h
   nodes/lockoptions.h
+  nodes/miscnodes.h
   nodes/replnodes.h
   nodes/supportnodes.h
 );
index f5cd1b74937443fe286e0cb20e7c2d69e41aa8ac..eb489ea3a70746c67c1734f2a1bb1ac1e0a6e770 100644 (file)
@@ -71,6 +71,7 @@
 #include "libpq/libpq.h"
 #include "libpq/pqformat.h"
 #include "mb/pg_wchar.h"
+#include "nodes/miscnodes.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/bgworker.h"
@@ -611,6 +612,128 @@ errfinish(const char *filename, int lineno, const char *funcname)
        CHECK_FOR_INTERRUPTS();
 }
 
+
+/*
+ * errsave_start --- begin a "soft" error-reporting cycle
+ *
+ * If "context" isn't an ErrorSaveContext node, this behaves as
+ * errstart(ERROR, domain), and the errsave() macro ends up acting
+ * exactly like ereport(ERROR, ...).
+ *
+ * If "context" is an ErrorSaveContext node, but the node creator only wants
+ * notification of the fact of a soft error without any details, we just set
+ * the error_occurred flag in the ErrorSaveContext node and return false,
+ * which will cause us to skip the remaining error processing steps.
+ *
+ * Otherwise, create and initialize error stack entry and return true.
+ * Subsequently, errmsg() and perhaps other routines will be called to further
+ * populate the stack entry.  Finally, errsave_finish() will be called to
+ * tidy up.
+ */
+bool
+errsave_start(struct Node *context, const char *domain)
+{
+       ErrorSaveContext *escontext;
+       ErrorData  *edata;
+
+       /*
+        * Do we have a context for soft error reporting?  If not, just punt to
+        * errstart().
+        */
+       if (context == NULL || !IsA(context, ErrorSaveContext))
+               return errstart(ERROR, domain);
+
+       /* Report that a soft error was detected */
+       escontext = (ErrorSaveContext *) context;
+       escontext->error_occurred = true;
+
+       /* Nothing else to do if caller wants no further details */
+       if (!escontext->details_wanted)
+               return false;
+
+       /*
+        * Okay, crank up a stack entry to store the info in.
+        */
+
+       recursion_depth++;
+
+       /* Initialize data for this error frame */
+       edata = get_error_stack_entry();
+       edata->elevel = LOG;            /* signal all is well to errsave_finish */
+       set_stack_entry_domain(edata, domain);
+       /* Select default errcode based on the assumed elevel of ERROR */
+       edata->sqlerrcode = ERRCODE_INTERNAL_ERROR;
+
+       /*
+        * Any allocations for this error state level should go into the caller's
+        * context.  We don't need to pollute ErrorContext, or even require it to
+        * exist, in this code path.
+        */
+       edata->assoc_context = CurrentMemoryContext;
+
+       recursion_depth--;
+       return true;
+}
+
+/*
+ * errsave_finish --- end a "soft" error-reporting cycle
+ *
+ * If errsave_start() decided this was a regular error, behave as
+ * errfinish().  Otherwise, package up the error details and save
+ * them in the ErrorSaveContext node.
+ */
+void
+errsave_finish(struct Node *context, const char *filename, int lineno,
+                          const char *funcname)
+{
+       ErrorSaveContext *escontext = (ErrorSaveContext *) context;
+       ErrorData  *edata = &errordata[errordata_stack_depth];
+
+       /* verify stack depth before accessing *edata */
+       CHECK_STACK_DEPTH();
+
+       /*
+        * If errsave_start punted to errstart, then elevel will be ERROR or
+        * perhaps even PANIC.  Punt likewise to errfinish.
+        */
+       if (edata->elevel >= ERROR)
+       {
+               errfinish(filename, lineno, funcname);
+               pg_unreachable();
+       }
+
+       /*
+        * Else, we should package up the stack entry contents and deliver them to
+        * the caller.
+        */
+       recursion_depth++;
+
+       /* Save the last few bits of error state into the stack entry */
+       set_stack_entry_location(edata, filename, lineno, funcname);
+
+       /* Replace the LOG value that errsave_start inserted */
+       edata->elevel = ERROR;
+
+       /*
+        * We skip calling backtrace and context functions, which are more likely
+        * to cause trouble than provide useful context; they might act on the
+        * assumption that a transaction abort is about to occur.
+        */
+
+       /*
+        * Make a copy of the error info for the caller.  All the subsidiary
+        * strings are already in the caller's context, so it's sufficient to
+        * flat-copy the stack entry.
+        */
+       escontext->error_data = palloc_object(ErrorData);
+       memcpy(escontext->error_data, edata, sizeof(ErrorData));
+
+       /* Exit error-handling context */
+       errordata_stack_depth--;
+       recursion_depth--;
+}
+
+
 /*
  * get_error_stack_entry --- allocate and initialize a new stack entry
  *
index 49845f67accb72abf93ecbffce35d366a5efc5b1..9958d38992bebd59428768625dfdbfb028a72135 100644 (file)
@@ -267,6 +267,78 @@ See windowapi.h for more information.
 information about the context of the CALL statement, particularly
 whether it is within an "atomic" execution context.
 
+* Some callers of datatype input functions (and in future perhaps
+other classes of functions) pass an instance of ErrorSaveContext.
+This indicates that the caller wishes to handle "soft" errors without
+a transaction-terminating exception being thrown: instead, the callee
+should store information about the error cause in the ErrorSaveContext
+struct and return a dummy result value.  Further details appear in
+"Handling Soft Errors" below.
+
+
+Handling Soft Errors
+--------------------
+
+Postgres' standard mechanism for reporting errors (ereport() or elog())
+is used for all sorts of error conditions.  This means that throwing
+an exception via ereport(ERROR) requires an expensive transaction or
+subtransaction abort and cleanup, since the exception catcher dare not
+make many assumptions about what has gone wrong.  There are situations
+where we would rather have a lighter-weight mechanism for dealing
+with errors that are known to be safe to recover from without a full
+transaction cleanup.  SQL-callable functions can support this need
+using the ErrorSaveContext context mechanism.
+
+To report a "soft" error, a SQL-callable function should call
+       errsave(fcinfo->context, ...)
+where it would previously have done
+       ereport(ERROR, ...)
+If the passed "context" is NULL or is not an ErrorSaveContext node,
+then errsave behaves precisely as ereport(ERROR): the exception is
+thrown via longjmp, so that control does not return.  If "context"
+is an ErrorSaveContext node, then the error information included in
+errsave's subsidiary reporting calls is stored into the context node
+and control returns from errsave normally.  The function should then
+return a dummy value to its caller.  (SQL NULL is recommendable as
+the dummy value; but anything will do, since the caller is expected
+to ignore the function's return value once it sees that an error has
+been reported in the ErrorSaveContext node.)
+
+If there is nothing to do except return after calling errsave(),
+you can save a line or two by writing
+       ereturn(fcinfo->context, dummy_value, ...)
+to perform errsave() and then "return dummy_value".
+
+An error reported "softly" must be safe, in the sense that there is
+no question about our ability to continue normal processing of the
+transaction.  Error conditions that should NOT be handled this way
+include out-of-memory, unexpected internal errors, or anything that
+cannot easily be cleaned up after.  Such cases should still be thrown
+with ereport, as they have been in the past.
+
+Considering datatype input functions as examples, typical "soft" error
+conditions include input syntax errors and out-of-range values.  An
+input function typically detects such cases with simple if-tests and
+can easily change the ensuing ereport call to an errsave or ereturn.
+Because of this restriction, it's typically not necessary to pass
+the ErrorSaveContext pointer down very far, as errors reported by
+low-level functions are typically reasonable to consider internal.
+(Another way to frame the distinction is that input functions should
+report all invalid-input conditions softly, but internal problems are
+hard errors.)
+
+Because no transaction cleanup will occur, a function that is exiting
+after errsave() returns will bear responsibility for resource cleanup.
+It is not necessary to be concerned about small leakages of palloc'd
+memory, since the caller should be running the function in a short-lived
+memory context.  However, resources such as locks, open files, or buffer
+pins must be closed out cleanly, as they would be in the non-error code
+path.
+
+Conventions for callers that use the ErrorSaveContext mechanism
+to trap errors are discussed with the declaration of that struct,
+in nodes/miscnodes.h.
+
 
 Functions Accepting or Returning Sets
 -------------------------------------
index cd0daa7e166a4339825d744e80ede64b3a260028..0d37f69298f731d7963aedb455a2bbca33043f2e 100644 (file)
@@ -23,6 +23,7 @@
 #include "lib/stringinfo.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
+#include "nodes/miscnodes.h"
 #include "nodes/nodeFuncs.h"
 #include "pgstat.h"
 #include "utils/acl.h"
@@ -1549,6 +1550,70 @@ InputFunctionCall(FmgrInfo *flinfo, char *str, Oid typioparam, int32 typmod)
        return result;
 }
 
+/*
+ * Call a previously-looked-up datatype input function, with non-exception
+ * handling of "soft" errors.
+ *
+ * This is basically like InputFunctionCall, but the converted Datum is
+ * returned into *result while the function result is true for success or
+ * false for failure.  Also, the caller may pass an ErrorSaveContext node.
+ * (We declare that as "fmNodePtr" to avoid including nodes.h in fmgr.h.)
+ *
+ * If escontext points to an ErrorSaveContext, any "soft" errors detected by
+ * the input function will be reported by filling the escontext struct and
+ * returning false.  (The caller can choose to test SOFT_ERROR_OCCURRED(),
+ * but checking the function result instead is usually cheaper.)
+ *
+ * If escontext does not point to an ErrorSaveContext, errors are reported
+ * via ereport(ERROR), so that there is no functional difference from
+ * InputFunctionCall; the result will always be true if control returns.
+ */
+bool
+InputFunctionCallSafe(FmgrInfo *flinfo, char *str,
+                                         Oid typioparam, int32 typmod,
+                                         fmNodePtr escontext,
+                                         Datum *result)
+{
+       LOCAL_FCINFO(fcinfo, 3);
+
+       if (str == NULL && flinfo->fn_strict)
+       {
+               *result = (Datum) 0;    /* just return null result */
+               return true;
+       }
+
+       InitFunctionCallInfoData(*fcinfo, flinfo, 3, InvalidOid, escontext, NULL);
+
+       fcinfo->args[0].value = CStringGetDatum(str);
+       fcinfo->args[0].isnull = false;
+       fcinfo->args[1].value = ObjectIdGetDatum(typioparam);
+       fcinfo->args[1].isnull = false;
+       fcinfo->args[2].value = Int32GetDatum(typmod);
+       fcinfo->args[2].isnull = false;
+
+       *result = FunctionCallInvoke(fcinfo);
+
+       /* Result value is garbage, and could be null, if an error was reported */
+       if (SOFT_ERROR_OCCURRED(escontext))
+               return false;
+
+       /* Otherwise, should get null result if and only if str is NULL */
+       if (str == NULL)
+       {
+               if (!fcinfo->isnull)
+                       elog(ERROR, "input function %u returned non-NULL",
+                                flinfo->fn_oid);
+       }
+       else
+       {
+               if (fcinfo->isnull)
+                       elog(ERROR, "input function %u returned NULL",
+                                flinfo->fn_oid);
+       }
+
+       return true;
+}
+
 /*
  * Call a previously-looked-up datatype output function.
  *
index 380a82b9de3582e5d984b997bd2ad35e83c0ffab..b7832d0aa2a622a81347791422a422f43c9e3d97 100644 (file)
@@ -700,6 +700,10 @@ extern Datum OidFunctionCall9Coll(Oid functionId, Oid collation,
 /* Special cases for convenient invocation of datatype I/O functions. */
 extern Datum InputFunctionCall(FmgrInfo *flinfo, char *str,
                                                           Oid typioparam, int32 typmod);
+extern bool InputFunctionCallSafe(FmgrInfo *flinfo, char *str,
+                                                                 Oid typioparam, int32 typmod,
+                                                                 fmNodePtr escontext,
+                                                                 Datum *result);
 extern Datum OidInputFunctionCall(Oid functionId, char *str,
                                                                  Oid typioparam, int32 typmod);
 extern char *OutputFunctionCall(FmgrInfo *flinfo, Datum val);
index e63881086eef8821d6540e6759433b59d28d6369..f0e60935b645574044a159fde8ea00b05f558c78 100644 (file)
@@ -16,6 +16,7 @@ node_support_input_i = [
   'nodes/bitmapset.h',
   'nodes/extensible.h',
   'nodes/lockoptions.h',
+  'nodes/miscnodes.h',
   'nodes/replnodes.h',
   'nodes/supportnodes.h',
   'nodes/value.h',
diff --git a/src/include/nodes/miscnodes.h b/src/include/nodes/miscnodes.h
new file mode 100644 (file)
index 0000000..b50ee60
--- /dev/null
@@ -0,0 +1,56 @@
+/*-------------------------------------------------------------------------
+ *
+ * miscnodes.h
+ *       Definitions for hard-to-classify node types.
+ *
+ * Node types declared here are not part of parse trees, plan trees,
+ * or execution state trees.  We only assign them NodeTag values because
+ * IsA() tests provide a convenient way to disambiguate what kind of
+ * structure is being passed through assorted APIs, such as function
+ * "context" pointers.
+ *
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/nodes/miscnodes.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef MISCNODES_H
+#define MISCNODES_H
+
+#include "nodes/nodes.h"
+
+/*
+ * ErrorSaveContext -
+ *             function call context node for handling of "soft" errors
+ *
+ * A caller wishing to trap soft errors must initialize a struct like this
+ * with all fields zero/NULL except for the NodeTag.  Optionally, set
+ * details_wanted = true if more than the bare knowledge that a soft error
+ * occurred is required.  The struct is then passed to a SQL-callable function
+ * via the FunctionCallInfo.context field; or below the level of SQL calls,
+ * it could be passed to a subroutine directly.
+ *
+ * After calling code that might report an error this way, check
+ * error_occurred to see if an error happened.  If so, and if details_wanted
+ * is true, error_data has been filled with error details (stored in the
+ * callee's memory context!).  FreeErrorData() can be called to release
+ * error_data, although that step is typically not necessary if the called
+ * code was run in a short-lived context.
+ */
+typedef struct ErrorSaveContext
+{
+       NodeTag         type;
+       bool            error_occurred; /* set to true if we detect a soft error */
+       bool            details_wanted; /* does caller want more info than that? */
+       ErrorData  *error_data;         /* details of error, if so */
+} ErrorSaveContext;
+
+/* Often-useful macro for checking if a soft error was reported */
+#define SOFT_ERROR_OCCURRED(escontext) \
+       ((escontext) != NULL && IsA(escontext, ErrorSaveContext) && \
+        ((ErrorSaveContext *) (escontext))->error_occurred)
+
+#endif                                                 /* MISCNODES_H */
index f107a818e818df64bdfa03dd74699f98d7df4a21..8025dce3357c57f07158bc35663a246d1b8cdee6 100644 (file)
 
 #include "lib/stringinfo.h"
 
+/* We cannot include nodes.h yet, so forward-declare struct Node */
+struct Node;
+
+
 /* Error level codes */
 #define DEBUG5         10                      /* Debugging messages, in categories of
                                                                 * decreasing detail. */
@@ -235,6 +239,63 @@ extern int getinternalerrposition(void);
        ereport(elevel, errmsg_internal(__VA_ARGS__))
 
 
+/*----------
+ * Support for reporting "soft" errors that don't require a full transaction
+ * abort to clean up.  This is to be used in this way:
+ *             errsave(context,
+ *                             errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+ *                             errmsg("invalid input syntax for type %s: \"%s\"",
+ *                                        "boolean", in_str),
+ *                             ... other errxxx() fields as needed ...);
+ *
+ * "context" is a node pointer or NULL, and the remaining auxiliary calls
+ * provide the same error details as in ereport().  If context is not a
+ * pointer to an ErrorSaveContext node, then errsave(context, ...)
+ * behaves identically to ereport(ERROR, ...).  If context is a pointer
+ * to an ErrorSaveContext node, then the information provided by the
+ * auxiliary calls is stored in the context node and control returns
+ * normally.  The caller of errsave() must then do any required cleanup
+ * and return control back to its caller.  That caller must check the
+ * ErrorSaveContext node to see whether an error occurred before
+ * it can trust the function's result to be meaningful.
+ *
+ * errsave_domain() allows a message domain to be specified; it is
+ * precisely analogous to ereport_domain().
+ *----------
+ */
+#define errsave_domain(context, domain, ...)   \
+       do { \
+               struct Node *context_ = (context); \
+               pg_prevent_errno_in_scope(); \
+               if (errsave_start(context_, domain)) \
+                       __VA_ARGS__, errsave_finish(context_, __FILE__, __LINE__, __func__); \
+       } while(0)
+
+#define errsave(context, ...)  \
+       errsave_domain(context, TEXTDOMAIN, __VA_ARGS__)
+
+/*
+ * "ereturn(context, dummy_value, ...);" is exactly the same as
+ * "errsave(context, ...); return dummy_value;".  This saves a bit
+ * of typing in the common case where a function has no cleanup
+ * actions to take after reporting a soft error.  "dummy_value"
+ * can be empty if the function returns void.
+ */
+#define ereturn_domain(context, dummy_value, domain, ...)      \
+       do { \
+               errsave_domain(context, domain, __VA_ARGS__); \
+               return dummy_value; \
+       } while(0)
+
+#define ereturn(context, dummy_value, ...)     \
+       ereturn_domain(context, dummy_value, TEXTDOMAIN, __VA_ARGS__)
+
+extern bool errsave_start(struct Node *context, const char *domain);
+extern void errsave_finish(struct Node *context,
+                                                  const char *filename, int lineno,
+                                                  const char *funcname);
+
+
 /* Support for constructing error strings separately from ereport() calls */
 
 extern void pre_format_elog_string(int errnumber, const char *domain);
index a2dfd5c9da811ca97af63d8a86c50bbbf5dea684..60c71d05fe19efa5fdffa99e32d25124158ab57c 100644 (file)
@@ -643,6 +643,7 @@ EquivalenceClass
 EquivalenceMember
 ErrorContextCallback
 ErrorData
+ErrorSaveContext
 EstimateDSMForeignScan_function
 EstimationInfo
 EventTriggerCacheEntry