Decrease stack space usage of mysql_execute_command()

The extensive usage of stack space, especially when used with ASan
(AdressSanitizer) of mysql_execute_command caused the test
rpl.rpl_row_sp011 to fail because it did run out of stack.  In this
test case mysql_execute_command is called recursively for each
function all.

Changes done:
- Changed a few functions that used big local variables to be marked
  __attribute__ ((noinline))
- Moved sub parts that used big local variables to external functions.
- Changed wsrep_commit_empty() from inline to normal function as this used
  more than 1K of stack space and because there is no reason for this
  rarely used function to be inline.

End result (with gcc 7.4.1 on Intel Xeon):

Starting point for stack space usage:

gcc -O:                                  7800
gcc with -fsanitize=address -O (ASan) : 27240

After this patch:

gcc -O:                                  1160
gcc -O0 (debug build)                    1584
gcc with -fsanitize=address -O (ASan):   4424
gcc with -fsanitize=address -O2 (ASan):  3874

A 6x improvement and will allow us to run all mtr tests with ASan.
This commit is contained in:
Monty 2019-08-17 17:05:46 +03:00 committed by Sergei Golubchik
parent e21408b799
commit 1fbaf8b6a8
3 changed files with 305 additions and 248 deletions

View file

@ -133,6 +133,10 @@ static void sql_kill_user(THD *thd, LEX_USER *user, killed_state state);
static bool lock_tables_precheck(THD *thd, TABLE_LIST *tables);
static bool execute_show_status(THD *, TABLE_LIST *);
static bool check_rename_table(THD *, TABLE_LIST *, TABLE_LIST *);
static bool generate_incident_event(THD *thd);
static int show_create_db(THD *thd, LEX *lex);
static bool alter_routine(THD *thd, LEX *lex);
static bool drop_routine(THD *thd, LEX *lex);
const char *any_db="*any*"; // Special symbol for check_access
@ -2873,7 +2877,8 @@ bool sp_process_definer(THD *thd)
@return FALSE in case of success, TRUE in case of error.
*/
static bool lock_tables_open_and_lock_tables(THD *thd, TABLE_LIST *tables)
static bool __attribute__ ((noinline))
lock_tables_open_and_lock_tables(THD *thd, TABLE_LIST *tables)
{
Lock_tables_prelocking_strategy lock_tables_prelocking_strategy;
MDL_deadlock_and_lock_abort_error_handler deadlock_handler;
@ -3034,7 +3039,8 @@ static bool do_execute_sp(THD *thd, sp_head *sp)
}
static int mysql_create_routine(THD *thd, LEX *lex)
static int __attribute__ ((noinline))
mysql_create_routine(THD *thd, LEX *lex)
{
DBUG_ASSERT(lex->sphead != 0);
DBUG_ASSERT(lex->sphead->m_db.str); /* Must be initialized in the parser */
@ -4443,40 +4449,8 @@ mysql_execute_command(THD *thd)
break;
}
case SQLCOM_REPLACE:
#ifndef DBUG_OFF
if (mysql_bin_log.is_open())
{
/*
Generate an incident log event before writing the real event
to the binary log. We put this event is before the statement
since that makes it simpler to check that the statement was
not executed on the slave (since incidents usually stop the
slave).
Observe that any row events that are generated will be
generated before.
This is only for testing purposes and will not be present in a
release build.
*/
Incident incident= INCIDENT_NONE;
DBUG_PRINT("debug", ("Just before generate_incident()"));
DBUG_EXECUTE_IF("incident_database_resync_on_replace",
incident= INCIDENT_LOST_EVENTS;);
if (incident)
{
Incident_log_event ev(thd, incident);
(void) mysql_bin_log.write(&ev); /* error is ignored */
if (mysql_bin_log.rotate_and_purge(true))
{
res= 1;
break;
}
}
DBUG_PRINT("debug", ("Just after generate_incident()"));
}
#endif
if ((res= generate_incident_event(thd)))
break;
/* fall through */
case SQLCOM_INSERT:
{
@ -5090,26 +5064,9 @@ mysql_execute_command(THD *thd)
break;
}
case SQLCOM_SHOW_CREATE_DB:
{
char db_name_buff[NAME_LEN+1];
LEX_CSTRING db_name;
DBUG_EXECUTE_IF("4x_server_emul",
my_error(ER_UNKNOWN_ERROR, MYF(0)); goto error;);
db_name.str= db_name_buff;
db_name.length= lex->name.length;
strmov(db_name_buff, lex->name.str);
WSREP_SYNC_WAIT(thd, WSREP_SYNC_WAIT_BEFORE_SHOW);
if (check_db_name((LEX_STRING*) &db_name))
{
my_error(ER_WRONG_DB_NAME, MYF(0), db_name.str);
break;
}
res= mysqld_show_create_db(thd, &db_name, &lex->name, lex->create_info);
res= show_create_db(thd, lex);
break;
}
case SQLCOM_CREATE_EVENT:
case SQLCOM_ALTER_EVENT:
#ifdef HAVE_EVENT_SCHEDULER
@ -5689,153 +5646,16 @@ mysql_execute_command(THD *thd)
case SQLCOM_ALTER_PROCEDURE:
case SQLCOM_ALTER_FUNCTION:
{
int sp_result;
const Sp_handler *sph= Sp_handler::handler(lex->sql_command);
if (check_routine_access(thd, ALTER_PROC_ACL, &lex->spname->m_db,
&lex->spname->m_name, sph, 0))
goto error;
/*
Note that if you implement the capability of ALTER FUNCTION to
alter the body of the function, this command should be made to
follow the restrictions that log-bin-trust-function-creators=0
already puts on CREATE FUNCTION.
*/
/* Conditionally writes to binlog */
sp_result= sph->sp_update_routine(thd, lex->spname, &lex->sp_chistics);
switch (sp_result)
{
case SP_OK:
my_ok(thd);
break;
case SP_KEY_NOT_FOUND:
my_error(ER_SP_DOES_NOT_EXIST, MYF(0),
sph->type_str(), ErrConvDQName(lex->spname).ptr());
goto error;
default:
my_error(ER_SP_CANT_ALTER, MYF(0),
sph->type_str(), ErrConvDQName(lex->spname).ptr());
goto error;
}
break;
}
if (alter_routine(thd, lex))
goto error;
break;
case SQLCOM_DROP_PROCEDURE:
case SQLCOM_DROP_FUNCTION:
case SQLCOM_DROP_PACKAGE:
case SQLCOM_DROP_PACKAGE_BODY:
{
#ifdef HAVE_DLOPEN
if (lex->sql_command == SQLCOM_DROP_FUNCTION &&
! lex->spname->m_explicit_name)
{
/* DROP FUNCTION <non qualified name> */
udf_func *udf = find_udf(lex->spname->m_name.str,
lex->spname->m_name.length);
if (udf)
{
if (check_access(thd, DELETE_ACL, "mysql", NULL, NULL, 1, 0))
goto error;
if (!(res = mysql_drop_function(thd, &lex->spname->m_name)))
{
my_ok(thd);
break;
}
my_error(ER_SP_DROP_FAILED, MYF(0),
"FUNCTION (UDF)", lex->spname->m_name.str);
goto error;
}
if (lex->spname->m_db.str == NULL)
{
if (lex->if_exists())
{
push_warning_printf(thd, Sql_condition::WARN_LEVEL_NOTE,
ER_SP_DOES_NOT_EXIST, ER_THD(thd, ER_SP_DOES_NOT_EXIST),
"FUNCTION (UDF)", lex->spname->m_name.str);
res= FALSE;
my_ok(thd);
break;
}
my_error(ER_SP_DOES_NOT_EXIST, MYF(0),
"FUNCTION (UDF)", lex->spname->m_name.str);
goto error;
}
/* Fall thought to test for a stored function */
}
#endif
int sp_result;
const Sp_handler *sph= Sp_handler::handler(lex->sql_command);
if (check_routine_access(thd, ALTER_PROC_ACL, &lex->spname->m_db, &lex->spname->m_name,
Sp_handler::handler(lex->sql_command), 0))
goto error;
WSREP_TO_ISOLATION_BEGIN(WSREP_MYSQL_DB, NULL, NULL);
/* Conditionally writes to binlog */
sp_result= sph->sp_drop_routine(thd, lex->spname);
#ifndef NO_EMBEDDED_ACCESS_CHECKS
/*
We're going to issue an implicit REVOKE statement so we close all
open tables. We have to keep metadata locks as this ensures that
this statement is atomic against concurent FLUSH TABLES WITH READ
LOCK. Deadlocks which can arise due to fact that this implicit
statement takes metadata locks should be detected by a deadlock
detector in MDL subsystem and reported as errors.
No need to commit/rollback statement transaction, it's not started.
TODO: Long-term we should either ensure that implicit REVOKE statement
is written into binary log as a separate statement or make both
dropping of routine and implicit REVOKE parts of one fully atomic
statement.
*/
DBUG_ASSERT(thd->transaction.stmt.is_empty());
close_thread_tables(thd);
if (sp_result != SP_KEY_NOT_FOUND &&
sp_automatic_privileges && !opt_noacl &&
sp_revoke_privileges(thd, lex->spname->m_db.str, lex->spname->m_name.str,
Sp_handler::handler(lex->sql_command)))
{
push_warning(thd, Sql_condition::WARN_LEVEL_WARN,
ER_PROC_AUTO_REVOKE_FAIL,
ER_THD(thd, ER_PROC_AUTO_REVOKE_FAIL));
/* If this happens, an error should have been reported. */
goto error;
}
#endif
res= sp_result;
switch (sp_result) {
case SP_OK:
my_ok(thd);
break;
case SP_KEY_NOT_FOUND:
if (lex->if_exists())
{
res= write_bin_log(thd, TRUE, thd->query(), thd->query_length());
push_warning_printf(thd, Sql_condition::WARN_LEVEL_NOTE,
ER_SP_DOES_NOT_EXIST, ER_THD(thd, ER_SP_DOES_NOT_EXIST),
sph->type_str(),
ErrConvDQName(lex->spname).ptr());
if (!res)
my_ok(thd);
break;
}
my_error(ER_SP_DOES_NOT_EXIST, MYF(0),
sph->type_str(), ErrConvDQName(lex->spname).ptr());
goto error;
default:
my_error(ER_SP_DROP_FAILED, MYF(0),
sph->type_str(), ErrConvDQName(lex->spname).ptr());
goto error;
}
break;
}
if (drop_routine(thd, lex))
goto error;
break;
case SQLCOM_SHOW_CREATE_PROC:
case SQLCOM_SHOW_CREATE_FUNC:
case SQLCOM_SHOW_CREATE_PACKAGE:
@ -6358,7 +6178,15 @@ static bool execute_sqlcom_select(THD *thd, TABLE_LIST *all_tables)
}
static bool execute_show_status(THD *thd, TABLE_LIST *all_tables)
/**
SHOW STATUS
Notes: This is noinline as we don't want to have system_status_var (> 3K)
to be on the stack of mysql_execute_command()
*/
static bool __attribute__ ((noinline))
execute_show_status(THD *thd, TABLE_LIST *all_tables)
{
bool res;
system_status_var old_status_var= thd->status_var;
@ -6443,8 +6271,9 @@ static TABLE *find_temporary_table_for_rename(THD *thd,
}
static bool check_rename_table(THD *thd, TABLE_LIST *first_table,
TABLE_LIST *all_tables)
static bool __attribute__ ((noinline))
check_rename_table(THD *thd, TABLE_LIST *first_table,
TABLE_LIST *all_tables)
{
DBUG_ASSERT(first_table == all_tables && first_table != 0);
TABLE_LIST *table;
@ -6483,6 +6312,227 @@ static bool check_rename_table(THD *thd, TABLE_LIST *first_table,
return 0;
}
/*
Generate an incident log event before writing the real event
to the binary log. We put this event is before the statement
since that makes it simpler to check that the statement was
not executed on the slave (since incidents usually stop the
slave).
Observe that any row events that are generated will be generated before.
This is only for testing purposes and will not be present in a release build.
*/
#ifndef DBUG_OFF
static bool __attribute__ ((noinline)) generate_incident_event(THD *thd)
{
if (mysql_bin_log.is_open())
{
Incident incident= INCIDENT_NONE;
DBUG_PRINT("debug", ("Just before generate_incident()"));
DBUG_EXECUTE_IF("incident_database_resync_on_replace",
incident= INCIDENT_LOST_EVENTS;);
if (incident)
{
Incident_log_event ev(thd, incident);
(void) mysql_bin_log.write(&ev); /* error is ignored */
if (mysql_bin_log.rotate_and_purge(true))
return 1;
}
DBUG_PRINT("debug", ("Just after generate_incident()"));
}
return 0;
}
#else
static bool generate_incident_event(THD *thd)
{
return 0;
}
#endif
static int __attribute__ ((noinline))
show_create_db(THD *thd, LEX *lex)
{
char db_name_buff[NAME_LEN+1];
LEX_CSTRING db_name;
DBUG_EXECUTE_IF("4x_server_emul",
my_error(ER_UNKNOWN_ERROR, MYF(0)); return 1;);
db_name.str= db_name_buff;
db_name.length= lex->name.length;
strmov(db_name_buff, lex->name.str);
if (check_db_name((LEX_STRING*) &db_name))
{
my_error(ER_WRONG_DB_NAME, MYF(0), db_name.str);
return 1;
}
return mysqld_show_create_db(thd, &db_name, &lex->name, lex->create_info);
}
/**
Called on SQLCOM_ALTER_PROCEDURE and SQLCOM_ALTER_FUNCTION
*/
static bool __attribute__ ((noinline))
alter_routine(THD *thd, LEX *lex)
{
int sp_result;
const Sp_handler *sph= Sp_handler::handler(lex->sql_command);
if (check_routine_access(thd, ALTER_PROC_ACL, &lex->spname->m_db,
&lex->spname->m_name, sph, 0))
return 1;
/*
Note that if you implement the capability of ALTER FUNCTION to
alter the body of the function, this command should be made to
follow the restrictions that log-bin-trust-function-creators=0
already puts on CREATE FUNCTION.
*/
/* Conditionally writes to binlog */
sp_result= sph->sp_update_routine(thd, lex->spname, &lex->sp_chistics);
switch (sp_result) {
case SP_OK:
my_ok(thd);
return 0;
case SP_KEY_NOT_FOUND:
my_error(ER_SP_DOES_NOT_EXIST, MYF(0),
sph->type_str(), ErrConvDQName(lex->spname).ptr());
return 1;
default:
my_error(ER_SP_CANT_ALTER, MYF(0),
sph->type_str(), ErrConvDQName(lex->spname).ptr());
return 1;
}
return 0; /* purecov: deadcode */
}
static bool __attribute__ ((noinline))
drop_routine(THD *thd, LEX *lex)
{
int sp_result;
#ifdef HAVE_DLOPEN
if (lex->sql_command == SQLCOM_DROP_FUNCTION &&
! lex->spname->m_explicit_name)
{
/* DROP FUNCTION <non qualified name> */
udf_func *udf = find_udf(lex->spname->m_name.str,
lex->spname->m_name.length);
if (udf)
{
if (check_access(thd, DELETE_ACL, "mysql", NULL, NULL, 1, 0))
return 1;
if (!mysql_drop_function(thd, &lex->spname->m_name))
{
my_ok(thd);
return 0;
}
my_error(ER_SP_DROP_FAILED, MYF(0),
"FUNCTION (UDF)", lex->spname->m_name.str);
return 1;
}
if (lex->spname->m_db.str == NULL)
{
if (lex->if_exists())
{
push_warning_printf(thd, Sql_condition::WARN_LEVEL_NOTE,
ER_SP_DOES_NOT_EXIST,
ER_THD(thd, ER_SP_DOES_NOT_EXIST),
"FUNCTION (UDF)", lex->spname->m_name.str);
my_ok(thd);
return 0;
}
my_error(ER_SP_DOES_NOT_EXIST, MYF(0),
"FUNCTION (UDF)", lex->spname->m_name.str);
return 1;
}
/* Fall trough to test for a stored function */
}
#endif /* HAVE_DLOPEN */
const Sp_handler *sph= Sp_handler::handler(lex->sql_command);
if (check_routine_access(thd, ALTER_PROC_ACL, &lex->spname->m_db,
&lex->spname->m_name,
Sp_handler::handler(lex->sql_command), 0))
return 1;
WSREP_TO_ISOLATION_BEGIN(WSREP_MYSQL_DB, NULL, NULL);
/* Conditionally writes to binlog */
sp_result= sph->sp_drop_routine(thd, lex->spname);
#ifndef NO_EMBEDDED_ACCESS_CHECKS
/*
We're going to issue an implicit REVOKE statement so we close all
open tables. We have to keep metadata locks as this ensures that
this statement is atomic against concurent FLUSH TABLES WITH READ
LOCK. Deadlocks which can arise due to fact that this implicit
statement takes metadata locks should be detected by a deadlock
detector in MDL subsystem and reported as errors.
No need to commit/rollback statement transaction, it's not started.
TODO: Long-term we should either ensure that implicit REVOKE statement
is written into binary log as a separate statement or make both
dropping of routine and implicit REVOKE parts of one fully atomic
statement.
*/
DBUG_ASSERT(thd->transaction.stmt.is_empty());
close_thread_tables(thd);
if (sp_result != SP_KEY_NOT_FOUND &&
sp_automatic_privileges && !opt_noacl &&
sp_revoke_privileges(thd, lex->spname->m_db.str, lex->spname->m_name.str,
Sp_handler::handler(lex->sql_command)))
{
push_warning(thd, Sql_condition::WARN_LEVEL_WARN,
ER_PROC_AUTO_REVOKE_FAIL,
ER_THD(thd, ER_PROC_AUTO_REVOKE_FAIL));
/* If this happens, an error should have been reported. */
return 1;
}
#endif /* NO_EMBEDDED_ACCESS_CHECKS */
switch (sp_result) {
case SP_OK:
my_ok(thd);
return 0;
case SP_KEY_NOT_FOUND:
int res;
if (lex->if_exists())
{
res= write_bin_log(thd, TRUE, thd->query(), thd->query_length());
push_warning_printf(thd, Sql_condition::WARN_LEVEL_NOTE,
ER_SP_DOES_NOT_EXIST,
ER_THD(thd, ER_SP_DOES_NOT_EXIST),
sph->type_str(),
ErrConvDQName(lex->spname).ptr());
if (res)
return 1;
my_ok(thd);
return 0;
}
my_error(ER_SP_DOES_NOT_EXIST, MYF(0),
sph->type_str(), ErrConvDQName(lex->spname).ptr());
return 1;
default:
my_error(ER_SP_DROP_FAILED, MYF(0),
sph->type_str(), ErrConvDQName(lex->spname).ptr());
return 1;
}
#ifdef WITH_WSREP
wsrep_error_label:
return 1;
#endif
}
/**
@brief Compare requested privileges with the privileges acquired from the
@ -9150,8 +9200,8 @@ void sql_kill(THD *thd, longlong id, killed_state state, killed_type type)
}
static
void sql_kill_user(THD *thd, LEX_USER *user, killed_state state)
static void __attribute__ ((noinline))
sql_kill_user(THD *thd, LEX_USER *user, killed_state state)
{
uint error;
ha_rows rows;

View file

@ -2782,3 +2782,54 @@ bool wsrep_consistency_check(THD *thd)
{
return thd->wsrep_consistency_check == CONSISTENCY_CHECK_RUNNING;
}
/*
Commit an empty transaction.
If the transaction is real and the wsrep transaction is still active,
the transaction did not generate any rows or keys and is committed
as empty. Here the wsrep transaction is rolled back and after statement
step is performed to leave the wsrep transaction in the state as it
never existed.
This should not be an inline functions as it requires a lot of stack space
because of WSREP_DBUG() usage. It's also not a function that is
frequently called.
*/
void wsrep_commit_empty(THD* thd, bool all)
{
DBUG_ENTER("wsrep_commit_empty");
WSREP_DEBUG("wsrep_commit_empty(%llu)", thd->thread_id);
if (wsrep_is_real(thd, all) &&
wsrep_thd_is_local(thd) &&
thd->wsrep_trx().active() &&
thd->wsrep_trx().state() != wsrep::transaction::s_committed)
{
/* @todo CTAS with STATEMENT binlog format and empty result set
seems to be committing empty. Figure out why and try to fix
elsewhere. */
DBUG_ASSERT(!wsrep_has_changes(thd) ||
(thd->lex->sql_command == SQLCOM_CREATE_TABLE &&
!thd->is_current_stmt_binlog_format_row()));
bool have_error= wsrep_current_error(thd);
int ret= wsrep_before_rollback(thd, all) ||
wsrep_after_rollback(thd, all) ||
wsrep_after_statement(thd);
/* The committing transaction was empty but it held some locks and
got BF aborted. As there were no certified changes in the
data, we ignore the deadlock error and rely on error reporting
by storage engine/server. */
if (!ret && !have_error && wsrep_current_error(thd))
{
DBUG_ASSERT(wsrep_current_error(thd) == wsrep::e_deadlock_error);
thd->wsrep_cs().reset_error();
}
if (ret)
{
WSREP_DEBUG("wsrep_commit_empty failed: %d", wsrep_current_error(thd));
}
}
DBUG_VOID_RETURN;
}

View file

@ -26,6 +26,8 @@
class THD;
void wsrep_commit_empty(THD* thd, bool all);
/*
Return true if THD has active wsrep transaction.
*/
@ -466,50 +468,4 @@ wsrep_current_error_status(THD* thd)
return thd->wsrep_cs().current_error_status();
}
/*
Commit an empty transaction.
If the transaction is real and the wsrep transaction is still active,
the transaction did not generate any rows or keys and is committed
as empty. Here the wsrep transaction is rolled back and after statement
step is performed to leave the wsrep transaction in the state as it
never existed.
*/
static inline void wsrep_commit_empty(THD* thd, bool all)
{
DBUG_ENTER("wsrep_commit_empty");
WSREP_DEBUG("wsrep_commit_empty(%llu)", thd->thread_id);
if (wsrep_is_real(thd, all) &&
wsrep_thd_is_local(thd) &&
thd->wsrep_trx().active() &&
thd->wsrep_trx().state() != wsrep::transaction::s_committed)
{
/* @todo CTAS with STATEMENT binlog format and empty result set
seems to be committing empty. Figure out why and try to fix
elsewhere. */
DBUG_ASSERT(!wsrep_has_changes(thd) ||
(thd->lex->sql_command == SQLCOM_CREATE_TABLE &&
!thd->is_current_stmt_binlog_format_row()));
bool have_error= wsrep_current_error(thd);
int ret= wsrep_before_rollback(thd, all) ||
wsrep_after_rollback(thd, all) ||
wsrep_after_statement(thd);
/* The committing transaction was empty but it held some locks and
got BF aborted. As there were no certified changes in the
data, we ignore the deadlock error and rely on error reporting
by storage engine/server. */
if (!ret && !have_error && wsrep_current_error(thd))
{
DBUG_ASSERT(wsrep_current_error(thd) == wsrep::e_deadlock_error);
thd->wsrep_cs().reset_error();
}
if (ret)
{
WSREP_DEBUG("wsrep_commit_empty failed: %d", wsrep_current_error(thd));
}
}
DBUG_VOID_RETURN;
}
#endif /* WSREP_TRANS_OBSERVER */