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.
This commit is contained in:
Alexey Kopytov
2017-01-21 16:58:38 +03:00
parent 609f7c4db3
commit cdabf72b7b
9 changed files with 150 additions and 31 deletions

View File

@ -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) +

View File

@ -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;

View File

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

View File

@ -46,6 +46,14 @@ function thread_run(thread_id)
end
end
-- ----------------------------------------------------------------------
-- Hooks
-- ----------------------------------------------------------------------
sysbench.hooks = {
-- sql_error = <func>,
}
-- ----------------------------------------------------------------------
-- Compatibility wrappers/aliases. These may be removed in later versions
-- ----------------------------------------------------------------------

View File

@ -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, '<prepared statement>')
end
function statement_methods.close(self)

View File

@ -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;
}

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2006 MySQL AB
Copyright (C) 2006-2016 Alexey Kopytov <akopytov@gmail.com>
Copyright (C) 2006-2017 Alexey Kopytov <akopytov@gmail.com>
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);

View File

@ -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 <<EOF
@ -122,9 +121,39 @@ function event()
print(m)
end
EOF
sysbench $SB_ARGS --mysql-host="non-existing" --pgsql-host="non-existing" run
# Error hooks
SB_ARGS="--verbosity=1 --test=$CRAMTMP/api_sql.lua --max-requests=1 --num-threads=1 $DB_DRIVER_ARGS"
cat >$CRAMTMP/api_sql.lua <<EOF
function sysbench.hooks.sql_error_ignorable(e)
print("Got an error descriptor:")
for k,v in pairs(e) do print(" " .. k .. " = ", v) end
end
function event()
local drv = sysbench.sql.driver()
local con = drv:connect()
local queries = {
"CREATE TABLE t(a CHAR(1) NOT NULL)",
"INSERT INTO t VALUES (1)",
"INSERT INTO t VALUES (NULL)",
[[INSERT INTO t VALUES ("test")]],
"DROP TABLE t",
"DROP TABLE t"
}
print('--')
con:query("DROP TABLE IF EXISTS t")
for i = 1, #queries do
local e, m = pcall(function () con:query(queries[i]) end)
if not e then print(m) end
end
print('--')
end
EOF
sysbench $SB_ARGS run
########################################################################
# Multiple connections test
########################################################################

View File

@ -50,7 +50,7 @@ SQL Lua API + MySQL tests
--
(last message repeated 1 times)
ALERT: attempt to use an already closed connection
[string "sysbench.sql.lua"]:*: SQL error, sql_errno = 0, sql_state = 'unknown' (glob)
*/api_sql.lua:*: SQL API error (glob)
ALERT: attempt to close an already closed connection
--
4
@ -59,6 +59,32 @@ SQL Lua API + MySQL tests
FATAL: unable to connect to MySQL server on host 'non-existing', port 3306, aborting...
FATAL: error 2005: Unknown MySQL server host 'non-existing' (0)
connection creation failed
--
FATAL: mysql_drv_query() returned error 1048 (Column 'a' cannot be null) for query 'INSERT INTO t VALUES (NULL)'
Got an error descriptor:
sql_errno = \t1048 (esc)
connection = \t<sql_connection> (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<sql_connection> (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<sql_connection> (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