1
0
mirror of https://github.com/MariaDB/server.git synced 2026-10-23 18:49:29 +03:00
Files
mariadb/sql/sql_parallel_thread.cc
Rex Johnston 0f0d7b24c1 PQ tidy up message queue processing, add in missing tests, fix...
in pwt_worker_base::init_worker_thd()
2026-09-07 14:08:34 +12:00

451 lines
15 KiB
C++

#include "sql_parallel_thread.h"
#include "mariadb.h"
#include "mysqld_error.h"
#include "sql_class.h"
#include "sql_priv.h"
#include "unireg.h"
#include "sql_select.h"
#include "sql_parallel_workers.h"
#include "debug_sync.h"
#include "transaction.h"
extern MYSQL_THD create_background_thd();
extern void *thd_attach_thd(MYSQL_THD thd);
extern void thd_detach_thd(void *save);
extern void destroy_background_thd(MYSQL_THD thd);
/**
@brief
Save error condition into pwt_queued_event object.
@return
true an error occurred
false error or warning is provided in *event
*/
static
bool save_error_to_queued_event(THD *thd, pwt_queued_event **event, uint error,
Sql_condition::enum_warning_level level,
const char *msg)
{
DBUG_EXECUTE_IF("pwt_error_to_queue_oom",
{ *event= nullptr; return true; });
*event= (pwt_queued_event*) my_malloc(key_memory_pwt_queued_event,
sizeof(pwt_queued_event),
MYF(0));
if (!*event)
return true;
(*event)->error= (pwt_error_message*) my_malloc(key_memory_pwt_error_message,
sizeof(pwt_error_message),
MYF(0));
if (!(*event)->error)
{
my_free(*event);
*event= nullptr;
return true;
}
(*event)->error->level= level;
if (level == Sql_condition::enum_warning_level::WARN_LEVEL_ERROR)
(*event)->error->worker_errno= thd->killed_errno();
else
(*event)->error->worker_errno= 0;
(*event)->error->code= error;
(*event)->error->message= (char *) my_malloc(key_memory_pwt_error_message,
strlen(msg)+1,
MYF(0));
if (!(*event)->error->message)
{
my_free((*event)->error);
my_free(*event);
*event= nullptr;
return true;
}
strmake((*event)->error->message, msg, strlen(msg));
return false;
}
/*
Capture errors and warnings and redirect them to worker->manager.
*/
class PWT_error_handler : public Internal_error_handler
{
pwt_manager_base *manager;
pwt_worker_base *worker;
public:
PWT_error_handler(pwt_manager_base *manager_arg, pwt_worker_base *worker_arg) :
manager(manager_arg), worker(worker_arg) {}
bool handle_condition(THD *thd,
uint sql_errno,
const char* sql_state,
Sql_condition::enum_warning_level *level,
const char* msg,
Sql_condition ** cond_hdl) override
{
/*
A genuine error (not a warning) raised while the worker runs the query
-- e.g. a WHERE/projection/join evaluation error -- must abort the whole
query. Trip fatal_error so the worker stops producing and the manager
aborts instead of sending a truncated result. The error text is still
relayed to the manager below and surfaced from finalize.
Exclude a killed worker's own ER_QUERY_INTERRUPTED: a KILL is reported
separately via kill_signal (which also carries the kill type, so a
KILL vs KILL QUERY of a worker maps to dropping the manager connection
vs just its query). Tripping fatal_error here would make the manager
take the generic-error path and lose that distinction.
*/
if (*level == Sql_condition::WARN_LEVEL_ERROR && !thd->killed)
{
//TODO: and we don't save which error we've got?
// Do we save it right below? If yes, why don't we
// enqueue it first?
// An error appears to have multiple elements, so we contain it, then
// queue it.
worker->on_fatal_error();
}
pwt_queued_event *event;
if (save_error_to_queued_event(thd, &event, sql_errno, *level, msg))
{
/*
Couldn't allocate the queued event. The worker THD's diagnostics
area is discarded when the worker exits, so flag the manager so it
can surface a single ER_OUTOFMEMORY warning to the user instead of
letting this condition vanish.
*/
manager->notify_message_dropped();
return true;
}
manager->record_event(event);
return true; // no further processing in worker thread
}
};
void pwt_manager_base::notify_message_dropped()
{
mysql_mutex_lock(&LOCK_pwt_manager);
messages_dropped= true;
mysql_mutex_unlock(&LOCK_pwt_manager);
}
#ifdef HAVE_PSI_INTERFACE
static PSI_thread_key key_thread_pwt;
static PSI_thread_info all_pwt_threads[]=
{
{ &key_thread_pwt, WORKER_NAME, PSI_FLAG_GLOBAL},
};
static PSI_mutex_key key_mutex_pwt_LOCK_worker,
key_mutex_pwt_LOCK_manager;
static PSI_mutex_info all_pwt_mutexes[]=
{
{ &key_mutex_pwt_LOCK_manager, "pwt_manager::LOCK_pwt_manager", 0},
{ &key_mutex_pwt_LOCK_worker, "pwt_worker::LOCK_worker", 0},
};
void pwt_threads_init_psi_keys(void)
{
const char *category= "sql";
PSI_server->register_thread(category, all_pwt_threads,
array_elements(all_pwt_threads));
mysql_mutex_register(category, all_pwt_mutexes,
array_elements(all_pwt_mutexes));
}
#endif
void *pwt_worker_base_thread_func(void *arg)
{
DBUG_ENTER("parallel_worker_thread_func");
pwt_worker_base *worker= (pwt_worker_base*) arg;
worker->init_and_run_thread_func();
return nullptr;
}
bool pwt_worker_base::create_thread()
{
if (mysql_thread_create(key_thread_pwt, &pthread, /*pthread_attr_t*/ nullptr,
pwt_worker_base_thread_func, this))
return 1;
return 0;
}
void pwt_worker_base::init_and_run_thread_func()
{
PWT_error_handler error_handler(manager_base, this);
/*
Set current_thd and thread local storage (my_thread_var) for our new THD
to ensure they have their own local objects/errors/warnings etc
*/
void *save= thd_attach_thd(thd);
/*
create_background_thd()'s THD(0) never ran the connection-setup path that
allocates debug_sync_control, so DEBUG_SYNC actions on this worker (e.g. the
pwt_worker_pause_before_signal sync point used by the tests) would assert on
a NULL control block. Initialise it here, on the worker thread, so the
MY_THREAD_SPECIFIC allocation is charged to this THD and freed symmetrically
by ~THD (debug_sync_end_thread) when destroy_background_thd() runs below.
No-op in non-DEBUG_SYNC builds and when debug_sync is inactive.
*/
#ifdef ENABLED_DEBUG_SYNC
if (!thd->debug_sync_control)
debug_sync_init_thread(thd);
#endif
my_thread_set_name(thd->connection_name.str);
THD_STAGE_INFO(thd, stage_sending_data);
thd->push_internal_handler(&error_handler);
DBUG_EXECUTE_IF("pwt_error_to_queue_oom",
{
push_warning(current_thd, Sql_condition::WARN_LEVEL_WARN, ER_UNKNOWN_ERROR,
"This is an example warning to show we can push a "
"warning from a worker thread to its manager ");
});
#ifdef ENABLED_DEBUG_SYNC
/*
we can't sync on the managers or our THD, spin the whole thing about
and use the global signal pool, NO_CLEAR_EVENT is needed because we have
multiple workers and the wrong one will likely consume the signal.
*/
DBUG_EXECUTE_IF("pwt_worker_pause_before_signal",
DBUG_ASSERT(!debug_sync_set_action(thd, STRING_WITH_LEN(
"now SIGNAL pwt_worker_paused WAIT_FOR pwt_worker_continue NO_CLEAR_EVENT"
))););
// TODO: what is this for? Test coverage for the above?
// yes
mysql_mutex_lock(&thd->LOCK_thd_kill);
if (thd->killed)
{
my_error(ER_QUERY_INTERRUPTED, MYF(0));
}
mysql_mutex_unlock(&thd->LOCK_thd_kill);
#endif
thread_func();
// manager needs to see this as atomic
mysql_mutex_lock(&LOCK_worker);
/*
LOCK_thd_kill is the canonical guard for thd->killed; a user-issued
KILL on this worker's thread_id goes through THD::awake() which holds
LOCK_thd_kill but not LOCK_worker, so we must nest both to get a
race-free snapshot for the manager.
*/
mysql_mutex_lock(&thd->LOCK_thd_kill);
mysql_mutex_unlock(&thd->LOCK_thd_kill);
thd->pop_internal_handler(); // maybe not needed
/*
Null it while we still hold LOCK_worker, and tear the THD down from a local
copy afterwards. abort_worker() reads this member under the same lock and
calls awake() on it, so it has to see either a THD that is still alive --
the lock keeps us out of the teardown below until it has -- or nullptr.
Leaving the member set would let it awake() a THD this thread has already
destroyed.
*/
THD *worker_thd= thd;
thd= nullptr;
mysql_mutex_unlock(&LOCK_worker);
// This is similar to what destroy_worker_thd() does:
server_threads.erase(worker_thd);
/*
executing thd_detach_thd sets my_thread_var to null, stopping our ability
use the normal mutex mechanisms, so we operate this outside the locked
region on a copy of our THD pointer
*/
thd_detach_thd(save);
destroy_background_thd(worker_thd);
}
/**
@brief
Empty warning/error queue from workers into our manager thread
@description
Surface errors/warnings the workers queued via PWT_error_handler. A worker
error that mattered to the result has already aborted the join during
execution (fatal_error or a propagated kill), so thd is already in error by
the time we get here; raising another error would trip the "can't overwrite
status" assertion in the diagnostics area. So only raise a queued ERROR
when thd is not already in error -- otherwise keep it as a warning. Plain
warnings are always safe to add.
@parameters
skip_interrupted whether or not to ignore a deluge of interrupted
errors
*/
void pwt_manager_base::process_pending_warnings(bool skip_interrupted)
{
bool surface_drop;
mysql_mutex_lock(&LOCK_pwt_manager);
surface_drop= messages_dropped;
messages_dropped= false;
pwt_queued_event *event;
while ((event= parallel_messages.get()))
{
if (pwt_error_message *err= event->error)
{
// if called during initialization, skip processing
if (!skip_interrupted || err->code != ER_QUERY_INTERRUPTED)
{
if (err->level == Sql_condition::enum_warning_level::WARN_LEVEL_ERROR &&
!thd->is_error())
my_message_sql(err->code, err->message, MYF(0));
else
push_warning(thd, Sql_condition::WARN_LEVEL_WARN, err->code,
err->message);
}
my_free(err->message);
my_free(err);
}
my_free(event);
}
mysql_mutex_unlock(&LOCK_pwt_manager);
if (surface_drop)
{
push_warning_printf(thd, Sql_condition::WARN_LEVEL_WARN, ER_OUTOFMEMORY,
"Parallel worker diagnostics were dropped due to "
"memory allocation failure");
}
}
bool pwt_worker_base::init_worker_thd(pwt_manager_base *manager_arg, THD *parent_thd,
int worker_nr)
{
manager_base= manager_arg;
/* First, do things that may fail early. */
LEX_CSTRING new_db;
if (parent_thd->db.str)
{
// Explicit call in ~THD/THD::free_connection()/my_free, so we do this
if (!(new_db.str= my_strndup(key_memory_pwt_db, parent_thd->db.str,
parent_thd->db.length,
MYF(MY_WME | ME_FATAL))))
return true; // OOM
new_db.length= parent_thd->db.length;
}
else
{
new_db.str= nullptr;
new_db.length= 0;
}
thd= create_background_thd();
if (!thd)
{
my_free(const_cast<char*>(new_db.str));
my_error(ER_INTERNAL_ERROR, MYF(0),
"init_parallel_workers: failed to create worker thread THD");
return true; // Failed
}
thd->db= new_db;
mysql_mutex_init(key_mutex_pwt_LOCK_worker, &LOCK_worker,
MY_MUTEX_INIT_FAST);
//mysql_cond_init(key_COND_pwt_worker, &COND_worker, nullptr);
thd->system_thread= SYSTEM_THREAD_GENERIC;
size_t len= my_snprintf(info.conn_name, MAX_THREAD_NAME,
WORKER_NAME);
thd->connection_name.str= info.conn_name;
thd->connection_name.length= len;
thd->security_ctx= parent_thd->security_ctx;
thd->set_command(parent_thd->get_command());
thd->start_utime= parent_thd->start_utime;
/*
A worker evaluates this session's expressions, so it is running this
session's query and has to say so. Items that hold a value for the
duration of one statement keep it against the query id they computed it
under: Item_func_curtime, and the NOW/SYSDATE family with it, recomputes
only when thd->query_id has moved past its own last_query_id. A
background THD starts at query id 0, which is also the value that field
is born with, so a copy that had not been evaluated before it was cloned
looked up to date to the worker and handed out its uninitialized
MYSQL_TIME -- what the is_valid_value_slow() assertion in Time::Time()
catches.
*/
thd->query_id= parent_thd->query_id;
thd->thread_id= next_thread_id();
my_snprintf(info.process_list, sizeof(info.process_list),
WORKER_NAME " %u " CONNECTION_NAME_THREAD " %llu",
worker_nr, parent_thd->thread_id);
thd->query_string= CSET_STRING(info.process_list,
strlen(info.process_list),
thd->query_charset());
// TODO: it is OK that we insert before starting the OS thread, right?
server_threads.insert(thd); // +information_schema.processlist
return false; // Ok
}
void pwt_worker_base::destroy_worker_thd()
{
server_threads.erase(thd);
/*
destroy_background_thd() requires current_thd to be NULL because it
re-attaches the background THD to this thread's TLS. We are running on
the user's query thread (current_thd == manager thd), so save/null/
restore around the call. Mirrors the create_background_thd() pattern.
*/
THD *save_thd= current_thd;
set_current_thd(nullptr);
destroy_background_thd(thd);
set_current_thd(save_thd);
}
/**
@brief
Abort this worker, called as part of an error condition
The worker may already be tearing itself down: parallel_worker_thread_func
nulls worker->thd and destroys the THD under LOCK_worker. Take that lock
and only awake() if the worker hasn't yet entered its exit section; if
it has, the worker is on its way out and pthread_join will reap it.
*/
void pwt_worker_base::abort_worker()
{
mysql_mutex_lock(&LOCK_worker);
if (thd)
thd->awake(ABORT_QUERY);
mysql_mutex_unlock(&LOCK_worker);
join_worker_thread();
cleanup_worker();
}
void pwt_worker_base::cleanup_worker()
{
mysql_mutex_destroy(&LOCK_worker);
// mysql_cond_destroy(&COND_worker);
}
pwt_manager_base::pwt_manager_base() :
messages_dropped(false)
{
mysql_mutex_init(key_mutex_pwt_LOCK_manager, &LOCK_pwt_manager,
MY_MUTEX_INIT_SLOW);
}
pwt_manager_base::~pwt_manager_base()
{
mysql_mutex_destroy(&LOCK_pwt_manager);
}