mysql-server/sql/server_component/ongoing_transaction_query_service.cc
2025-03-05 14:31:37 +07:00

234 lines
8.7 KiB
C++

/* Copyright (c) 2018, 2024, Oracle and/or its affiliates.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License, version 2.0,
as published by the Free Software Foundation.
This program is designed to work with certain software (including
but not limited to OpenSSL) that is licensed under separate terms,
as designated in a particular file or component or in included license
documentation. The authors of MySQL hereby grant you an additional
permission to link the program and your derivative works with the
separately licensed software that they have either included with
the program or referenced in the documentation.
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, version 2.0, 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 St, Fifth Floor, Boston, MA 02110-1301 USA */
#include <sql/current_thd.h>
#include <sql/mysqld_thd_manager.h>
#include <sql/sql_lex.h>
#include "mutex_lock.h" // MUTEX_LOCK
#include "mysql/components/services/log_builtins.h"
#include "mysql_ongoing_transaction_query_imp.h"
#include "sql/sql_class.h" // THD
#include "sql/sql_parse.h" // sql_command_flags
class Get_running_transactions : public Do_THD_Impl {
public:
Get_running_transactions() = default;
/*
This method relies on the assumption that a thread running a query
will either have an active query plan, or is in the middle of a
multi-statement transaction.
*/
void operator()(THD *thd) override {
if (thd->is_killed() || thd->is_error()) return;
{
MUTEX_LOCK(lock_thd_data, &thd->LOCK_thd_data);
if (thd->is_being_disposed()) return;
}
/*
LOCK_thd_data doesn't protect all the thd's data,
and unfortunately not those we're interested in.
Therefore, we're now taking LOCK_thd_query to
prevent the THD from being reset while we're
inspecting it. This prevents a race below between
when we're doing a sanity check on l->sql_command
and when we're dereferencing l->m_sql_cmd.
*/
MUTEX_LOCK(lock_thd_data, &thd->LOCK_thd_query);
LEX *l = thd->lex;
/*
In an ideal world, we might be able to just look at whether
sql_command_flags[sql_command] &
(CF_CHANGES_DATA | CF_AUTO_COMMIT_TRANS)
is true to identify interesting DDL. Unfortunately, sql_command
is not always set to a valid command. It will be SQLCOM_END
before parsing, and during e.g. stored routine processing.
To further muddy the waters, we change sql_command at various
locations in the code.
That said, this still wouldn't detect being in the middle of a
multi-statement transaction, so we also explicitly inspect
transaction state below.
*/
// "Command not otherwise specified"
enum_sql_command sql_command = SQLCOM_END;
/*
No flags (blocking or otherwise) found yet.
We must fail-open as we may never get a usable lex on some threads.
*/
int blocked_by_sql_command = 0;
/*
If an attachable transaction is active, we consider that
blocking. Crucially, we also can't trust sql_command / m_sql_cmd
while this is the case, as they will reflect the attached transaction,
not the enclosing command (which we're interested in).
Example:
CREATE USER itself will rightfully block (as a result of the
sql_command_flags that we get from its sql_command,
SQLCOM_CREATE_USER).
However, we internally run e.g. check_orphaned_definers() which
temporarily sets the lex to SQLCOM_SELECT, which would not be
considered a blocking statement in this method. Thus, if we
went by the attached sql_command (SQLCOM_SELECT, rather than
SQLCOM_CREATE_USER), the following scenario would be possible:
- CREATE USER starts. The lex info is set to SQLCOM_CREATE_USER.
- mysql_create_user() runs check_ophaned_definers(), which changes
the lex.
- This function is run, sees SQLCOM_SELECT on the lex (rather
than seeing SQLCOM_CREATE_USER as would be warranted by the
enclosing statement).
- This function reports no blocking statements / transactions
being underway.
- This function's caller stops waiting and proceeds when it
shouldn't, e.g. trying to change the primary in a group
replication scenario.
- An error is thrown because the caller prematurely sets
@@global.read_only while CREATE USER is still running;
CREATE USER needlessly fails (which is the exact case
we're trying to prevent here).
Therefore, if we detect an attachable transaction, we mark
the enclosing command as blocking for the time being, even
if the enclosing command might not warrant this. We do this
a) because we can not easily detect the enclosing command;
b) by setting all flags on the "blocked" bit vector (as we
do not know which apply, and this renders the correct
results).
*/
if (thd->is_attachable_transaction_active()) blocked_by_sql_command = -1;
/*
Get command code set on the lex.
If we get something valid, we'll inspect the flags for that command
to see whether the command auto-commits. DDL commands should match
this pattern.
*/
else if ((l != nullptr) && ((sql_command = l->sql_command) != SQLCOM_END)) {
/*
If we got something better than SQLCOM_END from the lex,
the lex was set up.
See whether this is a "new-style" command (i.e. it has
an object derived from Sql_cmd set on thd->lex->m_sql_cmd).
If so, that's the info we'll use.
Unfortunately, while there are situations where sql_command
is SQLCOM_END but there is valid non-NULL value in m_sql_cmd,
there are others where that value is garbage, so we may not
deref in such cases. But what we can't identify as a specific
command here, we still may have identified as TX_STMT_DDL in
the transaction tracker (see below).
*/
if (l->m_sql_cmd != nullptr)
sql_command = l->m_sql_cmd->sql_command_code();
blocked_by_sql_command =
sql_command_flags[sql_command] &
(CF_CHANGES_DATA | CF_REQUIRE_ACL_CACHE | CF_IMPLICIT_COMMIT_BEGIN |
CF_IMPLICIT_COMMIT_END);
}
/*
Query the transaction tracker for relevant flags.
TX_EXPLICIT indicates a transaction that was started explicitly,
e.g. with BEGIN / START TRANSACTION.
(See also in_active_multi_stmt_transaction().)
TX_STMT_DML is turned on if the statement "behaves like DML"
(by passing through run_before_dml_hook()).
TX_STMT_DDL is turned on if after parsing, the statement
identifies as DDL (by means of sql_cmd_type()) and
"behaves like DDL" (by passing through mark_trx_read_write()).
Due to the different life-cycles,
((tst->get_trx_state() & TX_STMT_DDL) > 0)
may differ from
(blocked_by_sql_command > 0)
This works to our advantage in certain corner cases as it
extends our gaze.
*/
TX_TRACKER_GET(tst);
int blocked_by_trx_tracker =
tst->get_trx_state() & (TX_EXPLICIT | TX_STMT_DML | TX_STMT_DDL);
/*
Now add this thread to the list of showstoppers for change-primary
if we found a reason to.
*/
if ((blocked_by_sql_command != 0) || (blocked_by_trx_tracker != 0)) {
thread_ids.push_back(thd->thread_id());
}
}
ulong get_transaction_count() { return thread_ids.size(); }
void fill_transaction_ids(unsigned long **ids) {
size_t number_thd = thread_ids.size();
*ids = (unsigned long *)my_malloc(
PSI_NOT_INSTRUMENTED, number_thd * sizeof(unsigned long), MYF(MY_WME));
int index = 0;
for (std::vector<my_thread_id>::iterator it = thread_ids.begin();
it != thread_ids.end(); ++it) {
(*ids)[index] = *it;
index++;
}
}
private:
/* Status of all threads are summed into this. */
std::vector<my_thread_id> thread_ids;
};
DEFINE_BOOL_METHOD(
mysql_ongoing_transactions_query_imp::get_ongoing_server_transactions,
(unsigned long **thread_ids, unsigned long *length)) {
Get_running_transactions trx_counter;
Global_THD_manager::get_instance()->do_for_all_thd(&trx_counter);
trx_counter.fill_transaction_ids(thread_ids);
*length = trx_counter.get_transaction_count();
return false;
}