533 lines
18 KiB
C++
533 lines
18 KiB
C++
/*
|
|
Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
|
|
|
|
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 also distributed 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 included with MySQL.
|
|
|
|
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
|
|
*/
|
|
|
|
// Implements
|
|
#include "storage/ndb/plugin/ndb_metadata_change_monitor.h"
|
|
|
|
#include <chrono>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <unordered_set>
|
|
#include <vector>
|
|
|
|
#include "my_dbug.h"
|
|
#include "mysql/psi/mysql_cond.h" // mysql_cond_t
|
|
#include "mysql/psi/mysql_mutex.h" // mysql_mutex_t
|
|
#include "sql/sql_class.h" // THD
|
|
#include "sql/table.h" // is_infoschema_db() / is_perfschema_db()
|
|
#include "storage/ndb/include/ndbapi/NdbError.hpp" // NdbError
|
|
#include "storage/ndb/plugin/ha_ndbcluster_binlog.h" // ndb_binlog_is_read_only
|
|
#include "storage/ndb/plugin/ha_ndbcluster_connection.h" // ndbcluster_is_connected
|
|
#include "storage/ndb/plugin/ndb_dd_client.h" // Ndb_dd_client
|
|
#include "storage/ndb/plugin/ndb_ndbapi_util.h" // ndb_get_*_names
|
|
#include "storage/ndb/plugin/ndb_sleep.h" // ndb_milli_sleep
|
|
#include "storage/ndb/plugin/ndb_thd.h" // thd_set_thd_ndb
|
|
#include "storage/ndb/plugin/ndb_thd_ndb.h" // Thd_ndb
|
|
|
|
Ndb_metadata_change_monitor::Ndb_metadata_change_monitor()
|
|
: Ndb_component("Metadata") {}
|
|
|
|
Ndb_metadata_change_monitor::~Ndb_metadata_change_monitor() {}
|
|
|
|
int Ndb_metadata_change_monitor::do_init() {
|
|
log_info("Initialization");
|
|
mysql_mutex_init(PSI_INSTRUMENT_ME, &m_wait_mutex, MY_MUTEX_INIT_FAST);
|
|
mysql_cond_init(PSI_INSTRUMENT_ME, &m_wait_cond);
|
|
return 0;
|
|
}
|
|
|
|
void Ndb_metadata_change_monitor::set_check_interval(
|
|
unsigned long new_check_interval) {
|
|
log_info("Check interval value changed to %lu", new_check_interval);
|
|
mysql_mutex_lock(&m_wait_mutex);
|
|
mysql_cond_signal(&m_wait_cond);
|
|
mysql_mutex_unlock(&m_wait_mutex);
|
|
}
|
|
|
|
void Ndb_metadata_change_monitor::log_NDB_error(
|
|
const NdbError &ndb_error) const {
|
|
log_info("Got NDB error %u: %s", ndb_error.code, ndb_error.message);
|
|
}
|
|
|
|
// NOTE: Most return paths contain info level log messages even in the case of
|
|
// failing conditions. The rationale behind this is that during testing, the
|
|
// vast majority of the errors were the result of a normal MySQL server
|
|
// shutdown. Thus, we stick to info level messages here with the hope that
|
|
// "actual" errors are caught in the binlog thread during the sync
|
|
|
|
static long long g_metadata_detected_count = 0;
|
|
|
|
static void increment_metadata_detected_count() { g_metadata_detected_count++; }
|
|
|
|
static SHOW_VAR ndb_status_vars_metadata_check[] = {
|
|
{"metadata_detected_count",
|
|
reinterpret_cast<char *>(&g_metadata_detected_count), SHOW_LONGLONG,
|
|
SHOW_SCOPE_GLOBAL},
|
|
{NullS, NullS, SHOW_LONG, SHOW_SCOPE_GLOBAL}};
|
|
|
|
int show_ndb_metadata_check(THD *, SHOW_VAR *var, char *) {
|
|
var->type = SHOW_ARRAY;
|
|
var->value = reinterpret_cast<char *>(&ndb_status_vars_metadata_check);
|
|
return 0;
|
|
}
|
|
|
|
bool Ndb_metadata_change_monitor::detect_logfile_group_changes(
|
|
THD *thd, const Thd_ndb *thd_ndb) {
|
|
// Fetch list of logfile groups from NDB
|
|
NdbDictionary::Dictionary *dict = thd_ndb->ndb->getDictionary();
|
|
std::unordered_set<std::string> lfg_in_NDB;
|
|
if (!ndb_get_logfile_group_names(dict, lfg_in_NDB)) {
|
|
log_NDB_error(dict->getNdbError());
|
|
log_info("Failed to fetch logfile group names from NDB");
|
|
return false;
|
|
}
|
|
|
|
Ndb_dd_client dd_client(thd);
|
|
// Fetch list of logfile groups from DD
|
|
std::unordered_set<std::string> lfg_in_DD;
|
|
if (!dd_client.fetch_ndb_logfile_group_names(lfg_in_DD)) {
|
|
log_and_clear_thd_conditions(thd, condition_logging_level::INFO);
|
|
log_info("Failed to fetch logfile group names from DD");
|
|
return false;
|
|
}
|
|
|
|
for (const auto logfile_group_name : lfg_in_NDB) {
|
|
if (lfg_in_DD.find(logfile_group_name) == lfg_in_DD.end()) {
|
|
// Exists in NDB but not in DD
|
|
std::vector<std::string> undofile_names;
|
|
if (!ndb_get_undofile_names(dict, logfile_group_name, &undofile_names)) {
|
|
log_info(
|
|
"Failed to get undofiles assigned to logfile group '%s', skip "
|
|
"submission",
|
|
logfile_group_name.c_str());
|
|
continue;
|
|
}
|
|
// Check if the logfile group's undofiles have been created
|
|
if (undofile_names.size() == 0) {
|
|
log_info(
|
|
"No undofiles assigned to logfile group '%s' found, skip "
|
|
"submission",
|
|
logfile_group_name.c_str());
|
|
continue;
|
|
}
|
|
if (ndbcluster_binlog_check_logfile_group_async(logfile_group_name)) {
|
|
increment_metadata_detected_count();
|
|
} else {
|
|
log_info("Failed to submit logfile group '%s' for synchronization",
|
|
logfile_group_name.c_str());
|
|
}
|
|
} else {
|
|
// Exists in both NDB and DD
|
|
lfg_in_DD.erase(logfile_group_name);
|
|
}
|
|
}
|
|
|
|
for (const auto logfile_group_name : lfg_in_DD) {
|
|
// Exists in DD but not in NDB
|
|
if (ndbcluster_binlog_check_logfile_group_async(logfile_group_name)) {
|
|
increment_metadata_detected_count();
|
|
} else {
|
|
log_info("Failed to submit logfile group '%s' for synchronization",
|
|
logfile_group_name.c_str());
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Ndb_metadata_change_monitor::detect_tablespace_changes(
|
|
THD *thd, const Thd_ndb *thd_ndb) {
|
|
// Fetch list of tablespaces from NDB
|
|
NdbDictionary::Dictionary *dict = thd_ndb->ndb->getDictionary();
|
|
std::unordered_set<std::string> tablespaces_in_NDB;
|
|
if (!ndb_get_tablespace_names(dict, tablespaces_in_NDB)) {
|
|
log_NDB_error(dict->getNdbError());
|
|
log_info("Failed to fetch tablespace names from NDB");
|
|
return false;
|
|
}
|
|
|
|
Ndb_dd_client dd_client(thd);
|
|
// Fetch list of tablespaces from DD
|
|
std::unordered_set<std::string> tablespaces_in_DD;
|
|
if (!dd_client.fetch_ndb_tablespace_names(tablespaces_in_DD)) {
|
|
log_and_clear_thd_conditions(thd, condition_logging_level::INFO);
|
|
log_info("Failed to fetch tablespace names from DD");
|
|
return false;
|
|
}
|
|
|
|
for (const auto tablespace_name : tablespaces_in_NDB) {
|
|
if (tablespaces_in_DD.find(tablespace_name) == tablespaces_in_DD.end()) {
|
|
// Exists in NDB but not in DD
|
|
std::vector<std::string> datafile_names;
|
|
if (!ndb_get_datafile_names(dict, tablespace_name, &datafile_names)) {
|
|
log_info(
|
|
"Failed to get datafiles assigned to tablespace '%s', skip "
|
|
"submission",
|
|
tablespace_name.c_str());
|
|
continue;
|
|
}
|
|
// Check if the tablespace's datafiles have been created
|
|
if (datafile_names.size() == 0) {
|
|
log_info(
|
|
"No datafiles assigned to tablespace '%s' found, skip submission",
|
|
tablespace_name.c_str());
|
|
continue;
|
|
}
|
|
if (ndbcluster_binlog_check_tablespace_async(tablespace_name)) {
|
|
increment_metadata_detected_count();
|
|
} else {
|
|
log_info("Failed to submit tablespace '%s' for synchronization",
|
|
tablespace_name.c_str());
|
|
}
|
|
} else {
|
|
// Exists in both NDB and DD
|
|
tablespaces_in_DD.erase(tablespace_name);
|
|
}
|
|
}
|
|
|
|
for (const auto tablespace_name : tablespaces_in_DD) {
|
|
// Exists in DD but not in NDB
|
|
if (ndbcluster_binlog_check_tablespace_async(tablespace_name)) {
|
|
increment_metadata_detected_count();
|
|
} else {
|
|
log_info("Failed to submit tablespace '%s' for synchronization",
|
|
tablespace_name.c_str());
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool Ndb_metadata_change_monitor::detect_changes_in_schema(
|
|
THD *thd, const Thd_ndb *thd_ndb, const std::string &schema_name) {
|
|
// Fetch list of tables in NDB
|
|
NdbDictionary::Dictionary *dict = thd_ndb->ndb->getDictionary();
|
|
std::unordered_set<std::string> ndb_tables_in_NDB;
|
|
if (!ndb_get_table_names_in_schema(dict, schema_name, &ndb_tables_in_NDB)) {
|
|
log_NDB_error(dict->getNdbError());
|
|
log_info("Failed to get list of tables in schema '%s' from NDB",
|
|
schema_name.c_str());
|
|
return false;
|
|
}
|
|
|
|
// Lock the schema in DD
|
|
Ndb_dd_client dd_client(thd);
|
|
if (!dd_client.mdl_lock_schema(schema_name.c_str())) {
|
|
log_and_clear_thd_conditions(thd, condition_logging_level::INFO);
|
|
log_info("Failed to MDL lock schema '%s'", schema_name.c_str());
|
|
return false;
|
|
}
|
|
|
|
// Fetch list of tables in DD, also acquire MDL lock on the tables
|
|
std::unordered_set<std::string> ndb_tables_in_DD;
|
|
std::unordered_set<std::string> local_tables_in_DD;
|
|
if (!dd_client.get_table_names_in_schema(
|
|
schema_name.c_str(), &ndb_tables_in_DD, &local_tables_in_DD)) {
|
|
log_and_clear_thd_conditions(thd, condition_logging_level::INFO);
|
|
log_info("Failed to get list of tables in schema '%s' from DD",
|
|
schema_name.c_str());
|
|
return false;
|
|
}
|
|
|
|
// Special case when all NDB tables belonging to a schema still exist in DD
|
|
// but not in NDB
|
|
if (ndb_tables_in_NDB.empty() && !ndb_tables_in_DD.empty()) {
|
|
for (const auto ndb_table_name : ndb_tables_in_DD) {
|
|
// Exists in DD but not in NDB
|
|
if (ndbcluster_binlog_check_table_async(schema_name, ndb_table_name)) {
|
|
increment_metadata_detected_count();
|
|
} else {
|
|
log_info("Failed to submit table '%s.%s' for synchronization",
|
|
schema_name.c_str(), ndb_table_name.c_str());
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Special case when all tables belonging to a schema still exist in NDB but
|
|
// not in DD (as either NDB or shadow tables)
|
|
if (!ndb_tables_in_NDB.empty() && ndb_tables_in_DD.empty() &&
|
|
local_tables_in_DD.empty()) {
|
|
for (const auto ndb_table_name : ndb_tables_in_NDB) {
|
|
// Exists in NDB but not in DD
|
|
if (ndbcluster_binlog_check_table_async(schema_name, ndb_table_name)) {
|
|
increment_metadata_detected_count();
|
|
} else {
|
|
log_info("Failed to submit table '%s.%s' for synchronization",
|
|
schema_name.c_str(), ndb_table_name.c_str());
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
for (const auto ndb_table_name : ndb_tables_in_NDB) {
|
|
if (ndb_tables_in_DD.find(ndb_table_name) == ndb_tables_in_DD.end() &&
|
|
local_tables_in_DD.find(ndb_table_name) == local_tables_in_DD.end()) {
|
|
// Exists in NDB but not in DD
|
|
if (ndbcluster_binlog_check_table_async(schema_name, ndb_table_name)) {
|
|
increment_metadata_detected_count();
|
|
} else {
|
|
log_info("Failed to submit table '%s.%s' for synchronization",
|
|
schema_name.c_str(), ndb_table_name.c_str());
|
|
}
|
|
} else {
|
|
// Exists in both NDB and DD
|
|
ndb_tables_in_DD.erase(ndb_table_name);
|
|
}
|
|
}
|
|
|
|
for (const auto ndb_table_name : ndb_tables_in_DD) {
|
|
// Exists in DD but not in NDB
|
|
if (ndbcluster_binlog_check_table_async(schema_name, ndb_table_name)) {
|
|
increment_metadata_detected_count();
|
|
} else {
|
|
log_info("Failed to submit table '%s.%s' for synchronization",
|
|
schema_name.c_str(), ndb_table_name.c_str());
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool Ndb_metadata_change_monitor::detect_table_changes(THD *thd,
|
|
const Thd_ndb *thd_ndb) {
|
|
// Fetch list of schemas in DD
|
|
Ndb_dd_client dd_client(thd);
|
|
std::vector<std::string> schema_names;
|
|
if (!dd_client.fetch_schema_names(&schema_names)) {
|
|
log_and_clear_thd_conditions(thd, condition_logging_level::INFO);
|
|
log_info("Failed to fetch schema names from DD");
|
|
return false;
|
|
}
|
|
|
|
for (const auto name : schema_names) {
|
|
if (is_infoschema_db(name.c_str()) || is_perfschema_db(name.c_str())) {
|
|
// We do not expect user changes in these schemas so they can be skipped
|
|
continue;
|
|
}
|
|
|
|
if (!detect_changes_in_schema(thd, thd_ndb, name)) {
|
|
log_info("Failed to detect tables changes in schema '%s'", name.c_str());
|
|
if (is_stop_requested()) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// RAII style class for THD
|
|
class Thread_handle_guard {
|
|
THD *const m_thd;
|
|
Thread_handle_guard(const Thread_handle_guard &) = delete;
|
|
|
|
public:
|
|
Thread_handle_guard() : m_thd(new THD()) {
|
|
m_thd->system_thread = SYSTEM_THREAD_BACKGROUND;
|
|
m_thd->thread_stack = reinterpret_cast<const char *>(&m_thd);
|
|
m_thd->store_globals();
|
|
}
|
|
|
|
~Thread_handle_guard() {
|
|
if (m_thd) {
|
|
m_thd->release_resources();
|
|
delete m_thd;
|
|
}
|
|
}
|
|
|
|
THD *get_thd() const { return m_thd; }
|
|
};
|
|
|
|
// RAII style class for Thd_ndb
|
|
class Thd_ndb_guard {
|
|
THD *const m_thd;
|
|
Thd_ndb *const m_thd_ndb;
|
|
Thd_ndb_guard() = delete;
|
|
Thd_ndb_guard(const Thd_ndb_guard &) = delete;
|
|
|
|
public:
|
|
Thd_ndb_guard(THD *thd) : m_thd(thd), m_thd_ndb(Thd_ndb::seize(m_thd)) {
|
|
thd_set_thd_ndb(m_thd, m_thd_ndb);
|
|
}
|
|
|
|
~Thd_ndb_guard() {
|
|
Thd_ndb::release(m_thd_ndb);
|
|
thd_set_thd_ndb(m_thd, nullptr);
|
|
}
|
|
|
|
const Thd_ndb *get_thd_ndb() const { return m_thd_ndb; }
|
|
};
|
|
|
|
extern bool opt_ndb_metadata_check;
|
|
extern unsigned long opt_ndb_metadata_check_interval;
|
|
|
|
void Ndb_metadata_change_monitor::do_run() {
|
|
DBUG_TRACE;
|
|
|
|
log_info("Starting...");
|
|
|
|
if (!wait_for_server_started()) {
|
|
return;
|
|
}
|
|
|
|
Thread_handle_guard thd_guard;
|
|
THD *thd = thd_guard.get_thd();
|
|
if (thd == nullptr) {
|
|
DBUG_ASSERT(false);
|
|
log_error("Failed to allocate THD");
|
|
return;
|
|
}
|
|
|
|
for (;;) {
|
|
// Outer loop to ensure that if the connection to NDB is lost, a fresh
|
|
// connection is established before the thread continues its processing
|
|
while (!ndbcluster_is_connected(1)) {
|
|
// No connection to NDB yet. Retry until connection is established while
|
|
// checking if stop has been requested at 1 second intervals
|
|
if (is_stop_requested()) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
Thd_ndb_guard thd_ndb_guard(thd);
|
|
const Thd_ndb *thd_ndb = thd_ndb_guard.get_thd_ndb();
|
|
if (thd_ndb == nullptr) {
|
|
DBUG_ASSERT(false);
|
|
log_error("Failed to allocate Thd_ndb");
|
|
return;
|
|
}
|
|
|
|
for (;;) {
|
|
// Inner loop where each iteration represents one "lap" of the thread
|
|
while (!opt_ndb_metadata_check) {
|
|
// Sleep and then check for change of state i.e. has metadata check been
|
|
// enabled or if a stop has been requested
|
|
ndb_milli_sleep(1000);
|
|
if (is_stop_requested()) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
for (unsigned long check_interval = opt_ndb_metadata_check_interval,
|
|
elapsed_wait_time = 0;
|
|
elapsed_wait_time < check_interval && !is_stop_requested();
|
|
check_interval = opt_ndb_metadata_check_interval) {
|
|
// Determine how long the next wait interval should be using the check
|
|
// interval requested by the user and time spent waiting by the thread
|
|
// already
|
|
const auto wait_interval = check_interval - elapsed_wait_time;
|
|
struct timespec abstime;
|
|
set_timespec(&abstime, wait_interval);
|
|
mysql_mutex_lock(&m_wait_mutex);
|
|
const auto start = std::chrono::steady_clock::now();
|
|
// Can be signalled from 2 places: do_wakeup() when a stop is requested
|
|
// or set_check_interval() when the interval is changed by the user.
|
|
// If a new interval is specified by the user, then the loop logic is
|
|
// written such that if new value <= elapsed_wait time, then this loop
|
|
// exits. Else, the thread waits for the remainder of the time that it
|
|
// needs to as determined at the start of the loop using wait_interval
|
|
mysql_cond_timedwait(&m_wait_cond, &m_wait_mutex, &abstime);
|
|
const auto finish = std::chrono::steady_clock::now();
|
|
mysql_mutex_unlock(&m_wait_mutex);
|
|
|
|
// Add latest wait time to total elapsed wait time across different
|
|
// iterations of the while loop
|
|
elapsed_wait_time +=
|
|
std::chrono::duration_cast<std::chrono::duration<unsigned long>>(
|
|
finish - start)
|
|
.count();
|
|
}
|
|
|
|
if (is_stop_requested()) {
|
|
return;
|
|
}
|
|
|
|
// Check if metadata check is still enabled even after the wait
|
|
if (!opt_ndb_metadata_check) {
|
|
continue;
|
|
}
|
|
|
|
// It's pointless to try and monitor metadata changes if schema
|
|
// synchronization is ongoing
|
|
if (ndb_binlog_is_read_only()) {
|
|
log_info(
|
|
"Schema synchronization is ongoing, this iteration of metadata"
|
|
" check is skipped");
|
|
continue;
|
|
}
|
|
|
|
// Check if NDB connection is still valid
|
|
if (!ndbcluster_is_connected(1)) {
|
|
// Break out of inner loop
|
|
log_info(
|
|
"Connection to NDB was lost. Attempting to establish a new "
|
|
"connection");
|
|
break;
|
|
}
|
|
|
|
log_info("Metadata check started");
|
|
|
|
ndbcluster_binlog_validate_sync_blacklist(thd);
|
|
|
|
if (!detect_logfile_group_changes(thd, thd_ndb)) {
|
|
log_info("Failed to detect logfile group metadata changes");
|
|
}
|
|
log_info("Logfile group metadata check completed");
|
|
|
|
if (is_stop_requested()) {
|
|
return;
|
|
}
|
|
|
|
if (!detect_tablespace_changes(thd, thd_ndb)) {
|
|
log_info("Failed to detect tablespace metadata changes");
|
|
}
|
|
log_info("Tablespace metadata check completed");
|
|
|
|
if (is_stop_requested()) {
|
|
return;
|
|
}
|
|
|
|
if (!detect_table_changes(thd, thd_ndb)) {
|
|
log_info("Failed to detect table metadata changes");
|
|
}
|
|
log_info("Table metadata check completed");
|
|
log_info("Metadata check completed");
|
|
}
|
|
}
|
|
}
|
|
|
|
int Ndb_metadata_change_monitor::do_deinit() {
|
|
log_info("Deinitialization");
|
|
mysql_mutex_destroy(&m_wait_mutex);
|
|
mysql_cond_destroy(&m_wait_cond);
|
|
return 0;
|
|
}
|
|
|
|
void Ndb_metadata_change_monitor::do_wakeup() {
|
|
log_info("Wakeup");
|
|
// Signal that a stop has been requested in case the thread is in the middle
|
|
// of a wait
|
|
mysql_mutex_lock(&m_wait_mutex);
|
|
mysql_cond_signal(&m_wait_cond);
|
|
mysql_mutex_unlock(&m_wait_mutex);
|
|
}
|