PG异常处理

在PG中经常可以看到ereport错误日志,本以为是直接打印日志输出,实际更加复杂。另外PG用C实现了TRY-CATCH语义,这点也十分神奇。

setjmp宏

要探究PG中的异常跳转,首先得熟悉setjmp宏,这里推荐菜鸟教程的一篇短文:

1
2
3
4
#include <setjmp.h>

int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

主要思想为:通过jmp_buf类型的全局变量保存程序切换的上下文,setjmp将jmp_buf初始化同时创建保存点,第一次调用返回0。然后再通过longjmp返回第一次调用setjmp处,setjmp的返回值为longjmp的第二个整型参数。

ereport 日志输出

在PG中,我们经常可以看到以下的错误处理代码:

1
2
3
if (!OidIsValid(get_role_oid(user_name_reset, false))) {
ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT), errmsg("role \"%s\" does not exist", user_name_reset)));
}

ereport接口定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* ----------
* New-style error reporting API: to be used in this way:
* ereport(ERROR,
* (errcode(ERRCODE_UNDEFINED_CURSOR),
* errmsg("portal \"%s\" not found", stmt->portalname),
* ... other errxxx() fields as needed ...));
*
* The error level is required, and so is a primary error message (errmsg
* or errmsg_internal). All else is optional. errcode() defaults to
* ERRCODE_INTERNAL_ERROR if elevel is ERROR or more, ERRCODE_WARNING
* if elevel is WARNING, or ERRCODE_SUCCESSFUL_COMPLETION if elevel is
* NOTICE or below.
*
* ereport_domain() allows a message domain to be specified, for modules that
* wish to use a different message catalog from the backend's. To avoid having
* one copy of the default text domain per .o file, we define it as NULL here
* and have errstart insert the default text domain. Modules can either use
* ereport_domain() directly, or preferably they can override the TEXTDOMAIN
* macro.
* ----------
*/
#define ereport_domain(elevel, domain, rest) \
(errstart(elevel, __FILE__, __LINE__, PG_FUNCNAME_MACRO, domain) ? (errfinish rest) : (void)0)

#define ereport(elevel, rest) ereport_domain(elevel, TEXTDOMAIN, rest)

ereport其实就是调用errstart,然后根据errstar的返回值决定调用errfinish还是什么都不做。

errstart

介绍errstart之前先介绍一个数据结构,用于错误数据信息

1
2
#define ERRORDATA_STACK_SIZE  5
static ErrorData errordata[ERRORDATA_STACK_SIZE];

elog.c会有一个错误处理的栈,这是因为我们在错误处理的时候还可能会发生错误。

errstart主要完成:

  • 初始化errdata信息
  • 将errdata压入errodata栈

errfinish

在关注errfinish之前,我们先关注errfinish的参数。在宏定义中,errfinish用于接收rest参数,而在实际使用中,rest这个参数实际上是由括号包裹的一串调用,分别是errcode和errmsg。

1
ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT), errmsg("role \"%s\" does not exist", user_name_reset)));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
* errcode --- add SQLSTATE error code to the current error
*
* The code is expected to be represented as per MAKE_SQLSTATE().
*/
int errcode(int sqlerrcode)
{
ErrorData* edata = &t_thrd.log_cxt.errordata[t_thrd.log_cxt.errordata_stack_depth];

/* we don't bother incrementing t_thrd.log_cxt.recursion_depth */
CHECK_STACK_DEPTH();

edata->sqlerrcode = sqlerrcode;

return 0; /* return value does not matter */
}
/*
* errmsg --- add a primary error message text to the current error
*
* In addition to the usual %-escapes recognized by printf, "%m" in
* fmt is replaced by the error message for the caller's value of errno.
*
* Note: no newline is needed at the end of the fmt string, since
* ereport will provide one for the output methods that need it.
*/
int errmsg(const char* fmt, ...)
{
ErrorData* edata = &t_thrd.log_cxt.errordata[t_thrd.log_cxt.errordata_stack_depth];
MemoryContext oldcontext;

t_thrd.log_cxt.recursion_depth++;
CHECK_STACK_DEPTH();
oldcontext = MemoryContextSwitchTo(ErrorContext);

EVALUATE_MESSAGE(message, false, true);

MemoryContextSwitchTo(oldcontext);
t_thrd.log_cxt.recursion_depth--;
return 0; /* return value does not matter */
}

