/* Copyright (c) 2018, 2025, 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;
}
