mirror of
https://github.com/MariaDB/server.git
synced 2026-10-23 18:49:29 +03:00
SELECT COUNT(*), SUM(b) FROM t ran serially. The workers could pre-aggregate a GROUP BY, but aggregation over the whole table was refused, because the shape the grouped path merges through is not there: with no group the server materialises nothing, and aggregates in end_send_group() straight off the last join tab -- no aggregation table, no group key, nothing for a partial to be merged into. It turns out neither end needs new machinery, and this commit is mostly the finding of that. The manager already merges these. end_send_group() reaches Item_sum::add(), and add() honours a value left by direct_add() for every aggregate the gate accepts -- COUNT adds a count instead of incrementing, AVG takes a partial sum together with the number of rows behind it. So the drain primes the aggregates and hands the row to the plan's own terminal here exactly as it does through end_update() for a GROUP BY, and the drain needed no case of its own. The workers accumulate into the grouped machinery with a single group in it. The layout gives this shape a group of one constant column, and from there it is the grouped path: a worker keeps one partial in a keyed container, merges into it through end_update(), and ships one row. Beyond costing a column per shipped row -- of which there is one per worker -- that is also what keeps Item_sum_avg::create_tmp_field() packing the count beside the sum, which it does only for a container that has a group. An average built any other way would have arrived without its count. So what is actually new is the gate. The aggregate tests -- the five that can be merged, no DISTINCT, one argument, an argument a worker can evaluate -- are the same for both shapes and move out to pwt_aggregates_can_be_merged(). What is left is the shape question, and which shape to ask about is the query's own grouping rather than the plan's: a query that names a GROUP BY stays the grouped question whatever the optimizer then did with it, so a plan with no group key to merge through is a refusal of that query and not an invitation to read it as aggregating over everything, which would answer something else. Only a query that never grouped reaches the second question. Refused there: a select-list item reading a column outside an aggregate. Its value is whichever row the terminal happened to hold, and a worker holds a different one. An empty result needed nothing. No rows means no groups means no partials shipped, and end_send_group() still sends its one row at end of records: COUNT(*) is 0, SUM is NULL, one row and not one per worker. The new test checks each of those rather than assuming them. main.parallel_query_aggregate_whole is that test: serial against parallel for COUNT, COUNT(col), SUM, MIN, MAX and AVG over integer, decimal and double columns; the empty case; that exactly one row is sent; and that a DISTINCT aggregate, STD, GROUP_CONCAT and a bare column outside an aggregate are still refused and run serially. main.parallel_query_aggregate gains the fall-back case, where the container turns out to have no group index: the constant group reaches the same fall-back, ships plain rows, and the manager aggregates them a row at a time. Known and accepted: adding doubles is not associative, so SUM and AVG over DOUBLE or FLOAT can differ from the serial answer, and between runs, in the last significant bit -- which worker summed which chunk is scheduling. Decimal and integer aggregates are exact and unaffected. The test rounds well clear of the last bit rather than leaving floats uncovered. Under a forced-worker sweep of the main suite (--parallel-worker-threads=4) three more tests differ than before, all of them EXPLAIN text where a query of this shape now says ALL_parallel or range_parallel: main.costs, main.func_group_innodb and main.innodb_full_scan. No value changes in any of them. This commit was prepared with Claude Code (Opus 5), which established that Item_sum::add() already honours a directly added partial -- the fact the whole change rests on -- and found that a container without a group would have dropped an average's count.
740 lines
25 KiB
C++
740 lines
25 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;
|
|
ORDER *preagg_group= nullptr;
|
|
pwt_preagg_kind preagg= PWT_PREAGG_NONE;
|
|
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 ..].
|
|
*/
|
|
/*
|
|
Scan-only: the workers read the driving table and nothing else, so that is
|
|
the only table they need a copy of and the only one whose columns travel.
|
|
*/
|
|
exec.n_tables= scan_only ? 1 : 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;
|
|
}
|
|
|
|
/*
|
|
No pre-aggregation and no sort stage in scan-only: this thread runs the
|
|
plan's own terminals, so both belong to it exactly as they would serially.
|
|
*/
|
|
preagg= scan_only ? PWT_PREAGG_NONE : pwt_preagg_shape(join, &preagg_group);
|
|
|
|
/*
|
|
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, preagg, preagg_group,
|
|
scan_only ? nullptr : pwt_manager_sort_order(join)) ||
|
|
setup_transport(thd, n) ||
|
|
(layout.plan_sorts && setup_sort_stage(thd)))
|
|
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.
|
|
*/
|
|
/*
|
|
Scan-only skips all of it but the container and the sink: no JOIN, no
|
|
JOIN_TABs, no grouping table and no cloned expressions. What it needs
|
|
instead is one Item_field per shipped column, over its own table copy.
|
|
|
|
The shipping container has to have the shape both ends of the transport
|
|
agree on, which for a grouped plan means the shape create_tmp_table()
|
|
gives a table that has a group key: Item_sum_avg::create_tmp_field() is
|
|
the one aggregate whose field differs between the two, packing its count
|
|
beside its sum only for the keyed form. The manager reads the container
|
|
back as its own recv container, which is keyed, so a container built
|
|
unkeyed here would be read short, one AVG's worth of bytes at a time.
|
|
The key costs nothing: a worker ships one row per group, so there is
|
|
nothing for it to collapse.
|
|
|
|
A worker that pre-aggregates does not get its container from here at all.
|
|
setup_worker_preagg() builds it, because the same table is the one the
|
|
aggregates accumulate into, and that has to be described by the worker's
|
|
own items rather than the manager's.
|
|
*/
|
|
ORDER *ship_group= nullptr;
|
|
if (layout.grouped && !(ship_group= layout.clone_group_defn(thd)))
|
|
{
|
|
my_error(ER_OUTOFMEMORY, MYF(0), (int) sizeof(ORDER));
|
|
goto cleanup_workers;
|
|
}
|
|
|
|
if (scan_only)
|
|
{
|
|
if (layout.make_container(thd, &worker->exec.result, ship_group) ||
|
|
!(worker->sink= source->make_sink(thd, i, &worker->exec.result)) ||
|
|
setup_scan_only_proj(thd, worker))
|
|
{
|
|
my_error(ER_INTERNAL_ERROR, MYF(0),
|
|
"init_parallel_workers: failed to set up worker scan");
|
|
goto cleanup_workers;
|
|
}
|
|
}
|
|
else if (setup_worker_join(thd, worker) ||
|
|
setup_worker_jointabs(thd, worker) ||
|
|
(!layout.grouped &&
|
|
layout.make_container(thd, &worker->exec.result, ship_group)) ||
|
|
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;
|
|
/*
|
|
Per-execution, all of it: a plan that runs more than once -- a correlated
|
|
subquery, a routine loop -- reports its last execution, not a running
|
|
total. The serial path accumulates across executions instead, but the
|
|
per-worker split and the chunk counts below are only meaningful for one
|
|
execution, and totals sitting beside them would answer a different
|
|
question about a different number of rows. So the counts this block sums
|
|
into start from zero every time, and everything ANALYZE prints for a
|
|
parallel scan describes the same, latest, run.
|
|
*/
|
|
tr->r_scans= 0;
|
|
tr->r_rows= 0;
|
|
tr->r_rows_after_where= 0;
|
|
tr->r_chunks_created= 0;
|
|
tr->r_chunks_resplit= 0;
|
|
if ((tr->r_rows_per_worker= thd->calloc<ha_rows>(nworkers())))
|
|
tr->n_workers= nworkers();
|
|
tr->r_engine_time_per_worker= thd->calloc<ulonglong>(nworkers());
|
|
tr->r_pages_read_time_per_worker= thd->calloc<ulonglong>(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;
|
|
}
|
|
/*
|
|
The worker's own engine figures, kept per worker as well as summed:
|
|
the sum says what the scan cost, the split says whether it cost the
|
|
workers the same, which is the question a parallel plan has to answer
|
|
and a serial one never asks.
|
|
*/
|
|
if (Table_access_tracker *tr= exec.jointabs[t]->tracker)
|
|
{
|
|
const ha_handler_stats *ws= &workers[i]->exec.tab_hstats[t];
|
|
if (tr->r_engine_time_per_worker)
|
|
tr->r_engine_time_per_worker[i]= ws->engine_time;
|
|
if (tr->r_pages_read_time_per_worker)
|
|
tr->r_pages_read_time_per_worker[i]= ws->pages_read_time;
|
|
}
|
|
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++;
|
|
// How the engine cut that one scan table up.
|
|
exec.scan_tab->table->file->parallel_get_chunk_stats(
|
|
&exec.jointabs[0]->tracker->r_chunks_created,
|
|
&exec.jointabs[0]->tracker->r_chunks_resplit);
|
|
}
|
|
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");
|
|
/*
|
|
Give the plan its own reader back before anything else, and unconditionally:
|
|
the JOIN_TAB is the plan's and outlives this manager, so a displaced reader
|
|
has to be restored even on a path that never built a team.
|
|
*/
|
|
if (scan_only_tab)
|
|
{
|
|
scan_only_tab->read_first_record= saved_read_first_record;
|
|
if (saved_select_cond)
|
|
{
|
|
scan_only_tab->set_select_cond(saved_select_cond, __LINE__);
|
|
saved_select_cond= nullptr;
|
|
}
|
|
scan_only_tab= nullptr;
|
|
}
|
|
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();
|
|
}
|