1. 功能简介
PostgreSQL 中 pldebugger 插件主要用于实现服务器端 PL/pgSQL 调试器,它提供了一种调试服务器端 PL/pgSQL 程序的方法。
通过 pldebugger 调试 PL/pgSQL 程序,需要至少两个数据库 session:一个是用于执行目标程序的 target session,另外一个是用于调试目标程序的 debug session。Target session 用于执行被调试 PL/pgSQL 存储过程,包括函数、过程、包存储过程等。同时,Debug session还必须使用 pldebugger 进行自身初始化,用来了解它要监视哪个目标会话。之后,调试会话可以调用 pldebugger 的入口点来读取从目标会话发布的事件并与目标会话进行通信。
2. 原理简析
2.1 pldebugger 整体结构
pldebugger 内置包整体结构体为 Client/Server 结构,实现上为通过 socket 方式实现,整体结构如下:
2.2 如何获取到 PL/pgSQL 执行信息?
PL/pgSQL 中包含一个类型为 PLpgSQL_plugin 的全局指针变量 plpgsql_plugin_ptr,结构如下:
/*
* A PLpgSQL_plugin structure represents an instrumentation plugin.
* To instrument PL/pgSQL, a plugin library must access the rendezvous
* variable "PLpgSQL_plugin" and set it to point to a PLpgSQL_plugin struct.
* Typically the struct could just be static data in the plugin library.
* We expect that a plugin would do this at library load time (_PG_init()).
* It must also be careful to set the rendezvous variable back to NULL
* if it is unloaded (_PG_fini()).
*
* This structure is basically a collection of function pointers --- at
* various interesting points in pl_exec.c, we call these functions
* (if the pointers are non-NULL) to give the plugin a chance to watch
* what we are doing.
*
* func_setup is called when we start a function, before we've initialized
* the local variables defined by the function.
*
* func_beg is called when we start a function, after we've initialized
* the local variables.
*
* func_end is called at the end of a function.
*
* stmt_beg and stmt_end are called before and after (respectively) each
* statement.
*
* Also, immediately before any call to func_setup, PL/pgSQL fills in the
* error_callback and assign_expr fields with pointers to its own
* plpgsql_exec_error_callback and exec_assign_expr functions. This is
* a somewhat ad-hoc expedient to simplify life for debugger plugins.
*/
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);
} PLpgSQL_plugin;
在 PL/pgSQL 初始化时,pl_handle r会将这个结构体指针放到 Rendezvous 哈希表中,key 为该 plugin ptr 的名字,entry 为该全局指针变量。
pldebugger 插件初始化的时候,通过 rendezvous hash table 利用 "PLpgSQL_plugin" 查询到 plpgsql_plugin_ptr 指针,然后将插件中的初始化后的结构体指针写入到相应位置。
此过程中,pldebugger 已经将实现的函数进行了注册,即在 pldebugger 插件中实现了 dbg_startup 函数用于在存储过程被调用时进行初始化处理以及 dbg_newstmt 函数用户存储过程每行执行时处理 PL/pgSQL 中包含的执行信息。
static void dbg_startup( PLpgSQL_execstate * estate, PLpgSQL_function * func );
static void dbg_newstmt( PLpgSQL_execstate * estate, PLpgSQL_stmt * stmt );
static PLpgSQL_plugin plugin_funcs = { dbg_startup, NULL, NULL, dbg_newstmt, NULL };
在 PL/pgSQL 存储过程执行时,plpgsql_estate_setup 会初始化对应的结构体部分,并添加部分 plpgsql 部分函数指针,此过程中主要为 plpgsql_exec_error_callback 和 exec_assign_expr 两个函数,分别用于处理 plpgsql 错误回调函数以及对存储过程中的变量进行赋值使用。
/* ----------
* Initialize a mostly empty execution state
* ----------
*/
static void
plpgsql_estate_setup(PLpgSQL_execstate *estate,
PLpgSQL_function *func,
ReturnSetInfo *rsi,
EState *simple_eval_estate,
ResourceOwner simple_eval_resowner)
{
HASHCTL ctl;
...
/*
* Let the plugin see this function before we initialize any local
* PL/pgSQL variables - note that we also give the plugin a few function
* pointers so it can call back into PL/pgSQL for doing things like
* variable assignments and stack traces
*/
if (*plpgsql_plugin_ptr)
{
(*plpgsql_plugin_ptr)->error_callback = plpgsql_exec_error_callback;
(*plpgsql_plugin_ptr)->assign_expr = exec_assign_expr;
if ((*plpgsql_plugin_ptr)->func_setup)
((*plpgsql_plugin_ptr)->func_setup) (estate, func);
}
}
exec_stmt 会执行每行存储过程,此时会通过 plpgsql_plugin_ptr 指针是否存在进行相应的调用,在 pldebugger 中即为调用 dbg_newstmt 进行信息处理。
exec_stmt(PLSQL_execstate * estate, PLSQL_stmt * stmt)
{
PLSQL_stmt *save_estmt = estate->err_stmt;
int rc;
TimestampTz save_ts;
estate->err_stmt = stmt;
/* Let the plugin know that we are about to execute this statement */
if (*plpgsql_plugin_ptr && (*plpgsql_plugin_ptr)->stmt_beg)
((*plpgsql_plugin_ptr)->stmt_beg) (estate, stmt);
...
}
2.3 调试过程是如何实现的?
考虑使用gdb调试,通过会包含以下基本操作:
break xxx
next(step over)
step(step into)
continue
print
info break
list
set
break 操作函数签名如下:
select pldbg_set_breakpoint(session INTEGER, func OID, linenumber INTEGER);
在使用 pldebugger 插件进行调试时,target session 中维护了一个 break point 的哈希表,debug session 通过 pldbg_set_breakpoint 打断点时,就是直接将该断点添加到哈希表中。然后在每行存储过程执行时,在 dbg_newstmt 函数中会检查该行是否存在断点,存在的话则停下,否则直接跳过该行。
注:这里的断点形式是 { function_oid, line_number },即使用存储过程的 oid 和 行数 进行标识的。
next 操作函数签名如下:
select pldbg_step_over(session INTEGER);
target session 处理的时候直接跳过即可,到下一行处理。这里面会存在以下两种情况:
该行是一个函数/过程调用:如果该函数没有正在被调试,那么不会进入该函数/过程,直接跳过该行;如果该函数正在被调试,那么会进入到该函数内部;
该行是一个普通行,直接跳过该行。
3. step 操作对应函数签名如下:
select pldbg_step_into(session INTEGER);
step 在 target session 处理时与 next 几乎一致,但是会额外在 per_session_ctx 中将 step_into_next_func 设置为 true,此时会在出现其他函数调用时进入该函数,判断逻辑如下:
static void
dbg_startup(PLpgSQL_execstate *estate, PLpgSQL_function *func)
{
if( func == NULL )
{
/*
* In general, this should never happen, but it seems to in the
* case of package constructors
*/
estate->plugin_info = NULL;
return;
}
if( !breakpointsForFunction( func->fn_oid ) && !per_session_ctx.step_into_next_func)
{
estate->plugin_info = NULL;
return;
}
initialize_plugin_info(estate, func);
}
4. continue 操作对应函数签名如下:
select pldbg_continue(session INTEGER);
continue 在处理的时候会在跳过该行(next操作)的基础上,设置 dbg_info.stepping = false,此时target session 不会进入 while 循环等待 client 的消息,会直接执行到结束。
/*
* When the debugger decides that it needs to step through (or into) a
* particular function invocation, it allocates a dbg_ctx and records the
* address of that structure in the executor's context structure
* (estate->plugin_info).
*
* The dbg_ctx keeps track of all of the information we need to step through
* code and display variable values
*/
typedef struct
{
PLpgSQL_function * func; /* Function definition */
bool stepping; /* If TRUE, stop at next statement */
var_value * symbols; /* Extra debugger-private info about variables */
char ** argNames; /* Argument names */
int argNameCount; /* Number of names pointed to by argNames */
void (* error_callback)(void *arg);
void (* assign_expr)( PLpgSQL_execstate *estate, PLpgSQL_datum *target, PLpgSQL_expr *expr );
} dbg_ctx;
5. print 操作对应函数签名:
select pldbg_get_variables(session INTEGER);
在处理 print 的时候,需要显示出来此时所有变量的值,包括存储过程的参数和 declare 出来的变量,该类变量均会存储在 PL/pgSQL 执行状态结构体中,包括变量的数量以及变量的值:
/*
* Runtime execution data
*/
typedef struct PLpgSQL_execstate
{
PLpgSQL_function *func; /* function being executed */
...
/*
* The datums representing the function's local variables. Some of these
* are local storage in this execstate, but some just point to the shared
* copy belonging to the PLpgSQL_function, depending on whether or not we
* need any per-execution state for the datum's dtype.
*/
int ndatums;
PLpgSQL_datum **datums;
...
void *plugin_info; /* reserved for use by optional plugin */
} PLpgSQL_execstate;
变量的值包含以下类型,目前仅支持 PLPGSQL_DTYPE_VAR 和 PLPGSQL_DTYPE_PROMISE 类型的打印,其余类型暂未支持:
/*
* Datum array node types
*/
typedef enum PLpgSQL_datum_type
{
PLPGSQL_DTYPE_VAR, <------- 显示这个
PLPGSQL_DTYPE_ROW,
PLPGSQL_DTYPE_REC,
PLPGSQL_DTYPE_RECFIELD,
PLPGSQL_DTYPE_PROMISE <------- 显示这个
} PLpgSQL_datum_type;
6. info break 操作对应的函数签名:
select pldbg_get_breakpoints(session INTEGER);
显示所有的 breakpoints 的时候就是遍历 breakpoints 的哈希表,然后返回所有的 breakpoints。
7. list 操作对应的函数签名:
select pldbg_get_source(session INTEGER, func OID);
target session 会通过 pg_proc 读取 prosrc 字段并返回。
8. set 操作对应的函数签名:
select pldbg_deposit_value(session INTEGER, varName TEXT, lineNumber INTEGER, value TEXT);
在进行 set_value 的时候,回想下在初始化的时候,初始化了以下结构(*plpgsql_plugin_ptr)->assign_expr = exec_assign_expr;
static void
plpgsql_estate_setup(PLpgSQL_execstate *estate,
PLpgSQL_function *func,
ReturnSetInfo *rsi,
EState *simple_eval_estate,
ResourceOwner simple_eval_resowner)
{
HASHCTL ctl;
...
if (*plpgsql_plugin_ptr)
{
(*plpgsql_plugin_ptr)->assign_expr = exec_assign_expr;
}
}
这里是通过调用 exec_assign_expr 进行处理,先构造一个 PLpgSQL_expr,然后将 value 部分先作为一个表达式进行处理,构造一个 SELECT value; 以此计算表达式的值。如果这里设置失败的话, 会将 value 作为字符串再处理一次,结束。
3. 总结
PostgreSQL 中 pldebugger 插件通常通过 pgAdmin 客户端进行调用进行使用,了解其中原理对于存储过程的调试将会有更加深入的理解和认识。
1. 功能简介
PostgreSQL 中 pldebugger 插件主要用于实现服务器端 PL/pgSQL 调试器,它提供了一种调试服务器端 PL/pgSQL 程序的方法。
通过 pldebugger 调试 PL/pgSQL 程序,需要至少两个数据库 session:一个是用于执行目标程序的 target session,另外一个是用于调试目标程序的 debug session。Target session 用于执行被调试 PL/pgSQL 存储过程,包括函数、过程、包存储过程等。同时,Debug session还必须使用 pldebugger 进行自身初始化,用来了解它要监视哪个目标会话。之后,调试会话可以调用 pldebugger 的入口点来读取从目标会话发布的事件并与目标会话进行通信。
2. 原理简析
2.1 pldebugger 整体结构
pldebugger 内置包整体结构体为 Client/Server 结构,实现上为通过 socket 方式实现,整体结构如下:
2.2 如何获取到 PL/pgSQL 执行信息?
PL/pgSQL 中包含一个类型为 PLpgSQL_plugin 的全局指针变量 plpgsql_plugin_ptr,结构如下:
/*
* A PLpgSQL_plugin structure represents an instrumentation plugin.
* To instrument PL/pgSQL, a plugin library must access the rendezvous
* variable "PLpgSQL_plugin" and set it to point to a PLpgSQL_plugin struct.
* Typically the struct could just be static data in the plugin library.
* We expect that a plugin would do this at library load time (_PG_init()).
* It must also be careful to set the rendezvous variable back to NULL
* if it is unloaded (_PG_fini()).
*
* This structure is basically a collection of function pointers --- at
* various interesting points in pl_exec.c, we call these functions
* (if the pointers are non-NULL) to give the plugin a chance to watch
* what we are doing.
*
* func_setup is called when we start a function, before we've initialized
* the local variables defined by the function.
*
* func_beg is called when we start a function, after we've initialized
* the local variables.
*
* func_end is called at the end of a function.
*
* stmt_beg and stmt_end are called before and after (respectively) each
* statement.
*
* Also, immediately before any call to func_setup, PL/pgSQL fills in the
* error_callback and assign_expr fields with pointers to its own
* plpgsql_exec_error_callback and exec_assign_expr functions. This is
* a somewhat ad-hoc expedient to simplify life for debugger plugins.
*/
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);
} PLpgSQL_plugin;
在 PL/pgSQL 初始化时,pl_handle r会将这个结构体指针放到 Rendezvous 哈希表中,key 为该 plugin ptr 的名字,entry 为该全局指针变量。
pldebugger 插件初始化的时候,通过 rendezvous hash table 利用 "PLpgSQL_plugin" 查询到 plpgsql_plugin_ptr 指针,然后将插件中的初始化后的结构体指针写入到相应位置。
此过程中,pldebugger 已经将实现的函数进行了注册,即在 pldebugger 插件中实现了 dbg_startup 函数用于在存储过程被调用时进行初始化处理以及 dbg_newstmt 函数用户存储过程每行执行时处理 PL/pgSQL 中包含的执行信息。
static void dbg_startup( PLpgSQL_execstate * estate, PLpgSQL_function * func );
static void dbg_newstmt( PLpgSQL_execstate * estate, PLpgSQL_stmt * stmt );
static PLpgSQL_plugin plugin_funcs = { dbg_startup, NULL, NULL, dbg_newstmt, NULL };
在 PL/pgSQL 存储过程执行时,plpgsql_estate_setup 会初始化对应的结构体部分,并添加部分 plpgsql 部分函数指针,此过程中主要为 plpgsql_exec_error_callback 和 exec_assign_expr 两个函数,分别用于处理 plpgsql 错误回调函数以及对存储过程中的变量进行赋值使用。
/* ----------
* Initialize a mostly empty execution state
* ----------
*/
static void
plpgsql_estate_setup(PLpgSQL_execstate *estate,
PLpgSQL_function *func,
ReturnSetInfo *rsi,
EState *simple_eval_estate,
ResourceOwner simple_eval_resowner)
{
HASHCTL ctl;
...
/*
* Let the plugin see this function before we initialize any local
* PL/pgSQL variables - note that we also give the plugin a few function
* pointers so it can call back into PL/pgSQL for doing things like
* variable assignments and stack traces
*/
if (*plpgsql_plugin_ptr)
{
(*plpgsql_plugin_ptr)->error_callback = plpgsql_exec_error_callback;
(*plpgsql_plugin_ptr)->assign_expr = exec_assign_expr;
if ((*plpgsql_plugin_ptr)->func_setup)
((*plpgsql_plugin_ptr)->func_setup) (estate, func);
}
}
exec_stmt 会执行每行存储过程,此时会通过 plpgsql_plugin_ptr 指针是否存在进行相应的调用,在 pldebugger 中即为调用 dbg_newstmt 进行信息处理。
exec_stmt(PLSQL_execstate * estate, PLSQL_stmt * stmt)
{
PLSQL_stmt *save_estmt = estate->err_stmt;
int rc;
TimestampTz save_ts;
estate->err_stmt = stmt;
/* Let the plugin know that we are about to execute this statement */
if (*plpgsql_plugin_ptr && (*plpgsql_plugin_ptr)->stmt_beg)
((*plpgsql_plugin_ptr)->stmt_beg) (estate, stmt);
...
}
2.3 调试过程是如何实现的?
考虑使用gdb调试,通过会包含以下基本操作:
- break xxx
- next(step over)
- step(step into)
- continue
- print
- info break
- list
- set
- break 操作函数签名如下:
select pldbg_set_breakpoint(session INTEGER, func OID, linenumber INTEGER);
在使用 pldebugger 插件进行调试时,target session 中维护了一个 break point 的哈希表,debug session 通过 pldbg_set_breakpoint 打断点时,就是直接将该断点添加到哈希表中。然后在每行存储过程执行时,在 dbg_newstmt 函数中会检查该行是否存在断点,存在的话则停下,否则直接跳过该行。
注:这里的断点形式是 { function_oid, line_number },即使用存储过程的 oid 和 行数 进行标识的。
- next 操作函数签名如下:
select pldbg_step_over(session INTEGER);
target session 处理的时候直接跳过即可,到下一行处理。这里面会存在以下两种情况:
- 该行是一个函数/过程调用:如果该函数没有正在被调试,那么不会进入该函数/过程,直接跳过该行;如果该函数正在被调试,那么会进入到该函数内部;
- 该行是一个普通行,直接跳过该行。
3. step 操作对应函数签名如下:
select pldbg_step_into(session INTEGER);
step 在 target session 处理时与 next 几乎一致,但是会额外在 per_session_ctx 中将 step_into_next_func 设置为 true,此时会在出现其他函数调用时进入该函数,判断逻辑如下:
static void
dbg_startup(PLpgSQL_execstate *estate, PLpgSQL_function *func)
{
if( func == NULL )
{
/*
* In general, this should never happen, but it seems to in the
* case of package constructors
*/
estate->plugin_info = NULL;
return;
}
if( !breakpointsForFunction( func->fn_oid ) && !per_session_ctx.step_into_next_func)
{
estate->plugin_info = NULL;
return;
}
initialize_plugin_info(estate, func);
}
4. continue 操作对应函数签名如下:
select pldbg_continue(session INTEGER);
continue 在处理的时候会在跳过该行(next操作)的基础上,设置 dbg_info.stepping = false,此时target session 不会进入 while 循环等待 client 的消息,会直接执行到结束。
/*
* When the debugger decides that it needs to step through (or into) a
* particular function invocation, it allocates a dbg_ctx and records the
* address of that structure in the executor's context structure
* (estate->plugin_info).
*
* The dbg_ctx keeps track of all of the information we need to step through
* code and display variable values
*/
typedef struct
{
PLpgSQL_function * func; /* Function definition */
bool stepping; /* If TRUE, stop at next statement */
var_value * symbols; /* Extra debugger-private info about variables */
char ** argNames; /* Argument names */
int argNameCount; /* Number of names pointed to by argNames */
void (* error_callback)(void *arg);
void (* assign_expr)( PLpgSQL_execstate *estate, PLpgSQL_datum *target, PLpgSQL_expr *expr );
} dbg_ctx;
5. print 操作对应函数签名:
select pldbg_get_variables(session INTEGER);
在处理 print 的时候,需要显示出来此时所有变量的值,包括存储过程的参数和 declare 出来的变量,该类变量均会存储在 PL/pgSQL 执行状态结构体中,包括变量的数量以及变量的值:
/*
* Runtime execution data
*/
typedef struct PLpgSQL_execstate
{
PLpgSQL_function *func; /* function being executed */
...
/*
* The datums representing the function's local variables. Some of these
* are local storage in this execstate, but some just point to the shared
* copy belonging to the PLpgSQL_function, depending on whether or not we
* need any per-execution state for the datum's dtype.
*/
int ndatums;
PLpgSQL_datum **datums;
...
void *plugin_info; /* reserved for use by optional plugin */
} PLpgSQL_execstate;
变量的值包含以下类型,目前仅支持 PLPGSQL_DTYPE_VAR 和 PLPGSQL_DTYPE_PROMISE 类型的打印,其余类型暂未支持:
/*
* Datum array node types
*/
typedef enum PLpgSQL_datum_type
{
PLPGSQL_DTYPE_VAR, <------- 显示这个
PLPGSQL_DTYPE_ROW,
PLPGSQL_DTYPE_REC,
PLPGSQL_DTYPE_RECFIELD,
PLPGSQL_DTYPE_PROMISE <------- 显示这个
} PLpgSQL_datum_type;
6. info break 操作对应的函数签名:
select pldbg_get_breakpoints(session INTEGER);
显示所有的 breakpoints 的时候就是遍历 breakpoints 的哈希表,然后返回所有的 breakpoints。
7. list 操作对应的函数签名:
select pldbg_get_source(session INTEGER, func OID);
target session 会通过 pg_proc 读取 prosrc 字段并返回。
8. set 操作对应的函数签名:
select pldbg_deposit_value(session INTEGER, varName TEXT, lineNumber INTEGER, value TEXT);
在进行 set_value 的时候,回想下在初始化的时候,初始化了以下结构(*plpgsql_plugin_ptr)->assign_expr = exec_assign_expr;
static void
plpgsql_estate_setup(PLpgSQL_execstate *estate,
PLpgSQL_function *func,
ReturnSetInfo *rsi,
EState *simple_eval_estate,
ResourceOwner simple_eval_resowner)
{
HASHCTL ctl;
...
if (*plpgsql_plugin_ptr)
{
(*plpgsql_plugin_ptr)->assign_expr = exec_assign_expr;
}
}
这里是通过调用 exec_assign_expr 进行处理,先构造一个 PLpgSQL_expr,然后将 value 部分先作为一个表达式进行处理,构造一个 SELECT value; 以此计算表达式的值。如果这里设置失败的话, 会将 value 作为字符串再处理一次,结束。
3. 总结
PostgreSQL 中 pldebugger 插件通常通过 pgAdmin 客户端进行调用进行使用,了解其中原理对于存储过程的调试将会有更加深入的理解和认识。