1
0
mirror of https://github.com/MariaDB/server.git synced 2026-10-23 18:49:29 +03:00

MDEV-39492 PQ framework for manager and worker threads

Introduces parallel_worker_threads variable to control the number
of worker threads created by a parallel execution query.

2 new files, sql_parallel_workers.h sql_parallel_workers.cc which
contain structures for the creation, management and deletion of
parallel worker threads (pwt_ in the name).  Main management
class created in the stack in JOIN::exec, implemented for the
top level select.

Threads are registed in server_threads, so are visible in
information_schema.processlist and the show processlist command.

We check that a kill query on a parallel worker is passed onto it's
manager and the query is properly aborted, and that a kill connection
is handled properly in parallel_worker.test.
This commit is contained in:
Rex Johnston
2026-05-06 11:18:47 +12:00
parent 66eefaeb95
commit 10c5f48692
26 changed files with 1926 additions and 24 deletions

View File

@@ -624,6 +624,20 @@ typedef uint64 my_thread_id;
*/
#define MY_THREAD_ID_MAX UINT32_MAX
#ifdef _WIN32
#define MAX_THREAD_NAME 256
#elif defined(__linux__)
#define MAX_THREAD_NAME 16
#elif defined(__FreeBSD__) || defined(__OpenBSD__)
#define MAX_THREAD_NAME 19
#include <pthread_np.h>
#elif defined(__apple_build_version__)
#include <sys/proc_info.h>
#define MAX_THREAD_NAME MAXTHREADNAMESIZE
#else
#define MAX_THREAD_NAME 16
#endif
extern void my_threadattr_global_init(void);
extern my_bool my_thread_global_init(void);
extern void my_thread_set_name(const char *);

View File

