Create infrastructure for "soft" error reporting.

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
This commit is contained in:
Tom Lane 2022-12-09 09:58:38 -05:00
parent beecbe8e50
commit d9f7f5d32f
11 changed files with 397 additions and 0 deletions

View 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>

View 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 \

View 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
);

View 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
*

View 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
-------------------------------------

View 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.
*

View 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);

View 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',

View File

@ -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 */

View File

@ -18,6 +18,10 @@
#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);

View File

@ -643,6 +643,7 @@ EquivalenceClass
EquivalenceMember
ErrorContextCallback
ErrorData
ErrorSaveContext
EstimateDSMForeignScan_function
EstimationInfo
EventTriggerCacheEntry