/* 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 #include #include #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::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 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; }