@@ -72,6 +72,7 @@ SET(SQL_EMBEDDED_SOURCES emb_qcache.cc libmysqld.c lib_sql.cc
../sql/mf_iocache.cc ../sql/my_decimal.cc
../sql/net_serv.cc ../sql/opt_range.cc
../sql/opt_group_by_cardinality.cc
../sql/sql_parallel_workers.cc
../sql/opt_rewrite_date_cmp.cc
../sql/opt_rewrite_remove_casefold.cc
../sql/opt_sargable_left.cc

View File

@@ -1008,6 +1008,9 @@ The following specify which files/extra groups are read (specified before remain
Cost of checking the row against the WHERE clause.
Increasing this will have the optimizer to prefer plans
with less row combinations
--parallel-worker-threads=#
Number of worker threads available for parallel query
execution. 0 means parallel execution is disabled
--path=name Comma-separated list of schema names that defines the
search order for stored routines
--performance-schema
@@ -1995,6 +1998,7 @@ optimizer-trace
optimizer-trace-max-mem-size 1048576
optimizer-use-condition-selectivity 4
optimizer-where-cost 0.032
parallel-worker-threads 0
path CURRENT_SCHEMA
performance-schema FALSE
performance-schema-accounts-size -1

View File

@@ -0,0 +1,63 @@
SET DEBUG_SYNC='RESET';
#
# MDEV-39492 Parallel Query: Study how to create worker threads
#
create table t1 (a int) engine=innodb;
insert into t1 select seq from seq_1_to_2;
set session parallel_worker_threads=3;
# we should currently see 3 warnings;
select SQL_BUFFER_RESULT a from t1;
a
1
2
set session parallel_worker_threads=10;
select SQL_BUFFER_RESULT a from t1;
a
1
2
set session parallel_worker_threads=0;
connect killee, localhost, root, , ;
# check that kill query on a parallel worker is passed to the manager
connection killee;
set session parallel_worker_threads=3;
SET @save_dbug= @@global.debug_dbug;
SET GLOBAL debug_dbug = "+d,pwt_worker_pause_before_signal";
select SQL_BUFFER_RESULT a from t1;;
# now use the default connection to view/kill the thread group executing
# the parallel work
connection default;
SET DEBUG_SYNC='now WAIT_FOR pwt_worker_paused';
# wait for all 3 workers to hit our debug sync point
# then have a look at what is in our process list without thread ID
select USER, DB, STATE, SUBSTRING(INFO, 1, 32)
from information_schema.processlist
where (INFO regexp '.*parallel worker.*' or
STATE regexp '.*parallel worker.*') and
NOT INFO regexp '.*information_schema\.processlist.*';
USER DB STATE SUBSTRING(INFO, 1, 32)
root test debug sync point: now Parallel Worker 3 For Thread ID
root test debug sync point: now Parallel Worker 2 For Thread ID
root test debug sync point: now Parallel Worker 1 For Thread ID
root test Reading data from parallel workers select SQL_BUFFER_RESULT a from
kill query ID;
# signal our workers to continue execution
SET DEBUG_SYNC = "now SIGNAL pwt_worker_continue";
# then wait for the manager thread to clean up and go back to sleep
# review error message on --reap
connection killee;
ERROR 70100: Query execution was interrupted
SET DEBUG_SYNC = "RESET";
# save as above, but kill a worker with a simple kill and see the
# connection drop
select SQL_BUFFER_RESULT a from t1;;
connection default;
SET DEBUG_SYNC='now WAIT_FOR pwt_worker_paused';
kill ID;
SET DEBUG_SYNC = "now SIGNAL pwt_worker_continue";
connection killee;
ERROR 70100: Query execution was interrupted
# killee connection now dead, confirmed below
connection default;
drop table t1;
SET GLOBAL debug_dbug = @save_dbug;
SET DEBUG_SYNC = "RESET";

View File

@@ -0,0 +1,110 @@
#
# Test KILL and KILL QUERY statements.
#
-- source include/count_sessions.inc
-- source include/not_embedded.inc
-- source include/have_innodb.inc
-- source include/have_sequence.inc
--source include/have_debug.inc
--source include/have_debug_sync.inc
SET DEBUG_SYNC='RESET';
--disable_service_connection
--echo #
--echo # MDEV-39492 Parallel Query: Study how to create worker threads
--echo #
create table t1 (a int) engine=innodb;
insert into t1 select seq from seq_1_to_2;
set session parallel_worker_threads=3;
--echo # we should currently see 3 warnings;
select SQL_BUFFER_RESULT a from t1;
set session parallel_worker_threads=10;
select SQL_BUFFER_RESULT a from t1;
set session parallel_worker_threads=0;
connect (killee, localhost, root, , );
--echo # check that kill query on a parallel worker is passed to the manager
--connection killee
let $id= `select connection_id()`;
set session parallel_worker_threads=3;
# worker THDs don't inherit session vars, we are using a debug_sync_set_action
# to pause execution in our worker threads just after they have done something
SET @save_dbug= @@global.debug_dbug;
SET GLOBAL debug_dbug = "+d,pwt_worker_pause_before_signal";
--send select SQL_BUFFER_RESULT a from t1;
--echo # now use the default connection to view/kill the thread group executing
--echo # the parallel work
--connection default
SET DEBUG_SYNC='now WAIT_FOR pwt_worker_paused';
let $name= "Parallel Worker 1 For Thread ID $id";
let $killID= `SELECT @kid:=ID from information_schema.processlist
where info like $name limit 1`;
--echo # wait for all 3 workers to hit our debug sync point
let $wait_condition=
select count(*) = 3 from information_schema.processlist
where state like 'debug sync%' and info like '%Thread ID $id';
--source include/wait_condition.inc
--echo # then have a look at what is in our process list without thread ID
#select * from information_schema.processlist;
select USER, DB, STATE, SUBSTRING(INFO, 1, 32)
from information_schema.processlist
where (INFO regexp '.*parallel worker.*' or
STATE regexp '.*parallel worker.*') and
NOT INFO regexp '.*information_schema\.processlist.*';
--replace_result $killID ID
eval kill query $killID;
--echo # signal our workers to continue execution
SET DEBUG_SYNC = "now SIGNAL pwt_worker_continue";
--echo # then wait for the manager thread to clean up and go back to sleep
let $wait_condition=
select count(*) = 1 from information_schema.processlist
where command = 'Sleep' and id = $id;
--source include/wait_condition.inc
#select * from information_schema.processlist;
--echo # review error message on --reap
--connection killee
--error ER_QUERY_INTERRUPTED
reap;
SET DEBUG_SYNC = "RESET";
--echo # save as above, but kill a worker with a simple kill and see the
--echo # connection drop
--send select SQL_BUFFER_RESULT a from t1;
connection default;
SET DEBUG_SYNC='now WAIT_FOR pwt_worker_paused';
let $name= "Parallel Worker 1 For Thread ID $id";
let $killID= `SELECT @kid:=ID from information_schema.processlist
where info like $name limit 1`;
--replace_result $killID ID
eval kill $killID;
SET DEBUG_SYNC = "now SIGNAL pwt_worker_continue";
let $wait_condition=
select count(*) = 0 from information_schema.processlist
where command = 'Sleep' and id = $id;
--source include/wait_condition.inc
connection killee;
--disable_warnings
--error ER_QUERY_INTERRUPTED
reap;
--echo # killee connection now dead, confirmed below
connection default;
drop table t1;
SET GLOBAL debug_dbug = @save_dbug;
SET DEBUG_SYNC = "RESET";
--source include/wait_until_count_sessions.inc

View File

@@ -0,0 +1,30 @@
#
# MDEV-39492 Parallel Query: OOM in worker error_to_queue surfaces a
# single ER_OUTOFMEMORY warning so worker diagnostics aren't silently
# lost.
#
set @save_dbug=@@global.debug_dbug;
set global debug_dbug="+d,pwt_error_to_queue_oom";
set session parallel_worker_threads=4;
# The prototype worker emits either a warning or my_error() depending on the
# parity of its thread id. Either path runs through error_to_queue; the
# DBUG injection forces both into the OOM branch, so neither the original
# warning nor the original error reaches the user. We expect just the
# manager-surfaced ER_OUTOFMEMORY warnings.
#
# Four workers rather than one: fewer than PARALLEL_QUERY_MIN_WORKERS are
# declined and run serially, and then no worker reaches the injection at
# all. Asking for one used to work and quietly stopped testing anything.
create table t1 (a int) engine=innodb;
insert into t1 select seq from seq_1_to_2000;
select * from t1 where a > 1999;
a
2000
Warnings:
Warning 1037 Parallel worker diagnostics were dropped due to memory allocation failure
show warnings;
Level Code Message
Warning 1037 Parallel worker diagnostics were dropped due to memory allocation failure
drop table t1;
set global debug_dbug=@save_dbug;
set session parallel_worker_threads=default;

View File

@@ -0,0 +1,45 @@
#
# Test OOM handling in the parallel worker error/warning queue.
#
-- source include/count_sessions.inc
-- source include/have_innodb.inc
-- source include/not_embedded.inc
-- source include/have_sequence.inc
-- source include/have_debug.inc
--disable_service_connection
--echo #
--echo # MDEV-39492 Parallel Query: OOM in worker error_to_queue surfaces a
--echo # single ER_OUTOFMEMORY warning so worker diagnostics aren't silently
--echo # lost.
--echo #
set @save_dbug=@@global.debug_dbug;
set global debug_dbug="+d,pwt_error_to_queue_oom";
set session parallel_worker_threads=4;
--echo # The prototype worker emits either a warning or my_error() depending on the
--echo # parity of its thread id. Either path runs through error_to_queue; the
--echo # DBUG injection forces both into the OOM branch, so neither the original
--echo # warning nor the original error reaches the user. We expect just the
--echo # manager-surfaced ER_OUTOFMEMORY warnings.
--echo #
--echo # Four workers rather than one: fewer than PARALLEL_QUERY_MIN_WORKERS are
--echo # declined and run serially, and then no worker reaches the injection at
--echo # all. Asking for one used to work and quietly stopped testing anything.
create table t1 (a int) engine=innodb;
# The table needs more leaf pages than one: the engine divides a table by
# its B-tree and declines a table it cannot divide, so a two-row table runs
# serially and no worker reaches the injected failure. The WHERE keeps the
# output small; the whole table is still scanned.
insert into t1 select seq from seq_1_to_2000;
select * from t1 where a > 1999;
show warnings;
drop table t1;
set global debug_dbug=@save_dbug;
set session parallel_worker_threads=default;
--source include/wait_until_count_sessions.inc

View File

@@ -3012,6 +3012,16 @@ NUMERIC_BLOCK_SIZE NULL
ENUM_VALUE_LIST NULL
READ_ONLY NO
COMMAND_LINE_ARGUMENT REQUIRED
VARIABLE_NAME PARALLEL_WORKER_THREADS
VARIABLE_SCOPE SESSION
VARIABLE_TYPE BIGINT UNSIGNED
VARIABLE_COMMENT Number of worker threads available for parallel query execution. 0 means parallel execution is disabled
NUMERIC_MIN_VALUE 0
NUMERIC_MAX_VALUE 100
NUMERIC_BLOCK_SIZE 1
ENUM_VALUE_LIST NULL
READ_ONLY NO
COMMAND_LINE_ARGUMENT REQUIRED
VARIABLE_NAME PATH
VARIABLE_SCOPE SESSION
VARIABLE_TYPE VARCHAR

View File

@@ -20,12 +20,7 @@
#include <stdio.h>
#ifdef _WIN32
#define MAX_THREAD_NAME 256
typedef HRESULT (*func_SetThreadDescription)(HANDLE,PCWSTR);
#elif defined(__linux__)
#define MAX_THREAD_NAME 16
#elif defined(__FreeBSD__) || defined(__OpenBSD__)
#include <pthread_np.h>
#endif
#if defined(HAVE_PSI_THREAD_INTERFACE) && !defined DBUG_OFF

View File

@@ -118,6 +118,7 @@ SET (SQL_SOURCE
../sql-common/client_plugin.c
opt_range.cc vector_mhnsw.cc
opt_group_by_cardinality.cc
sql_parallel_workers.cc
opt_rewrite_date_cmp.cc
opt_rewrite_remove_casefold.cc
opt_sargable_left.cc

View File

@@ -3750,6 +3750,7 @@ public:
DBUG_RETURN(rnd_end());
}
int ha_rnd_init_with_error(bool scan) __attribute__ ((warn_unused_result));
int ha_reset();
/* this is necessary in many places, e.g. in HANDLER command */
int ha_index_or_rnd_end()

View File

@@ -9978,6 +9978,7 @@ PSI_stage_info stage_starting= { 0, "starting", 0};
PSI_stage_info stage_waiting_for_flush= { 0, "Waiting for non trans tables to be flushed", 0};
PSI_stage_info stage_waiting_for_ddl= { 0, "Waiting for DDLs", 0};
PSI_stage_info stage_waiting_for_reset_master= { 0, "Waiting for a running RESET MASTER to complete", 0};
PSI_stage_info stage_reading_data_from_parallel_worker= { 0, "Reading data from parallel workers", 0};
#ifdef WITH_WSREP
// Additional Galera thread states
@@ -10070,6 +10071,11 @@ PSI_memory_key key_memory_user_var_entry_value;
PSI_memory_key key_memory_String_value;
PSI_memory_key key_memory_WSREP;
PSI_memory_key key_memory_trace_ddl_info;
PSI_memory_key key_memory_pwt_queued_event;
PSI_memory_key key_memory_pwt_error_message;
PSI_memory_key key_memory_pwt_workers;
PSI_memory_key key_memory_pwt_db;
PSI_memory_key key_memory_pwt_batch_rows;
#ifdef HAVE_PSI_INTERFACE
@@ -10211,7 +10217,8 @@ PSI_stage_info *all_server_stages[]=
& stage_reading_semi_sync_ack,
& stage_waiting_for_deadlock_kill,
& stage_starting,
& stage_waiting_for_reset_master
& stage_waiting_for_reset_master,
& stage_reading_data_from_parallel_worker
#ifdef WITH_WSREP
,
& stage_waiting_isolation,
@@ -10323,6 +10330,9 @@ static PSI_memory_info all_server_memory[]=
{ &key_memory_trace_ddl_info, "TRACE_DDL_INFO", 0}
};
extern void pwt_init_psi_keys(void);
/**
Initialise all the performance schema instrumentation points
used by the server.
@@ -10409,6 +10419,7 @@ void init_server_psi_keys(void)
stmt_info_rpl.m_flags= PSI_FLAG_MUTABLE;
mysql_statement_register(category, &stmt_info_rpl, 1);
#endif
pwt_init_psi_keys();
}
#endif /* HAVE_PSI_INTERFACE */

View File

@@ -500,6 +500,11 @@ extern PSI_memory_key key_memory_Table_trigger_dispatcher;
extern PSI_memory_key key_memory_native_functions;
extern PSI_memory_key key_memory_WSREP;
extern PSI_memory_key key_memory_trace_ddl_info;
extern PSI_memory_key key_memory_pwt_queued_event;
extern PSI_memory_key key_memory_pwt_error_message;
extern PSI_memory_key key_memory_pwt_workers;
extern PSI_memory_key key_memory_pwt_db;
extern PSI_memory_key key_memory_pwt_batch_rows;
/*
MAINTAINER: Please keep this list in order, to limit merge collisions.
@@ -647,6 +652,7 @@ extern PSI_stage_info stage_slave_background_wait_request;
extern PSI_stage_info stage_waiting_for_deadlock_kill;
extern PSI_stage_info stage_starting;
extern PSI_stage_info stage_waiting_for_reset_master;
extern PSI_stage_info stage_reading_data_from_parallel_worker;
#ifdef WITH_WSREP
// Additional Galera thread states
extern PSI_stage_info stage_waiting_isolation;

View File

@@ -1403,6 +1403,8 @@ public:
void save_last_pos() override
{ file->position(record); }
int get_type() override { return QS_TYPE_RANGE; }
/* Number of disjoint key intervals this scan covers. */
size_t num_ranges() const { return ranges.elements; }
void add_keys_and_lengths(String *key_names, String *used_lengths) override;
Explain_quick_select *get_explain(MEM_ROOT *alloc) override;
#ifndef DBUG_OFF

View File

@@ -452,6 +452,8 @@ constexpr privilege_t PRIV_SET_SYSTEM_GLOBAL_VAR_MAX_CONNECT_ERRORS=
// Was SUPER_ACL prior to 10.5.2
constexpr privilege_t PRIV_SET_SYSTEM_GLOBAL_VAR_MAX_PASSWORD_ERRORS=
CONNECTION_ADMIN_ACL;
constexpr privilege_t PRIV_SET_SYSTEM_GLOBAL_VAR_PARALLEL_WORKER_THREADS=
CONNECTION_ADMIN_ACL;
// Was SUPER_ACL prior to 10.5.2
constexpr privilege_t PRIV_SET_SYSTEM_GLOBAL_VAR_PROXY_PROTOCOL_NETWORKS=
CONNECTION_ADMIN_ACL;

View File

@@ -83,6 +83,7 @@ struct READ_RECORD
*/
Copy_field *copy_field;
Copy_field *copy_field_end;
public:
READ_RECORD() : table(NULL), cache(NULL) {}
~READ_RECORD() { end_read_record(this); }

View File

@@ -722,7 +722,7 @@ const char *thd_where(THD *thd)
THD::THD(my_thread_id id, bool is_wsrep_applier)
:Statement(&main_lex, &main_mem_root, STMT_CONVENTIONAL_EXECUTION,
/* statement id */ 0),
rli_fake(0), rgi_fake(0), rgi_slave(NULL),
rli_fake(0), rgi_fake(0), rgi_slave(NULL), parallel_worker(NULL),
protocol_text(this), protocol_binary(this), initial_status_var(0),
m_current_stage_key(0), m_psi(0), start_time(0), start_time_sec_part(0),
in_sub_stmt(0), log_all_errors(0),
@@ -5490,8 +5490,7 @@ void destroy_thd(MYSQL_THD thd)
/**
Create a THD that only has auxiliary functions
It will never be added to the global connection list
server_threads. It does not represent any client connection.
It does not represent any client connection.
It should never be counted, because it will stall the
shutdown. It is solely for engine's internal use,

View File

@@ -873,6 +873,7 @@ typedef struct system_variables
ulong server_id;
ulong session_track_transaction_info;
ulong threadpool_priority;
ulong parallel_worker_threads;
ulong vers_alter_history;
/* deadlock detection */
@@ -3320,7 +3321,7 @@ enum class THD_WHERE
const char *thd_where(THD *thd);
class pwt_worker;
/**
@class THD
@@ -3366,6 +3367,8 @@ public:
/* Slave applier execution context */
rpl_group_info* rgi_slave;
pwt_worker *parallel_worker;
union {
rpl_io_thread_info *rpl_io_info;
rpl_sql_thread_info *rpl_sql_info;

View File

@@ -1564,7 +1564,12 @@ int Explain_table_access::print_explain(select_result_sink *output,
/* `type` column */
StringBuffer<64> join_type_buf;
if (rowid_filter == NULL)
push_str(thd, &item_list, join_type_str[type]);
{
if (type == JT_ALL && use_parallel_scan)
push_str(thd, &item_list, "PARALLEL");
else
push_str(thd, &item_list, join_type_str[type]);
}
else
{
join_type_buf.append(join_type_str[type], strlen(join_type_str[type]));
@@ -2052,7 +2057,10 @@ void Explain_table_access::print_explain_json(Explain_query *query,
}
}
writer->add_member("access_type").add_str(join_type_str[type]);
if (type == JT_ALL && use_parallel_scan)
writer->add_member("access_type").add_str("PARALLEL");
else
writer->add_member("access_type").add_str(join_type_str[type]);
add_json_keyset(writer, "possible_keys", &possible_keys);

View File

@@ -812,6 +812,7 @@ public:
enum join_type type;
bool used_partitions_set;
bool use_parallel_scan;
/* Empty means "NULL" will be printed */
String_list possible_keys;

1136
sql/sql_parallel_workers.cc Normal file

File diff suppressed because it is too large Load Diff

243
sql/sql_parallel_workers.h Normal file
View File

@@ -0,0 +1,243 @@
#ifndef SQL_PARALLEL_WORKERS_H
#define SQL_PARALLEL_WORKERS_H
#include "mariadb.h"
#include "sql_class.h"
#include "mysqld.h"
#include "sql_error.h"
extern MYSQL_THD create_background_thd();
extern void destroy_background_thd(MYSQL_THD thd);
extern void *thd_attach_thd(MYSQL_THD thd);
extern void thd_detach_thd(void *save);
// PWT Parallel Worker Thread
/*
Message Types
*/
class pwt_error_message
{
public:
uint worker_errno;
uint code;
Sql_condition::enum_warning_level level;
char *message;
};
/*
Event type. Inherits ilink so it can live in an I_List<pwt_queued_event>.
*/
class pwt_queued_event : public ilink
{
public:
pwt_error_message *error;
};
/*
Number of rows a worker packs into its batch buffer before handing it to the
manager. The worker hands rows to the manager a batch at a time rather than
one at a time so the channel mutex is touched once per PWT_CHUNK_ROWS rows
instead of once per row. Each worker reuses a single raw row buffer (see
pwt_worker::batch_rows): it fills the buffer with up to PWT_CHUNK_ROWS raw
record images, hands it to the manager and blocks until the manager has
drained it, then refills it for the next batch.
*/
#define PWT_CHUNK_ROWS 2048
class pwt_manager;
typedef struct st_join_table JOIN_TAB;
class JOIN;
#define WORKER_NAME "Parallel Worker"
#define WORKER_ID_LENGTH 3
#define WORKER_NAME_LENGTH 15
#define CONNECTION_NAME_THREAD "For Thread ID"
#define CONNECTION_NAME_THREAD_LENGTH 13
#define THREAD_ID_LENGTH 20 // ull can occupy 20 chars
class pwt_worker_batch
{
public:
/*
This worker's single reused row buffer, PWT_CHUNK_ROWS * reclength bytes,
allocated up front in init_parallel_workers. The worker fills it with raw
record[0] images memcpy'd straight from scan_table (the worker and the
manager share that table's TABLE_SHARE, so a byte-for-byte record copy
reconstructs the row on the manager side -- which is why BLOB/TEXT tables,
whose payload lives off the record buffer, are excluded from parallel
scan). It hands the buffer to the manager, blocks until the manager has
drained it, then refills from the top. The worker and the manager never
touch it at the same time, so it needs no per-row locking.
*/
uchar *rows;
uint count; // rows the worker placed in batch_rows
/*
Hand-off flag for batch_rows, guarded by pwt_manager::LOCK_data.
The worker sets it true once the buffer is filled and ready for the manager;
the manager clears it once the buffer is drained, releasing the worker to
refill. See pwt_manager::handoff_batch / the consumer read function.
*/
bool full;
};
class pwt_worker_execution
{
public:
/*
Per-worker copy of the manager's first non-const source table.
Opened in init_parallel_workers, closed in the worker thread before
its THD is destroyed.
*/
TABLE *scan_table;
JOIN *worker_join; // The JOIN for the worker's JOIN_TABs
JOIN_TAB *join_tabs; // n_tables x non-const JOIN_TABs in order
Table_access_tracker *tab_stats;
ha_handler_stats *tab_hstats;
STATUS_VAR stats;
Parallel_worker_ctx *handler_ctx;
};
struct pwt_worker_info
{
char conn_name[MAX_THREAD_NAME+1];
/*
This is displayed in information_schema.processlist.info
Currently "Parallel Worker {1..N} For Thread M"
*/
char process_list[WORKER_NAME_LENGTH+
1+WORKER_ID_LENGTH+1+
CONNECTION_NAME_THREAD_LENGTH+
1+THREAD_ID_LENGTH+1];
};
struct pwt_worker_state
{
bool joined;
bool finished;
killed_state killed;
};
/*
Parallel Worker Thread specific attributes
*/
class pwt_worker
{
public:
THD *thd;
pwt_manager *manager;
pthread_t pthread;
mysql_mutex_t LOCK_worker;
mysql_cond_t COND_worker;
pwt_worker_info info;
pwt_worker_state state;
pwt_worker_batch batch;
pwt_worker_execution exec;
};
/*
Streaming channel. Each worker (producer) fills its single reused row
buffer (batch_rows) and hands it to the manager (single consumer) by
setting its batch_full flag; the manager drains the buffer from its first
join_tab read function and runs the rest of the join as the batches arrive,
instead of waiting for every worker to finish first.
LOCK_data guards cur_worker, the workers' batch_full flags, active_workers
and the flags below. COND_data_avail wakes the consumer when a worker
fills its buffer or finishes; COND_data_space wakes a worker when the
manager has drained its buffer so it may refill. Because each worker owns
one buffer and blocks until it is drained, at most one batch per worker is
ever outstanding -- the single buffer is the natural backpressure bound.
EOF for the consumer is the state (no worker has batch_full set &&
active_workers == 0).
*/
class pwt_manager_drain
{
public:
mysql_mutex_t LOCK_data;
mysql_cond_t COND_data_avail;
mysql_cond_t COND_data_space;
pwt_worker *cur_worker; // worker whose buffer the consumer drains
uint cur_cursor; // consumer's row index within cur_worker
uint reclength; // shared record image size (bytes)
uint active_workers; // producers still running
bool fatal_error; // a producer hit a real engine error
bool stop; // consumer wants producers to stop
pwt_manager_drain():
cur_worker(nullptr),
active_workers(0),
fatal_error(false),
stop(false)
{ }
};
/*
Class to create, manage and eventually destroy a "team" of worker threads.
*/
class pwt_manager : public Sql_alloc
{
public:
pwt_worker *workers;
uint nworkers;
I_List<pwt_queued_event> parallel_messages;
mysql_mutex_t LOCK_pwt_thread;
THD *thd;
JOIN *join;
JOIN_TAB *scan_tab;
pwt_manager_drain drain;
/*
Set under LOCK_pwt_thread when a worker fails to allocate a queued event.
The manager surfaces a single ER_OUTOFMEMORY warning so the user sees
that worker diagnostics were dropped instead of silently disappearing.
*/
bool messages_dropped;
/*
Set once the workers have been stopped and pthread_join'd (quiesce_workers).
Workers read this join's source table and write their row buffers, so they
must be reaped before JOIN::join_free()->cleanup() frees the source table;
quiesce_workers is called from join_free, and again (idempotently) from
finalize.
*/
bool reaped;
/*
Set (under LOCK_data) to a worker's killed_state when that worker exits
because it was killed -- e.g. a user KILL [QUERY] aimed at a parallel
worker. The consumer propagates it to the manager's own THD so the join
aborts with the right error (ER_QUERY_INTERRUPTED) before any result is
sent, rather than completing and trying to raise the error too late.
*/
killed_state kill_signal;
pwt_manager():
workers(nullptr),
nworkers(0),
messages_dropped(false),
reaped(false),
kill_signal(NOT_KILLED)
{}
~pwt_manager()
{
finalize_parallel_workers(current_thd, join);
}
int init_parallel_workers(THD *thd, JOIN *join, JOIN_TAB *scan_tab);
void quiesce_workers();
void finalize_parallel_workers(THD *thd, JOIN *join);
bool handoff_batch(pwt_worker *worker);
void free_queue();
};
extern bool table_can_be_parallel_scanned(TABLE *table);
extern bool scale_cost_for_parallel_scan(THD *thd, TABLE *table,
ALL_READ_COST *cost);
extern int parallel_init_read_record(JOIN_TAB *tab);
#endif

View File

@@ -26,6 +26,7 @@
*/
#include "mariadb.h"
#include "mysqld_error.h"
#include "sql_priv.h"
#include "unireg.h"
#include "sql_select.h"
@@ -71,6 +72,7 @@
#include "derived_handler.h"
#include "opt_hints.h"
#include "opt_group_by_cardinality.h"
#include "sql_parallel_workers.h"
/*
A key part number that means we're using a fulltext scan.
@@ -2793,6 +2795,122 @@ setup_subq_exit:
DBUG_RETURN(0);
}
/*
@brief
Whether the access method the plan settled on is one the engine will hand
out in pieces.
@description
Asked of the driving table only, and asked about the *access method*: the
table-level question -- a real base table, no blob-backed columns, not
fulltext-searched, not partitioned, an engine that does this at all -- is
table_can_be_parallel_scanned()'s, and the caller asks it first. Keeping
the two apart matters because the optimizer's cost hook
(scale_cost_for_parallel_scan) asks only the table-level one, and a copy
here would be free to drift from it.
*/
static bool is_parallel_scan_applicable(JOIN_TAB *join_tab)
{
/*
Two access methods are eligible: a table scan (EXPLAIN type=ALL) and a
range scan over one index. A range scan reaches here as either JT_ALL or
JT_RANGE, depending on how the plan arrived at it -- make_join_select()
promotes a ref to JT_RANGE when the range uses a longer key -- and both
read through join_init_read_record. read_first_record is checked next to
->type because this runs after make_join_readinfo(), which is what actually
picked the reader.
A complete scan of one index (type=index, JT_NEXT) is NOT eligible, even
though InnoDB advertises PSCAN_INDEX_FULL. Handing one to the coordinator
trips dict_index_check_search_tuple() in InnoDB -- the search tuple carries
more fields than the index has unique fields in the tree. The engine side
of that (MDEV-40005) is newer than the worker side, and nothing in the SQL
layer has driven it yet; until it does, this is the check that keeps the
two apart.
*/
if (!((join_tab->type == JT_ALL || join_tab->type == JT_RANGE) &&
join_tab->read_first_record == join_init_read_record))
return false;
/*
The plan may be relying on this table's rows arriving in index order to
satisfy ORDER BY or GROUP BY without a filesort, but a parallel scan
does not preserve that order. Reject parallelization in that case.
*/
if (join_tab->join->ordered_index_usage != JOIN::ordered_index_void)
return false;
if (join_tab->filesort || join_tab->filesort_result ||
join_tab->need_to_build_rowid_filter || join_tab->rowid_filter ||
join_tab->distinct)
return false;
const uint32 pscan_support= join_tab->table->file->parallel_scan_support();
SQL_SELECT *sql_select= join_tab->select;
if (sql_select && sql_select->quick)
{
/*
The case of a range scan.
Only a plain range over the clustered index can be handed to the
parallel coordinator, and only if it has few enough intervals to be
worth splitting.
OLEGS: we try to make handler API engine-agnostic but here ^^^ we mention
"clustered index" which is only valid for InnoDB. In fact, what we currently
support is a full table scan which is indeed a scan of the clustered index
in InnoDB. Need to think about it
Why the clustered index and not any index: the coordinator converts each
endpoint against it -- ha_innobase's pscan_convert_key() builds the tuple
from table->s->primary_key -- so an interval over a secondary index would
be read as a primary-key interval and scan the wrong part of the table.
Asking for one is what trips dict_index_check_search_tuple(), the same
assertion the index scan above hits.
*/
const uint MAX_PARALLEL_SCAN_RANGES= 128;
if (sql_select->quick->get_type() == QUICK_SELECT_I::QS_TYPE_RANGE &&
sql_select->quick->index < join_tab->table->s->keys &&
join_tab->use_quick != 2 /*exclude dynamic range*/ &&
((QUICK_RANGE_SELECT*) sql_select->quick)->num_ranges() <=
MAX_PARALLEL_SCAN_RANGES)
{
/*
The engine decides whether it can partition this particular index -
see ha_innobase::pscan_resolve_index(), which declines spatial, FTS,
virtual-column and descending indexes - and we fall back to the serial
reader on HA_ERR_UNSUPPORTED.
TODO: this looks very InnoDB-specific. We should have a more generic
way to ask the engine
*/
if (!(join_tab->table->s->primary_key < MAX_KEY &&
sql_select->quick->index == join_tab->table->s->primary_key))
return false; // not the clustered index
/*
A rowid-ordered scan collects the row ids, sorts them and then sweeps
the clustered index in that order. Taking such a plan would quietly
throw the sorted sweep away. Leave it serial.
*/
if (((QUICK_RANGE_SELECT*) sql_select->quick)->mrr_flags &
DSMRR_IMPL_SORT_ROWIDS)
return false;
return (pscan_support & handler::PSCAN_TABLE_RANGE) != 0;
}
else
{
return false;
}
}
else
{
return (pscan_support & handler::PSCAN_TABLE_FULL) != 0;
}
}
/*
@brief
@@ -3582,6 +3700,45 @@ int JOIN::optimize_stage2()
if (init_range_rowid_filters())
DBUG_RETURN(1);
/*
Run this query's driving-table scan in parallel workers if possible.
1) worker threads are available;
2) the table itself is one a worker can read and ship
(table_can_be_parallel_scanned);
3) the access method the plan settled on is one the engine will divide
(is_parallel_scan_applicable), which is also where the engine's
parallel_scan_support() bitmap is matched against it.
This is the last thing optimize_stage2() decides, and it has to be:
everything above can still change the first table's access method, and
make_join_readinfo() -- which is where this check used to live -- runs
before test_if_skip_sort_order() has had its say. A table given an ordered
index scan after the fact is not one that can be handed out in chunks.
Engine-intrinsic constraints (consistent-read only, record format,
discarded tablespace, ...) are not known here. They are enforced later
inside parallel_init_coordinator(), which declines with HA_ERR_UNSUPPORTED,
and parallel_init_read_record() then restores the serial reader.
*/
{
JOIN_TAB *first= first_linear_tab(this, WITH_BUSH_ROOTS,
WITHOUT_CONST_TABLES);
if (thd->variables.parallel_worker_threads > 0 && //1
first && table_can_be_parallel_scanned(first->table) && //2
is_parallel_scan_applicable(first)) //3
{
first->use_parallel_scan= true;
first->read_first_record= parallel_init_read_record;
if (unlikely(thd->trace_started()))
{
Json_writer_object trace_pscan(thd);
trace_pscan.add("chosen_for_parallel_scan",
first->table->alias.c_ptr());
}
}
}
error= 0;
if (select_options & SELECT_DESCRIBE)
@@ -4372,8 +4529,6 @@ bool JOIN::make_aggr_tables_info()
DBUG_RETURN(false);
}
bool
JOIN::create_postjoin_aggr_table(JOIN_TAB *tab, List<Item> *table_fields,
ORDER *table_group,
@@ -4406,13 +4561,20 @@ JOIN::create_postjoin_aggr_table(JOIN_TAB *tab, List<Item> *table_fields,
if (tmp_table_keep_current_rowid)
add_fields_for_current_rowid(tab, table_fields);
tab->tmp_table_param->skip_create_table= true;
TABLE* table= create_tmp_table(thd, tab->tmp_table_param, *table_fields,
table_group, distinct,
save_sum_fields, select_options,
table_rows_limit,
&empty_clex_str, true, keep_row_order);
if (!table)
DBUG_RETURN(true);
goto err;
// instantiate this now so the parallel worker manager can write to it
if (instantiate_tmp_table(table, tab->tmp_table_param->keyinfo,
tab->tmp_table_param->start_recinfo,
&tab->tmp_table_param->recinfo,
select_options))
goto err;
tmp_table_param.using_outer_summary_function=
tab->tmp_table_param->using_outer_summary_function;
tab->join= this;
@@ -4910,10 +5072,14 @@ int JOIN::exec()
select_lex->select_number))
dbug_serve_apcs(thd, 1);
);
ANALYZE_START_TRACKING(thd, &explain->time_tracker);
res= exec_inner();
ANALYZE_STOP_TRACKING(thd, &explain->time_tracker);
if (parallel_work_manager)
parallel_work_manager->finalize_parallel_workers(thd, this);
DBUG_EXECUTE_IF("show_explain_probe_join_exec_end",
if (dbug_user_var_equals_int(thd,
"show_explain_probe_select_id",
@@ -9963,6 +10129,19 @@ best_access_path(JOIN *join,
}
}
/*
A full table scan of the driving table can be shared across
parallel_worker_threads workers (see make_join_readinfo). Discount its
row-read/copy cost accordingly so the optimizer accounts for the
parallelism. Only the driving table (idx == const_tables) is ever
parallel-scanned, and 'cost' is a local copy, so the cached per-table
estimate is untouched.
*/
if ((type == JT_ALL || type == JT_RANGE) &&
table_can_be_parallel_scanned(table) &&
idx == join->const_tables)
scale_cost_for_parallel_scan(thd, table, &cost);
/*
Note: the condition checked here is very out of date and incorrect.
Below, we use a more accurate check when assigning the value of
@@ -13485,7 +13664,9 @@ bool JOIN::get_best_combination()
j->index= cur_pos->forced_index;
}
else
{
j->type= JT_ALL;
}
if (cur_pos->use_join_buffer &&
tablenr != const_tables)
full_join= 1;
@@ -16693,7 +16874,6 @@ make_join_readinfo(JOIN *join, ulonglong options, uint no_jbuf_after)
break;
}
}
DBUG_RETURN(FALSE);
}
@@ -16830,6 +17010,7 @@ void JOIN_TAB::cleanup()
else
table->file->ha_index_or_rnd_end();
preread_init_done= FALSE;
if (table->pos_in_table_list && table->pos_in_table_list->jtbm_subselect)
{
if (table->pos_in_table_list->jtbm_subselect->is_jtbm_const_tab)
@@ -17275,6 +17456,16 @@ void JOIN::join_free()
bool can_unlock= full;
DBUG_ENTER("JOIN::join_free");
/*
Parallel-scan workers read this join's source and batch tables. cleanup()
below frees those tables, so the workers must be stopped and reaped first
-- otherwise a worker that has not yet seen the stop request dereferences a
freed table->file. On normal completion the workers have already finished,
so this is a cheap no-op.
*/
if (parallel_work_manager)
parallel_work_manager->quiesce_workers();
cleanup(full);
for (tmp_unit= select_lex->first_inner_unit();
@@ -17456,6 +17647,12 @@ void JOIN::cleanup(bool full)
{
set_items_ref_array(items0);
}
if (parallel_work_manager)
{
delete parallel_work_manager;
parallel_work_manager= NULL;
}
DBUG_VOID_RETURN;
}
@@ -23340,12 +23537,13 @@ bool Virtual_tmp_table::check_assignability_from(const TABLE &table,
}
bool open_tmp_table(TABLE *table)
bool open_tmp_table(TABLE *table, bool cross_thread)
{
int error;
if (unlikely((error= table->file->ha_open(table, table->s->path.str, O_RDWR,
HA_OPEN_TMP_TABLE |
HA_OPEN_INTERNAL_TABLE |
(cross_thread? 0 :
HA_OPEN_INTERNAL_TABLE) |
HA_OPEN_SIZE_TRACKING))))
{
table->file->print_error(error, MYF(0)); /* purecov: inspected */
@@ -24352,7 +24550,8 @@ do_select(JOIN *join, Procedure *procedure)
bool instantiate_tmp_table(TABLE *table, KEY *keyinfo,
TMP_ENGINE_COLUMNDEF *start_recinfo,
TMP_ENGINE_COLUMNDEF **recinfo,
ulonglong options)
ulonglong options,
bool cross_thread)
{
DBUG_ASSERT(table->s->keys == 0 || table->key_info == keyinfo);
DBUG_ASSERT(table->s->keys <= 1);
@@ -24370,7 +24569,7 @@ bool instantiate_tmp_table(TABLE *table, KEY *keyinfo,
empty_record(table);
table->status= STATUS_NO_RECORD;
}
if (open_tmp_table(table))
if (open_tmp_table(table, cross_thread))
return TRUE;
return FALSE;
@@ -31262,6 +31461,7 @@ bool JOIN_TAB::save_explain_data(Explain_table_access *eta,
tab_type= type == JT_HASH ? JT_HASH_RANGE : JT_RANGE;
}
eta->type= tab_type;
eta->use_parallel_scan= use_parallel_scan;
/* Build "possible_keys" value */
// psergey-todo: why does this use thd MEM_ROOT??? Doesn't this

View File

@@ -33,6 +33,7 @@
#include "sql_update.h"
#include "cset_narrowing.h"
#include "sql_parallel_workers.h"
typedef struct st_join_table JOIN_TAB;
/* Values in optimize */
@@ -667,6 +668,7 @@ typedef struct st_join_table {
bool shortcut_for_distinct;
bool sorted;
bool cached_pfs_batch_update;
bool use_parallel_scan;
/*
If it's not 0 the number stored this field indicates that the index
@@ -1992,6 +1994,8 @@ public:
*/
Sql_cmd_dml *sql_cmd_dml;
pwt_manager *parallel_work_manager{0};
JOIN(THD *thd_arg, List<Item> &fields_arg, ulonglong select_options_arg,
select_result *result_arg)
:fields_list(fields_arg)
@@ -2024,6 +2028,7 @@ public:
int optimize_inner();
int optimize_stage2();
int optimize_stage2_and_finish();
void choose_parallel_scan();
bool build_explain();
int reinit();
int init_execution();
@@ -2220,7 +2225,6 @@ private:
In this case we can stop scanning t2 when we have found one t1.a
*/
void optimize_distinct();
void cleanup_item_list(List<Item> &items) const;
bool add_having_as_table_cond(JOIN_TAB *tab);
bool make_aggr_tables_info();
@@ -2943,8 +2947,9 @@ bool create_internal_tmp_table(TABLE *table, KEY *keyinfo,
bool instantiate_tmp_table(TABLE *table, KEY *keyinfo,
TMP_ENGINE_COLUMNDEF *start_recinfo,
TMP_ENGINE_COLUMNDEF **recinfo,
ulonglong options);
bool open_tmp_table(TABLE *table);
ulonglong options,
bool cross_thread= false);
bool open_tmp_table(TABLE *table, bool cross_thread= false);
void fix_list_after_tbl_changes(SELECT_LEX *new_parent, List<TABLE_LIST> *tlist);
void optimize_keyuse(JOIN *join, DYNAMIC_ARRAY *keyuse_array);
bool sort_and_filter_keyuse(JOIN *join, DYNAMIC_ARRAY *keyuse,

View File

@@ -2496,6 +2496,17 @@ Sys_slave_parallel_workers(
ON_UPDATE(fix_slave_parallel_threads));
static Sys_var_on_access_global<Sys_var_ulong,
PRIV_SET_SYSTEM_GLOBAL_VAR_PARALLEL_WORKER_THREADS>
Sys_parallel_worker_threads(
"parallel_worker_threads",
"Number of worker threads available for parallel query execution. "
"0 means parallel execution is disabled",
SESSION_VAR(parallel_worker_threads), CMD_LINE(REQUIRED_ARG),
VALID_RANGE(0,100), DEFAULT(0), BLOCK_SIZE(1));
/* VALID_RANGE(0,100), DEFAULT(1), BLOCK_SIZE(1)); // testing 123 */
static bool
check_slave_domain_parallel_threads(sys_var *self, THD *thd, set_var *var)
{

View File

@@ -15053,7 +15053,7 @@ int ha_innobase::parallel_init_worker(Parallel_worker_ctx *wctx)
DBUG_ASSERT(worker_ctx && worker_ctx->m_pcoordinator);
auto exec_ctx= worker_ctx->m_pcoordinator->get_job_for_worker(worker_ctx);
if (exec_ctx == nullptr)
return HA_ERR_END_OF_FILE; // No more data
return HA_ERR_END_OF_FILE; // No more data
/* Save the prebuilt-owned search_tuple: parallel_get_next_row() points
m_prebuilt->search_tuple at a chunk boundary key owned by the