errmsg通过EVALUATE_MESSAGE宏格式化字符串,将错误信息字符串计入到edata中的message中?

(1)error以下等级

然后再看errfinish的函数本体:

  • 完成error_context_stack的回调功能,为errfinish增加报错信息
  • 完成EmitErrorReport,为errfinish发送错误信息
1
2
3
for (econtext = t_thrd.log_cxt.error_context_stack; econtext != NULL; econtext = econtext->previous)
(*econtext->callback)(econtext->arg);
EmitErrorReport();

至此,一个简单的日志输出完成,返回调用ereport的代码处继续执行。

(2)error以上等级

error以上等级不再返回,调用 PG_RE_THROW(); 完成长跳转,根据PG _exception_stack记录的保存点进行跳转。PG _exception_stack是一个全局变量,在postgresMain中你会看到sigsetjmp的设置

1
if (sigsetjmp(local_sigjmp_buf, 1) != 0) {

TRY-CATCAH语义实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/* ----------
* API for catching ereport(ERROR) exits. Use these macros like so:
*
* PG_TRY();
* {
* ... code that might throw ereport(ERROR) ...
* }
* PG_CATCH();
* {
* ... error recovery code ...
* }
* PG_END_TRY();
*
* (The braces are not actually necessary, but are recommended so that
* pg_indent will indent the construct nicely.) The error recovery code
* can optionally do PG_RE_THROW() to propagate the same error outwards.
*
* Note: while the system will correctly propagate any new ereport(ERROR)
* occurring in the recovery section, there is a small limit on the number
* of levels this will work for. It's best to keep the error recovery
* section simple enough that it can't generate any new errors, at least
* not before popping the error stack.
*
* Note: an ereport(FATAL) will not be caught by this construct; control will
* exit straight through proc_exit(). Therefore, do NOT put any cleanup
* of non-process-local resources into the error recovery section, at least
* not without taking thought for what will happen during ereport(FATAL).
* The PG_ENSURE_ERROR_CLEANUP macros provided by storage/ipc.h may be
* helpful in such cases.
*
* Note: Don't execute statements such as break, continue, goto, or return in
* PG_TRY. If you need to use these statements, you must recovery
* PG_exception_stack first.
* ----------
*/
#define PG_TRY() \
do { \
sigjmp_buf* save_exception_stack = t_thrd.log_cxt.PG_exception_stack; \
ErrorContextCallback* save_context_stack = t_thrd.log_cxt.error_context_stack; \
sigjmp_buf local_sigjmp_buf; \
int tryCounter, *oldTryCounter = NULL; \
if (sigsetjmp(local_sigjmp_buf, 0) == 0) { \
t_thrd.log_cxt.PG_exception_stack = &local_sigjmp_buf; \
oldTryCounter = gstrace_tryblock_entry(&tryCounter)

#define PG_CATCH() \
} \
else \
{ \
t_thrd.log_cxt.PG_exception_stack = save_exception_stack; \
t_thrd.log_cxt.error_context_stack = save_context_stack; \
gstrace_tryblock_exit(true, oldTryCounter)

#define PG_END_TRY() \
} \
t_thrd.log_cxt.PG_exception_stack = save_exception_stack; \
t_thrd.log_cxt.error_context_stack = save_context_stack; \
gstrace_tryblock_exit(false, oldTryCounter); \
}while (0)

在PG_TRY() 中主要设置了一个本地跳转保存点local_sigjmp_buf ,并将全局变量PG_exception_stack 设置为它,那么在发生错误时大于error等级的ereport调用就会返回此处,进而执行else分支(也就是PG_CATCH() 所在部分的代码)。

注意在CATCH中加载保存的状态点。


reference:https://blog.csdn.net/jackgo73/article/details/135130840

作者

Desirer

发布于

2024-08-04

更新于

2024-11-15

许可协议