From cdabf72b7b38af68dc69a7c78227553e54e7ef89 Mon Sep 17 00:00:00 2001 From: Alexey Kopytov Date: Sat, 21 Jan 2017 16:58:38 +0300 Subject: [PATCH] Implement customer error hooks. If a Lua script defines the sysbench.hooks.sql_error_ignorable function, sysbench calls it whenever an SQL error occurs and passes information about connection, failed query, SQL error number, state and message as arguments. If the function returns true, the error is ignored and the entire event is restarted. Otherwise sysbench aborts scripts execution. --- sysbench/db_driver.h | 2 + sysbench/drivers/mysql/drv_mysql.c | 10 ++-- sysbench/drivers/pgsql/drv_pgsql.c | 6 +-- sysbench/lua/internal/sysbench.lua | 8 +++ sysbench/lua/internal/sysbench.sql.lua | 68 +++++++++++++++++++------- sysbench/sb_lua.c | 17 ++++++- sysbench/sb_lua.h | 7 ++- tests/include/api_sql_common.sh | 35 +++++++++++-- tests/t/api_sql_mysql.t | 28 ++++++++++- 9 files changed, 150 insertions(+), 31 deletions(-) diff --git a/sysbench/db_driver.h b/sysbench/db_driver.h index f969b93..d46b510 100644 --- a/sysbench/db_driver.h +++ b/sysbench/db_driver.h @@ -226,6 +226,7 @@ typedef struct db_conn db_error_t error; /* Database-independent error code */ int sql_errno; /* Database-specific error code */ const char *sql_state; /* Database-specific SQL state */ + const char *sql_errmsg; /* Database-specific error message */ db_driver_t *driver; /* DB driver for this connection */ void *ptr; /* Driver-specific data */ db_result_t rs; /* Result set */ @@ -245,6 +246,7 @@ typedef struct db_conn sizeof(void *) + sizeof(void *) + sizeof(void *) + + sizeof(void *) + sizeof(db_result_t) + sizeof(db_conn_state_t) + sizeof(int) + diff --git a/sysbench/drivers/mysql/drv_mysql.c b/sysbench/drivers/mysql/drv_mysql.c index bd789ba..e73af37 100644 --- a/sysbench/drivers/mysql/drv_mysql.c +++ b/sysbench/drivers/mysql/drv_mysql.c @@ -709,9 +709,13 @@ static db_error_t check_error(db_conn_t *sb_con, const char *func, DEBUG("mysql_errno(%p) = %u", con, sb_con->sql_errno); sb_con->sql_errno = (int) error; + sb_con->sql_state = mysql_sqlstate(con); DEBUG("mysql_state(%p) = %s", con, sb_con->sql_state); + sb_con->sql_errmsg = mysql_error(con); + DEBUG("mysql_error(%p) = %s", con, sb_con->sql_errmsg); + /* Check if the error code is specified in --mysql-ignore-errors, and return DB_ERROR_IGNORABLE if so, or DB_ERROR_FATAL otherwise @@ -724,7 +728,7 @@ static db_error_t check_error(db_conn_t *sb_con, const char *func, if (error == tmp || !strcmp(val, "all")) { - log_text(LOG_DEBUG, "Ignoring error %u %s, ", error, mysql_error(con)); + log_text(LOG_DEBUG, "Ignoring error %u %s, ", error, sb_con->sql_errmsg); /* Check if we should reconnect */ switch (error) @@ -751,10 +755,10 @@ static db_error_t check_error(db_conn_t *sb_con, const char *func, if (query) log_text(LOG_FATAL, "%s returned error %u (%s) for query '%s'", - func, error, mysql_error(con), query); + func, error, sb_con->sql_errmsg, query); else log_text(LOG_FATAL, "%s returned error %u (%s)", - func, error, mysql_error(con)); + func, error, sb_con->sql_errmsg); *type = DB_STAT_ERROR; diff --git a/sysbench/drivers/pgsql/drv_pgsql.c b/sysbench/drivers/pgsql/drv_pgsql.c index 8dc18d9..5b055f6 100644 --- a/sysbench/drivers/pgsql/drv_pgsql.c +++ b/sysbench/drivers/pgsql/drv_pgsql.c @@ -497,8 +497,7 @@ static db_error_t pgsql_check_status(db_conn_t *con, PGresult *pgres, rs->stat_type = DB_STAT_ERROR; con->sql_state = PQresultErrorField(pgres, PG_DIAG_SQLSTATE); - - const char * const errmsg = PQerrorMessage(pgcon); + con->sql_errmsg = PQerrorMessage(pgcon); if (!strcmp(con->sql_state, "40P01") /* deadlock_detected */ || !strcmp(con->sql_state, "23505") /* unique violation */) @@ -508,7 +507,8 @@ static db_error_t pgsql_check_status(db_conn_t *con, PGresult *pgres, } else { - log_text(LOG_FATAL, "%s() failed: %d %s", funcname, status, errmsg); + log_text(LOG_FATAL, "%s() failed: %d %s", funcname, status, + con->sql_errmsg); if (query != NULL) log_text(LOG_FATAL, "failed query was: %s", query); diff --git a/sysbench/lua/internal/sysbench.lua b/sysbench/lua/internal/sysbench.lua index 6fcd6aa..05e0ab5 100644 --- a/sysbench/lua/internal/sysbench.lua +++ b/sysbench/lua/internal/sysbench.lua @@ -46,6 +46,14 @@ function thread_run(thread_id) end end +-- ---------------------------------------------------------------------- +-- Hooks +-- ---------------------------------------------------------------------- + +sysbench.hooks = { + -- sql_error = , +} + -- ---------------------------------------------------------------------- -- Compatibility wrappers/aliases. These may be removed in later versions -- ---------------------------------------------------------------------- diff --git a/sysbench/lua/internal/sysbench.sql.lua b/sysbench/lua/internal/sysbench.sql.lua index fd86f2c..c37d6a3 100644 --- a/sysbench/lua/internal/sysbench.sql.lua +++ b/sysbench/lua/internal/sysbench.sql.lua @@ -74,11 +74,18 @@ typedef struct sql_error_t error; /* Driver-independent error code */ int sql_errno; /* Driver-specific error code */ const char *sql_state; /* Database-specific SQL state */ + const char *sql_errmsg; /* Database-specific error message */ + sql_driver *driver; /* DB driver for this connection */ const char opaque[?]; } sql_connection; -typedef struct db_stmt sql_statement; +typedef struct +{ + sql_connection *connection; + + const char opaque[?]; +} sql_statement; /* Result set definition */ @@ -208,33 +215,54 @@ function connection_methods.disconnect(self) return assert(ffi.C.db_connection_close(self) == 0) end -function connection_methods.check_error(self, rs) +function connection_methods.check_error(self, rs, query) if rs ~= nil or self.error == sysbench.sql.error.NONE then return rs end - local sql_state = self.sql_state ~= nil and - ffi.string(self.sql_state) or 'unknown' - - if self.error == sysbench.sql.error.IGNORABLE then - -- Throw an error containing the SQL error number provided by the SQL - -- driver. It can be caught by the user script to do some extra steps to - -- restart a transaction (e.g. reprepare statements after a - -- reconnect). Otherwise it will be caught by thread_run() in - -- sysbench.lua, in which case the event will be restarted. - error({ errcode = sysbench.error.RESTART_EVENT, - error = self.sql_errno, - sql_errno = self.sql_errno, - sql_state = sql_state}) + if self.sql_state == nil or self.sql_errmsg == nil then + -- It must be an API error, don't bother trying to downgrade it an + -- ignorable error + error("SQL API error", 3) end - error(string.format("SQL error, sql_errno = %d, sql_state = '%s'", - self.sql_errno, sql_state)) + local sql_state = ffi.string(self.sql_state) + local sql_errmsg = ffi.string(self.sql_errmsg) + + -- Create an error descriptor containing connection, failed query, SQL error + -- number, state and error message provided by the SQL driver + errdesc = { + connection = self, + query = query, + sql_errno = self.sql_errno, + sql_state = sql_state, + sql_errmsg = sql_errmsg + } + + -- Check if the error has already been marked as ignorable by the driver, or + -- there is an error hook that allows downgrading it to IGNORABLE + if (self.error == sysbench.sql.error.FATAL and + type(sysbench.hooks.sql_error_ignorable) == "function" and + sysbench.hooks.sql_error_ignorable(errdesc)) or + self.error == sysbench.sql.error.IGNORABLE + then + -- Throw a 'restart event' exception that can be caught by the user script + -- to do some extra steps to restart a transaction (e.g. reprepare + -- statements after a reconnect). Otherwise it will be caught by + -- thread_run() in sysbench.lua, in which case the entire current event + -- will be restarted without extra processing. + errdesc.errcode = sysbench.error.RESTART_EVENT + error(errdesc, 3) + end + + -- Just throw a regular error message on a fatal error + error(string.format("SQL error, errno = %d, state = '%s': %s", + self.sql_errno, sql_state, sql_errmsg), 2) end function connection_methods.query(self, query) local rs = ffi.C.db_query(self, query, #query) - return self:check_error(rs) + return self:check_error(rs, query) end function connection_methods.bulk_insert_init(self, query) @@ -259,6 +287,7 @@ end -- row from the result set, if available, or nil otherwise function connection_methods.query_row(self, query) local rs = self:query(query) + if rs == nil then return nil end @@ -321,7 +350,8 @@ function statement_methods.bind_param(self, ...) end function statement_methods.execute(self) - return ffi.C.db_execute(self) + local rs = ffi.C.db_execute(self) + return self.connection:check_error(rs, '') end function statement_methods.close(self) diff --git a/sysbench/sb_lua.c b/sysbench/sb_lua.c index 8fbf35b..f63bee1 100644 --- a/sysbench/sb_lua.c +++ b/sysbench/sb_lua.c @@ -182,7 +182,7 @@ static void check_connection(lua_State *L, sb_lua_ctxt_t *ctxt) static bool func_available(lua_State *L, const char *func) { lua_getglobal(L, func); - bool rc = !lua_isnil(L, -1) && lua_type(L, -1) == LUA_TFUNCTION; + bool rc = lua_isfunction(L, -1); lua_pop(L, 1); return rc; @@ -1110,3 +1110,18 @@ int sb_lua_event_stop(lua_State *L) return 0; } + +/* Check if a specified hook exists */ + +bool sb_lua_hook_defined(lua_State *L, const char *name) +{ + lua_getglobal(L, "sysbench"); + lua_getfield(L, -1, "hooks"); + lua_getfield(L, -1, name); + + bool rc = lua_isfunction(L, -1); + + lua_pop(L, 3); + + return rc; +} diff --git a/sysbench/sb_lua.h b/sysbench/sb_lua.h index cffcf2b..412dc76 100644 --- a/sysbench/sb_lua.h +++ b/sysbench/sb_lua.h @@ -1,5 +1,5 @@ /* Copyright (C) 2006 MySQL AB - Copyright (C) 2006-2016 Alexey Kopytov + Copyright (C) 2006-2017 Alexey Kopytov This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -21,7 +21,12 @@ #endif #include "sysbench.h" +#include "lua.h" /* Load a specified Lua script */ sb_test_t *sb_load_lua(const char *testname); + +bool sb_lua_hook_defined(lua_State *L, const char *name); + +int sb_lua_hook_call(const char *name); diff --git a/tests/include/api_sql_common.sh b/tests/include/api_sql_common.sh index 1f774cb..eb6650d 100644 --- a/tests/include/api_sql_common.sh +++ b/tests/include/api_sql_common.sh @@ -109,10 +109,9 @@ function event() print('--') end EOF - +sysbench $SB_ARGS run # Failed connection handling -sysbench $SB_ARGS run SB_ARGS="--verbosity=1 --test=$CRAMTMP/api_sql.lua --max-requests=1 --num-threads=1 $DB_DRIVER_ARGS" cat >$CRAMTMP/api_sql.lua <$CRAMTMP/api_sql.lua < (esc) + query = \tINSERT INTO t VALUES (NULL) (esc) + sql_state = \t23000 (esc) + sql_errmsg = \tColumn 'a' cannot be null (esc) + */api_sql.lua:*: SQL error, errno = 1048, state = '23000': Column 'a' cannot be null (glob) + FATAL: mysql_drv_query() returned error 1406 (Data too long for column 'a' at row 1) for query 'INSERT INTO t VALUES ("test")' + Got an error descriptor: + sql_errno = \t1406 (esc) + connection = \t (esc) + query = \tINSERT INTO t VALUES ("test") (esc) + sql_state = \t22001 (esc) + sql_errmsg = \tData too long for column 'a' at row 1 (esc) + */api_sql.lua:*: SQL error, errno = 1406, state = '22001': Data too long for column 'a' at row 1 (glob) + FATAL: mysql_drv_query() returned error 1051 (Unknown table 'sbtest.t') for query 'DROP TABLE t' + Got an error descriptor: + sql_errno = \t1051 (esc) + connection = \t (esc) + query = \tDROP TABLE t (esc) + sql_state = \t42S02 (esc) + sql_errmsg = \tUnknown table 'sbtest.t' (esc) + */api_sql.lua:*: SQL error, errno = 1051, state = '42S02': Unknown table 'sbtest.t' (glob) + -- 1 2 3