Commit 8e9e1c39 authored by sjaakola's avatar sjaakola Committed by Jan Lindström

MDEV-27649 Crash with PS execute after BF abort

This commit contains a test for reproducing the issue in MDEV-27649,
where a transaction, executing a prepared statment, is BF aborted.
The scenario, in MDEV-27649  has a transaction which has prepared a PS,
but not yet executed it, and this transaction is then BF aborted in this state.
When the BF aborted transaction tries to execute the PS, it will receive deadlock error.
But, when it tries to execute the PS second time, the node crashes.

Mtr test galera.galera_bf_abort_ps_bind, exercises this scenario.

However, mtr test platform does not have mechanism to control the execution of PS in required detail.
For this purpose, mysqltetst.cc was extended to contain 4 new commands:
PS_prepare   - to prepare a prepared statement
PS_bind      - to bind values for parameters for the PS
PS_execute   - to execute the PS
PS_close     - to close the PS

The support for controlling prepared statments in mtr scripts is quite minimal
in this commit. Limitations are:
* only one PS can be used by a connection, at a time
* only input parameters can be bound for the PS
* only varchar, integer or float type of parameters can be bound

added the result

fixes
Reviewed-by: default avatarJan Lindström <jan.lindstrom@mariadb.com>
parent 069139a5
......@@ -319,6 +319,7 @@ struct st_connection
char *name;
size_t name_len;
MYSQL_STMT* stmt;
MYSQL_BIND *ps_params;
/* Set after send to disallow other queries before reap */
my_bool pending;
......@@ -393,6 +394,10 @@ enum enum_commands {
Q_ENABLE_PREPARE_WARNINGS, Q_DISABLE_PREPARE_WARNINGS,
Q_RESET_CONNECTION,
Q_OPTIMIZER_TRACE,
Q_PS_PREPARE,
Q_PS_BIND,
Q_PS_EXECUTE,
Q_PS_CLOSE,
Q_UNKNOWN, /* Unknown command. */
Q_COMMENT, /* Comments, ignored. */
Q_COMMENT_WITH_COMMAND,
......@@ -506,6 +511,10 @@ const char *command_names[]=
"disable_prepare_warnings",
"reset_connection",
"optimizer_trace",
"PS_prepare",
"PS_bind",
"PS_execute",
"PS_close",
0
};
......@@ -7848,6 +7857,15 @@ static void handle_no_active_connection(struct st_command *command,
var_set_errno(2006);
}
/* handler functions to execute prepared statement calls in client C API */
void run_prepare_stmt(struct st_connection *cn, struct st_command *command, const char *query,
size_t query_len, DYNAMIC_STRING *ds, DYNAMIC_STRING *ds_warnings);
void run_bind_stmt(struct st_connection *cn, struct st_command *command, const char *query,
size_t query_len, DYNAMIC_STRING *ds, DYNAMIC_STRING *ds_warnings);
void run_execute_stmt(struct st_connection *cn, struct st_command *command, const char *query,
size_t query_len, DYNAMIC_STRING *ds, DYNAMIC_STRING *ds_warnings);
void run_close_stmt(struct st_connection *cn, struct st_command *command, const char *query,
size_t query_len, DYNAMIC_STRING *ds, DYNAMIC_STRING *ds_warnings);
/*
Run query using MySQL C API
......@@ -7879,6 +7897,32 @@ void run_query_normal(struct st_connection *cn, struct st_command *command,
DBUG_VOID_RETURN;
}
/* handle prepared statement commands */
switch (command->type) {
case Q_PS_PREPARE:
run_prepare_stmt(cn, command, query, query_len, ds, ds_warnings);
flags &= ~QUERY_SEND_FLAG;
goto end;
break;
case Q_PS_BIND:
run_bind_stmt(cn, command, query, query_len, ds, ds_warnings);
flags &= ~QUERY_SEND_FLAG;
goto end;
break;
case Q_PS_EXECUTE:
run_execute_stmt(cn, command, query, query_len, ds, ds_warnings);
flags &= ~QUERY_SEND_FLAG;
goto end;
break;
case Q_PS_CLOSE:
run_close_stmt(cn, command, query, query_len, ds, ds_warnings);
flags &= ~QUERY_SEND_FLAG;
goto end;
break;
default: /* not a prepared statement command */
break;
}
if (flags & QUERY_SEND_FLAG)
{
/*
......@@ -8434,6 +8478,408 @@ void run_query_stmt(struct st_connection *cn, struct st_command *command,
DBUG_VOID_RETURN;
}
/*
prepare query using prepared statement C API
SYNPOSIS
run_prepare_stmt
mysql - mysql handle
command - current command pointer
query - query string to execute
query_len - length query string to execute
ds - output buffer where to store result form query
RETURN VALUE
error - function will not return
*/
void run_prepare_stmt(struct st_connection *cn, struct st_command *command, const char *query, size_t query_len, DYNAMIC_STRING *ds, DYNAMIC_STRING *ds_warnings)
{
MYSQL *mysql= cn->mysql;
MYSQL_STMT *stmt;
DYNAMIC_STRING ds_prepare_warnings;
DBUG_ENTER("run_prepare_stmt");
DBUG_PRINT("query", ("'%-.60s'", query));
/*
Init a new stmt if it's not already one created for this connection
*/
if(!(stmt= cn->stmt))
{
if (!(stmt= mysql_stmt_init(mysql)))
die("unable to init stmt structure");
cn->stmt= stmt;
}
/* Init dynamic strings for warnings */
if (!disable_warnings)
{
init_dynamic_string(&ds_prepare_warnings, NULL, 0, 256);
}
/*
Prepare the query
*/
char* PS_query= command->first_argument;
size_t PS_query_len= command->end - command->first_argument;
if (do_stmt_prepare(cn, PS_query, PS_query_len))
{
handle_error(command, mysql_stmt_errno(stmt),
mysql_stmt_error(stmt), mysql_stmt_sqlstate(stmt), ds);
goto end;
}
/*
Get the warnings from mysql_stmt_prepare and keep them in a
separate string
*/
if (!disable_warnings)
append_warnings(&ds_prepare_warnings, mysql);
end:
DBUG_VOID_RETURN;
}
/*
bind parameters for a prepared statement C API
SYNPOSIS
run_bind_stmt
mysql - mysql handle
command - current command pointer
query - query string to execute
query_len - length query string to execute
ds - output buffer where to store result form query
RETURN VALUE
error - function will not return
*/
void run_bind_stmt(struct st_connection *cn, struct st_command *command,
const char *query, size_t query_len, DYNAMIC_STRING *ds,
DYNAMIC_STRING *ds_warnings
)
{
MYSQL_STMT *stmt= cn->stmt;
DBUG_ENTER("run_bind_stmt");
DBUG_PRINT("query", ("'%-.60s'", query));
MYSQL_BIND *ps_params= cn->ps_params;
if (ps_params)
{
for (size_t i=0; i<stmt->param_count; i++)
{
my_free(ps_params[i].buffer);
ps_params[i].buffer= NULL;
}
my_free(ps_params);
ps_params= NULL;
}
/* Init PS-parameters. */
cn->ps_params= ps_params = (MYSQL_BIND*)my_malloc(sizeof(MYSQL_BIND) * stmt->param_count,
MYF(MY_WME));
bzero((char *) ps_params, sizeof(MYSQL_BIND) * stmt->param_count);
int i=0;
char *c;
long *l;
double *d;
char *p= strtok((char*)command->first_argument, " ");
while (p != nullptr)
{
(void)strtol(p, &c, 10);
if (!*c)
{
ps_params[i].buffer_type= MYSQL_TYPE_LONG;
l= (long*)my_malloc(sizeof(long), MYF(MY_WME));
*l= strtol(p, &c, 10);
ps_params[i].buffer= (void*)l;
ps_params[i].buffer_length= 8;
}
else
{
(void)strtod(p, &c);
if (!*c)
{
ps_params[i].buffer_type= MYSQL_TYPE_DECIMAL;
d= (double*)my_malloc(sizeof(double), MYF(MY_WME));
*d= strtod(p, &c);
ps_params[i].buffer= (void*)d;
ps_params[i].buffer_length= 8;
}
else
{
ps_params[i].buffer_type= MYSQL_TYPE_STRING;
ps_params[i].buffer= strdup(p);
ps_params[i].buffer_length= (unsigned long)strlen(p);
}
}
p= strtok(nullptr, " ");
i++;
}
int rc= mysql_stmt_bind_param(stmt, ps_params);
if (rc)
{
die("mysql_stmt_bind_param() failed': %d %s",
mysql_stmt_errno(stmt), mysql_stmt_error(stmt));
}
DBUG_VOID_RETURN;
}
/*
execute query using prepared statement C API
SYNPOSIS
run_axecute_stmt
mysql - mysql handle
command - current command pointer
query - query string to execute
query_len - length query string to execute
ds - output buffer where to store result form query
RETURN VALUE
error - function will not return
*/
void run_execute_stmt(struct st_connection *cn, struct st_command *command,
const char *query, size_t query_len, DYNAMIC_STRING *ds,
DYNAMIC_STRING *ds_warnings
)
{
MYSQL_RES *res= NULL; /* Note that here 'res' is meta data result set */
MYSQL *mysql= cn->mysql;
MYSQL_STMT *stmt= cn->stmt;
DYNAMIC_STRING ds_execute_warnings;
DBUG_ENTER("run_execute_stmt");
DBUG_PRINT("query", ("'%-.60s'", query));
/* Init dynamic strings for warnings */
if (!disable_warnings)
{
init_dynamic_string(&ds_execute_warnings, NULL, 0, 256);
}
#if MYSQL_VERSION_ID >= 50000
if (cursor_protocol_enabled)
{
/*
Use cursor when retrieving result
*/
ulong type= CURSOR_TYPE_READ_ONLY;
if (mysql_stmt_attr_set(stmt, STMT_ATTR_CURSOR_TYPE, (void*) &type))
die("mysql_stmt_attr_set(STMT_ATTR_CURSOR_TYPE) failed': %d %s",
mysql_stmt_errno(stmt), mysql_stmt_error(stmt));
}
#endif
/*
Execute the query
*/
if (do_stmt_execute(cn))
{
handle_error(command, mysql_stmt_errno(stmt),
mysql_stmt_error(stmt), mysql_stmt_sqlstate(stmt), ds);
goto end;
}
/*
When running in cursor_protocol get the warnings from execute here
and keep them in a separate string for later.
*/
if (cursor_protocol_enabled && !disable_warnings)
append_warnings(&ds_execute_warnings, mysql);
/*
We instruct that we want to update the "max_length" field in
mysql_stmt_store_result(), this is our only way to know how much
buffer to allocate for result data
*/
{
my_bool one= 1;
if (mysql_stmt_attr_set(stmt, STMT_ATTR_UPDATE_MAX_LENGTH, (void*) &one))
die("mysql_stmt_attr_set(STMT_ATTR_UPDATE_MAX_LENGTH) failed': %d %s",
mysql_stmt_errno(stmt), mysql_stmt_error(stmt));
}
/*
If we got here the statement succeeded and was expected to do so,
get data. Note that this can still give errors found during execution!
Store the result of the query if if will return any fields
*/
if (mysql_stmt_field_count(stmt) && mysql_stmt_store_result(stmt))
{
handle_error(command, mysql_stmt_errno(stmt),
mysql_stmt_error(stmt), mysql_stmt_sqlstate(stmt), ds);
goto end;
}
/* If we got here the statement was both executed and read successfully */
handle_no_error(command);
if (!disable_result_log)
{
/*
Not all statements creates a result set. If there is one we can
now create another normal result set that contains the meta
data. This set can be handled almost like any other non prepared
statement result set.
*/
if ((res= mysql_stmt_result_metadata(stmt)) != NULL)
{
/* Take the column count from meta info */
MYSQL_FIELD *fields= mysql_fetch_fields(res);
uint num_fields= mysql_num_fields(res);
if (display_metadata)
append_metadata(ds, fields, num_fields);
if (!display_result_vertically)
append_table_headings(ds, fields, num_fields);
append_stmt_result(ds, stmt, fields, num_fields);
mysql_free_result(res); /* Free normal result set with meta data */
/*
Normally, if there is a result set, we do not show warnings from the
prepare phase. This is because some warnings are generated both during
prepare and execute; this would generate different warning output
between normal and ps-protocol test runs.
The --enable_prepare_warnings command can be used to change this so
that warnings from both the prepare and execute phase are shown.
*/
}
else
{
/*
This is a query without resultset
*/
}
/*
Fetch info before fetching warnings, since it will be reset
otherwise.
*/
if (!disable_info)
append_info(ds, mysql_stmt_affected_rows(stmt), mysql_info(mysql));
if (display_session_track_info)
append_session_track_info(ds, mysql);
if (!disable_warnings)
{
/* Get the warnings from execute */
/* Append warnings to ds - if there are any */
if (append_warnings(&ds_execute_warnings, mysql) ||
ds_execute_warnings.length ||
ds_warnings->length)
{
dynstr_append_mem(ds, "Warnings:\n", 10);
if (ds_warnings->length)
dynstr_append_mem(ds, ds_warnings->str,
ds_warnings->length);
if (ds_execute_warnings.length)
dynstr_append_mem(ds, ds_execute_warnings.str,
ds_execute_warnings.length);
}
}
}
end:
if (!disable_warnings)
{
dynstr_free(&ds_execute_warnings);
}
/*
We save the return code (mysql_stmt_errno(stmt)) from the last call sent
to the server into the mysqltest builtin variable $mysql_errno. This
variable then can be used from the test case itself.
*/
var_set_errno(mysql_stmt_errno(stmt));
revert_properties();
/* Close the statement if reconnect, need new prepare */
{
#ifndef EMBEDDED_LIBRARY
my_bool reconnect;
mysql_get_option(mysql, MYSQL_OPT_RECONNECT, &reconnect);
if (reconnect)
#else
if (mysql->reconnect)
#endif
{
if (cn->ps_params)
{
for (size_t i=0; i<stmt->param_count; i++)
{
my_free(cn->ps_params[i].buffer);
cn->ps_params[i].buffer= NULL;
}
my_free(cn->ps_params);
}
mysql_stmt_close(stmt);
cn->stmt= NULL;
cn->ps_params= NULL;
}
}
DBUG_VOID_RETURN;
}
/*
close a prepared statement C API
SYNPOSIS
run_close_stmt
mysql - mysql handle
command - current command pointer
query - query string to execute
query_len - length query string to execute
ds - output buffer where to store result form query
RETURN VALUE
error - function will not return
*/
void run_close_stmt(struct st_connection *cn, struct st_command *command,
const char *query, size_t query_len, DYNAMIC_STRING *ds,
DYNAMIC_STRING *ds_warnings
)
{
MYSQL_STMT *stmt= cn->stmt;
DBUG_ENTER("run_close_stmt");
DBUG_PRINT("query", ("'%-.60s'", query));
if (cn->ps_params)
{
for (size_t i=0; i<stmt->param_count; i++)
{
my_free(cn->ps_params[i].buffer);
cn->ps_params[i].buffer= NULL;
}
my_free(cn->ps_params);
}
/* Close the statement */
if (stmt)
{
mysql_stmt_close(stmt);
cn->stmt= NULL;
}
cn->ps_params= NULL;
DBUG_VOID_RETURN;
}
/*
......@@ -9474,6 +9920,10 @@ int main(int argc, char **argv)
/* fall through */
case Q_QUERY:
case Q_REAP:
case Q_PS_PREPARE:
case Q_PS_BIND:
case Q_PS_EXECUTE:
case Q_PS_CLOSE:
{
my_bool old_display_result_vertically= display_result_vertically;
/* Default is full query, both reap and send */
......
connection node_2;
connection node_1;
CREATE TABLE t (i int primary key auto_increment, j varchar(20) character set utf8);
connect node_1a, 127.0.0.1, root, , test, $NODE_MYPORT_1;
connection node_1a;
SET SESSION wsrep_sync_wait = 0;
connection node_1;
insert into t values (1, 'first');
PS_prepare INSERT INTO t(j) VALUES (?);;
PS_bind node1;
PS_execute;
PS_execute;
select * from t;
i j
1 first
3 node1
5 node1
PS_close;
PS_prepare INSERT INTO t(j) VALUES (?);;
PS_bind node1;
begin;
update t set j='node1' where i=1;
connection node_2;
update t set j='node2' where i=1;
connection node_1a;
connection node_1;
PS_execute;
ERROR 40001: Deadlock found when trying to get lock; try restarting transaction
PS_execute;
commit;
select * from t;
i j
1 node2
3 node1
5 node1
7 node1
drop table t;
!include ../galera_2nodes.cnf
[mysqld.1]
wsrep-debug=1
[mysqld.2]
wsrep-debug=1
--source include/galera_cluster.inc
--source include/have_innodb.inc
CREATE TABLE t (i int primary key auto_increment, j varchar(20) character set utf8);
--connect node_1a, 127.0.0.1, root, , test, $NODE_MYPORT_1
--connection node_1a
SET SESSION wsrep_sync_wait = 0;
--connection node_1
insert into t values (1, 'first');
# prepare a statement for inserting rows into table t
--PS_prepare INSERT INTO t(j) VALUES (?);
# bind parameter, to insert with column j having value 'node1'
--PS_bind node1
# insert two rows with the PS
# this is for showing that two execute commands can follow a bind command
--PS_execute
--PS_execute
select * from t;
# close the prepared statement, and prepare a new PS,
# this happens to be same as the first PS
# also bind parameter for the PS
--PS_close
--PS_prepare INSERT INTO t(j) VALUES (?);
--PS_bind node1
# start a transaction and make one update
# leaving the transaction open
begin;
update t set j='node1' where i=1;
# replicate a transaction from node2, which BF aborts the open
# transaction in node1
--connection node_2
update t set j='node2' where i=1;
# wait until the BF has completed, and update from node_2 has committed
--connection node_1a
--let $wait_condition = SELECT COUNT(*) = 1 FROM t WHERE j='node2'
--source include/wait_condition.inc
# continue the open transaction, trying to insert third row, deadlock is now observed
--connection node_1
--error ER_LOCK_DEADLOCK
--PS_execute
# try to insert one more row
--PS_execute
commit;
select * from t;
drop table t;
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment