1
0
mirror of https://github.com/MariaDB/server.git synced 2026-10-23 18:49:29 +03:00
Files
mariadb/sql/sql_parallel_workers.cc
2026-09-07 14:08:35 +12:00

636 lines
21 KiB
C++

/*
Copyright (c) 2026, MariaDB
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
the Free Software Foundation; version 2 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335
USA */
/**
@file
The parallel worker threads: creating the team, the THD each worker runs
under, relaying their diagnostics, and reaping them.
What a worker actually runs -- the gate, the per-worker copy of the plan and
the join over a worker's chunk -- is in sql_parallel_execution.cc. How a row
a worker has finished with reaches the manager is in
sql_parallel_transport.cc; this file builds that transport and owns the state
its two ends share about the team as a whole.
*/
#include "mariadb.h"
#include "mysqld_error.h"
#include "sql_class.h"
#include "sql_parallel_thread.h"
#include "sql_priv.h"
#include "unireg.h"
#include "sql_select.h"
#include "sql_parallel_workers.h"
#include "debug_sync.h"
#include "transaction.h"
#ifdef HAVE_PSI_INTERFACE
static PSI_mutex_key key_mutex_pwt_LOCK_data;
static PSI_mutex_info all_pwt_mutexes[]=
{
{ &key_mutex_pwt_LOCK_data, "pwt_manager::LOCK_data", 0},
};
static PSI_cond_key //key_COND_pwt_worker,
key_COND_pwt_data_avail;
static PSI_cond_info all_pwt_conds[]=
{
// { &key_COND_pwt_worker, "pwt_worker::COND_worker", 0},
{ &key_COND_pwt_data_avail, "pwt_manager::COND_data_avail", 0},
};
static PSI_memory_info all_pwt_memory[]=
{
{ &key_memory_pwt_queued_event, "pwt_queued_event", 0},
{ &key_memory_pwt_error_message, "pwt_error_message", 0},
{ &key_memory_pwt_db, "pwt_worker::db", 0},
{ &key_memory_pwt_batch_rows, "pwt_batch_sink::rows", 0},
};
#endif /* HAVE_PSI_INTERFACE */
void pwt_manager_base::report_fatal_error()
{
mysql_mutex_lock(&LOCK_data);
fatal_error= true;
/* This is to notify other workers too */
mysql_cond_signal(&COND_data_avail);
mysql_mutex_unlock(&LOCK_data);
}
void pwt_worker_base::on_fatal_error()
{
mgr2->report_fatal_error();
}
void pwt_manager_base::report_worker_final_state(killed_state state, bool err)
{
mysql_mutex_lock(&LOCK_data);
if (state && kill_signal == NOT_KILLED)
kill_signal= state;
if (err)
fatal_error= true;
active_workers--;
mysql_cond_signal(&COND_data_avail);
mysql_mutex_unlock(&LOCK_data);
}
int pwt_manager_base::locked__process_manager_wakeup()
{
/*
A worker exited because it was killed: propagate the kill to the
manager's own THD so the query aborts now with ER_QUERY_INTERRUPTED,
before any result is sent.
*/
if (killed_by_worker() != NOT_KILLED && !thd->killed)
{
mysql_mutex_assert_owner(&LOCK_data);
killed_state ks= killed_by_worker();
// TODO: the following was done when not holding LOCK_data. Does it matter?
mysql_mutex_lock(&thd->LOCK_thd_kill);
thd->killed= ks;
mysql_mutex_unlock(&thd->LOCK_thd_kill);
return 1;
}
if (fatal_error) // a worker failed
return 1;
if (!active_workers) // All workers have finished.
return -1;
return 0;
}
pwt_manager_base::pwt_manager_base() :
kill_signal(NOT_KILLED), fatal_error(false), active_workers(0)
{
mysql_mutex_init(key_mutex_pwt_LOCK_data, &LOCK_data, MY_MUTEX_INIT_FAST);
mysql_cond_init(key_COND_pwt_data_avail, &COND_data_avail, nullptr);
}
pwt_manager_base::~pwt_manager_base()
{
mysql_cond_destroy(&COND_data_avail);
mysql_mutex_destroy(&LOCK_data);
}
/*
This is run after the thread func has finished.
*/
void pwt_worker_base::thread_func_end()
{
mysql_mutex_lock(&thd->LOCK_thd_kill);
killed_state killed= thd->killed;
mysql_mutex_unlock(&thd->LOCK_thd_kill);
mgr2->report_worker_final_state(killed, err);
pwt_thread_with_stats::thread_func_end();
}
pwt_worker::pwt_worker(pwt_manager *manager_arg) :
pwt_worker_base(manager_arg), manager(manager_arg), sink(nullptr)
{}
/**
@brief
Entry point for our worker threads, arg supplied by manager details what
needs to be run
*/
void pwt_worker::thread_func()
{
execute_and_signal_manager();
/*
Close our private table copies while we are still attached to our THD
(current_thd == thd) and, crucially, before destroy_background_thd()
tears down the THD's transaction: the engine handle's close frees state
that references that transaction (InnoDB's prebuilt). The manager never
touches a started worker's tables, so no lock is needed here.
*/
snapshot_table_stats(); // while the tables are still open
close_tables();
thread_func_end();
}
/*
Snapshot the quick select's key intervals into the `ranges` array.
An empty result means "scan the whole table".
@return true on error (my_error() has been called)
*/
static bool parallel_build_key_ranges(JOIN_TAB *tab,
Dynamic_array<KEY_MULTI_RANGE> *ranges)
{
/*
Only a table scan contributes intervals. A JT_NEXT tab may still carry a
quick select that the plan discarded in favour of the index scan; its
->index need not be the index being scanned, so it must not be harvested.
*/
if (!tab->use_parallel_scan || tab->type == JT_NEXT ||
!tab->select || !tab->select->quick)
return false;
QUICK_RANGE_SELECT *quick= (QUICK_RANGE_SELECT*) tab->select->quick;
range_seq_t seq= quick_range_seq_init(quick, 0, 0);
KEY_MULTI_RANGE range;
while (!quick_range_seq_next(seq, &range))
{
if (ranges->append(range))
return true;
}
return false;
}
/**
@brief
Initialise our parallel worker threads, setting their own new THD objects.
Set up our mutexs for synchronization.
Called from the management thread for applicable queries at the top level.
@return
HA_ERR_UNSUPPORTED fall back to serial scan
0 success
1 failure
*/
int pwt_manager::init_parallel_workers(THD *thd, JOIN *join,
JOIN_TAB *scan_tab)
{
const uint n= thd->variables.parallel_worker_threads;
if (n == 0)
return HA_ERR_UNSUPPORTED;
Dynamic_array<KEY_MULTI_RANGE> ranges(PSI_INSTRUMENT_MEM, 0, 16);
if (parallel_build_key_ranges(scan_tab, &ranges))
return 1;
uint i= 0;
TABLE *table= scan_tab->table;
handler *file= table->file;
this->exec.join= join;
this->thd= thd;
this->exec.scan_tab= scan_tab;
/*
Which index the engine is being asked to divide. The intervals, when there
are any, are over the quick select's index; a bare index scan divides the
index the plan picked; and MAX_KEY means the whole table.
*/
const uint keynr= ranges.size() ? scan_tab->select->quick->index
: scan_tab->type == JT_NEXT ? scan_tab->index : MAX_KEY;
// Initialize engine's parallel scan coordinator, ranges copied if reqd.
int err= file->parallel_init_coordinator(n, keynr, ranges);
if (err == HA_ERR_UNSUPPORTED)
{
// Signal to fall back to the serial record reader
return err;
}
else if (err)
{
// Real error from the engine
file->print_error(err, MYF(0));
return err;
}
/*
Room for the whole team's pointers up front, so that appending the workers
below does not grow the array a piece at a time. Failing here is a plain
out-of-memory before anything has been set up: fall back to a serial scan.
*/
if (workers.reserve(n))
{
file->parallel_end_coordinator();
return HA_ERR_UNSUPPORTED;
}
/*
The non-const join tables in join order (exec.jointabs[0] == scan_tab). These
plus each worker's table copies form the manager->worker table map used to
rebind the cloned conditions/refs/select list. No semijoin bushes here (the
gate excludes them), so the tabs are simply join_tab[const_tables ..].
*/
exec.n_tables= join->table_count - join->const_tables;
if (!(exec.jointabs= thd->alloc<JOIN_TAB*>(exec.n_tables)) ||
!(exec.tables= thd->alloc<TABLE*>(exec.n_tables)))
{
my_error(ER_OUTOFMEMORY, MYF(0), (int) (exec.n_tables * sizeof(void*)));
goto cleanup_workers;
}
for (uint t= 0; t < exec.n_tables; t++)
{
exec.jointabs[t]= &join->join_tab[join->const_tables + t];
exec.tables[t]= exec.jointabs[t]->table;
}
/*
Work out the row shape the workers and this thread will agree on, and build
the transport that carries rows in it. Both are described in
sql_parallel_transport.h; from here on nothing in this file knows how a row
travels, only that a worker has a sink to hand one to and we have a source
to take the next one from.
*/
if (layout.build(thd, join, exec.tables, exec.n_tables,
pwt_preagg_group(join)) ||
setup_transport(thd, n))
goto cleanup_workers;
reaped= false;
for (i= 0; i < n; i++)
{
/*
One worker object at a time, its pointer appended to the team as soon as
it exists: from here on the teardown below finds exactly the workers that
were allocated, and each of those says for itself how far it got.
*/
pwt_worker *worker= new pwt_worker(this);
if (!worker || workers.append(worker))
{
/*
Nothing has been done to this worker yet, and it never made it into
the team, so it is ours to delete; the teardown below will not see it.
*/
delete worker;
my_error(ER_OUTOFMEMORY, MYF(0), (int) sizeof(pwt_worker));
goto cleanup_workers;
}
if (worker->init_worker_thd(thd, /*worker_nr=*/i+1))
goto cleanup_workers;
worker->exec.handler_ctx= file->parallel_get_worker_context(i);
DBUG_ASSERT(worker->exec.handler_ctx);
/*
Give this worker its own copy of every non-const join table, opened from
the shared TABLE_SHARE (open_worker_tables); the driving table is
exec.tables[0] / exec.scan_table.
*/
if (open_worker_tables(thd, worker))
goto cleanup_workers;
/*
Set up how this worker joins the non-driving tables (access method,
worker-bound ref clone, condition), its result container, and private
clones of the WHERE condition + select list with field references rebound
to this worker's table copies. At run time the worker scans the driving
chunk, joins the inner tables, projects exec.proj into
its result container and ships that record image.
*/
if (setup_worker_join(thd, worker) ||
setup_worker_jointabs(thd, worker) ||
layout.make_container(thd, &worker->exec.result) ||
setup_worker_preagg(thd, worker) ||
!(worker->sink= source->make_sink(thd, i, &worker->exec.result)) ||
clone_worker_exprs(thd, worker))
{
my_error(ER_INTERNAL_ERROR, MYF(0),
"init_parallel_workers: failed to set up worker execution");
goto cleanup_workers;
}
/*
Fail one worker as if its thread could not be created, so a test can reach
cleanup_workers part-way through building the team: that is the only path
that aborts a worker, and the only one that tears a partly built team
down. Injected here rather than after create_thread() so the failing
worker is one that never started a thread, which is what lets the teardown
close its tables and destroy its THD itself.
Which worker decides which parts of the teardown run. Failing the last
leaves every earlier worker running, so they are aborted and joined;
failing the first means the later ones were never allocated, so the
teardown walks a team of one.
*/
bool inject_create_failure= false;
DBUG_EXECUTE_IF("pwt_init_fail_last_worker",
inject_create_failure= (i + 1 == n););
DBUG_EXECUTE_IF("pwt_init_fail_first_worker",
inject_create_failure= (i == 0););
if (inject_create_failure || worker->create_thread())
{
my_error(ER_INTERNAL_ERROR, MYF(0),
"init_parallel_workers: failed to create worker thread");
goto cleanup_workers;
}
}
return 0;
cleanup_workers:
/*
A worker spawned before the failure may be blocked inside the transport
waiting for the manager to take its rows. Release them so the joins below
can complete.
*/
mysql_mutex_lock(&LOCK_data);
request_stop();
mysql_mutex_unlock(&LOCK_data);
/*
Every worker in the team, not just the ones we know we started: each one
knows what its own startup reached, so the ones with a thread are aborted
and joined and the one that failed part-way gives back only what it did
take. That is what makes this one label instead of one per stage of the
build.
*/
for (uint j= 0; j < nworkers(); j++)
workers[j]->abort_worker();
/*
Release the transport's hold on the containers before freeing them: a sink
may still be naming a worker THD that is now gone.
*/
destroy_transport();
process_pending_warnings(true);
file->parallel_end_coordinator();
return 1; // reached only on failure
}
#ifdef HAVE_PSI_INTERFACE
void pwt_init_psi_keys(void)
{
const char *category= "sql";
int count;
count= array_elements(all_pwt_mutexes);
mysql_mutex_register(category, all_pwt_mutexes, count);
count= array_elements(all_pwt_conds);
mysql_cond_register(category, all_pwt_conds, count);
count= array_elements(all_pwt_memory);
mysql_memory_register(category, all_pwt_memory, count);
pwt_transport_init_psi_keys();
}
#endif
/**
@brief
Stop the producers and pthread_join them.
The workers read this join's source table (their private handler), so they
must be reaped before JOIN::join_free()->cleanup() frees that table --
otherwise a worker that has not yet observed the stop request dereferences a
freed table->file. That is why this is called from join_free(), ahead of the
table teardown, and again (idempotently, guarded by 'reaped') from finalize.
On a normal completion the workers have already finished; on an early-out
(LIMIT) or an abort (KILL/error) we ask them to stop here. A worker
re-checks 'stop' at each batch hand-off, so it exits within at most one
batch -- without raising ER_QUERY_INTERRUPTED, which matters for a normal
early-out.
*/
void pwt_manager::quiesce_workers()
{
DBUG_ENTER("pwt_manager::quiesce_workers");
if (!nworkers() || reaped)
DBUG_VOID_RETURN;
/*
The consumer may have stopped part-way through a producer's rows: drop that
position before the producers are reaped, so nothing is left pointing into
them.
*/
if (source)
source->release_position();
mysql_mutex_lock(&LOCK_data);
request_stop();
mysql_mutex_unlock(&LOCK_data);
for (uint i= 0; i < nworkers(); i++)
workers[i]->reap_worker();
/*
The work the workers did was this session's work, so its statistics are the
session's too. Each worker left them in its pwt_worker before its THD was
destroyed, and every worker has now been joined, so this thread is the only
one touching either side and no locking is needed.
*/
for (uint i= 0; i < nworkers(); i++)
workers[i]->save_worker_stats(thd);
/*
Give ANALYZE what the workers did. The manager never runs the driving table's
read loop, so the JOIN_TAB trackers the optimizer left for ANALYZE to read
stay at zero and the report says the table was never touched. The trackers
and the handlers are the manager's, every worker has been joined, so this
thread is the only one touching either side.
Keep the split as well as the sum. Summing tells ANALYZE how much was read,
which is what a serial plan reports too; the per-worker figures tell it how
evenly the engine's chunks divided that between the workers, which only a
parallel plan has to answer for. Allocated on the statement's mem_root,
which outlives the ANALYZE output the tracker is read for.
*/
for (uint t= 0; t < exec.n_tables; t++)
{
Table_access_tracker *tr= exec.jointabs[t]->tracker;
if (!tr)
continue;
if ((tr->r_rows_per_worker= thd->calloc<ha_rows>(nworkers())))
tr->n_workers= nworkers();
}
for (uint i= 0; i < nworkers(); i++)
for (uint t= 0; t < exec.n_tables; t++)
{
if (Table_access_tracker *tr= exec.jointabs[t]->tracker)
{
/*
Not the driving table's: sub_select() counts one scan of it per worker
and the report wants one between them, added below.
*/
if (t)
tr->r_scans+= workers[i]->exec.tab_stats[t].r_scans;
tr->r_rows+= workers[i]->exec.tab_stats[t].r_rows;
tr->r_rows_after_where+=
workers[i]->exec.tab_stats[t].r_rows_after_where;
if (tr->r_rows_per_worker)
tr->r_rows_per_worker[i]= workers[i]->exec.tab_stats[t].r_rows;
}
if (ha_handler_stats *hs= exec.tables[t]->file->handler_stats)
hs->add(&workers[i]->exec.tab_hstats[t]);
}
/*
The chunks are one scan of the driving table between them, so report one,
which is what the serial plan reports and what makes r_rows per scan comparable.
*/
if (exec.jointabs[0]->tracker)
exec.jointabs[0]->tracker->r_scans++;
reaped= true;
DBUG_VOID_RETURN;
}
/*
@brief
Give back everything setup_transport() and the team's shared state took:
both ends of the transport, the row containers, LOCK_data/COND_data_avail
and the pwt_worker objects.
@description
The one teardown tail, reached both when the team failed to build and when
the query finished with it. Every worker has been joined by the time we get
here -- reaped on the finishing path, aborted on the failing one -- so both
ends of the transport are idle and nothing else can be waiting on the mutex
or the condition. A half-built worker may have no sink at all, which is why
each one is checked; free_containers() reads workers[], so it has to come
before the objects go.
*/
void pwt_manager::destroy_transport()
{
for (uint i= 0; i < nworkers(); i++)
if (workers[i]->sink)
workers[i]->sink->cleanup();
if (source)
source->cleanup();
free_containers(thd); // the transport has let go of them
destroy_workers();
}
/**
@brief
Reap the workers (if not already) and tear the channel down.
Called from JOIN::exec() once exec_inner() has finished. Worker errors and
warnings collected by PWT_error_handler are surfaced here, after the join's
own result has been produced.
*/
void pwt_manager::finalize_parallel_workers(THD *thd, JOIN *join)
{
DBUG_ENTER("pwt_manager::finalize_parallel_workers");
if (!nworkers())
DBUG_VOID_RETURN;
quiesce_workers(); // stop + join (no-op if already reaped)
exec.scan_tab->table->file->parallel_end_coordinator();
process_pending_warnings(false);
destroy_transport();
DBUG_VOID_RETURN;
}
/*
@brief
Delete the team's pwt_worker objects and empty workers[], so that the
manager reads as having no team again.
@note
Only the objects go: the array itself keeps its buffer until the manager
is destroyed. Every caller has already reaped the workers, so nothing is
still holding a pointer to one.
*/
void pwt_manager::destroy_workers()
{
for (uint i= 0; i < nworkers(); i++)
delete workers[i];
workers.clear();
}
/*
@brief
Build the result-row transport: the manager's consuming end of it.
@description
One place makes the choice (pwt_create_transport), and the source it
returns is what makes the matching sinks, so a worker is never paired with
an end of a different kind. Called once the row layout is known; each
worker's sink is made later, with the rest of that worker's setup, because
a sink may need the container that setup builds.
@return true on error (my_error() has been called).
*/
bool pwt_manager::setup_transport(THD *thd, uint n_workers)
{
return (source= pwt_create_transport(thd, this, n_workers,
layout.reclength)) == nullptr;
}
/*
@brief
Ask the producers to stop, and wake any that are waiting on us.
The request is the team's (a satisfied LIMIT, a KILL, an error, a team that
failed to start); the waking is the transport's, and is a no-op for a
transport in which a producer never waits for the consumer. Caller holds
LOCK_data.
*/
void pwt_manager::request_stop()
{
mysql_mutex_assert_owner(&LOCK_data);
workers_must_stop= true;
if (source)
source->wake_producers();
}