From 19c3bd9843bf3b7887f1e65093a95adf0faab522 Mon Sep 17 00:00:00 2001 From: Kamil Holubicki Date: Thu, 12 Sep 2024 12:59:51 +0200 Subject: [PATCH] PXC-4433: Create file with current MySQL state https://perconadev.atlassian.net/browse/PXC-4433 server.state file containing current server state is being created in data directory if the server is started with create_server_state_file=ON option. create_server_state_file default value is OFF. --- sql/CMakeLists.txt | 1 + sql/dd/impl/bootstrap/bootstrapper.cc | 2 + sql/dd/impl/upgrade/server.cc | 8 +- sql/dd/upgrade_57/upgrade.cc | 2 + sql/mysqld.cc | 14 +- sql/mysqld.h | 1 + sql/server_status_file.cc | 191 ++++++++++++++++++++++++++ sql/server_status_file.h | 19 +++ sql/signal_handler.cc | 12 ++ sql/sys_vars.cc | 8 ++ storage/innobase/ut/ut0dbg.cc | 2 + 11 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 sql/server_status_file.cc create mode 100644 sql/server_status_file.h diff --git a/sql/CMakeLists.txt b/sql/CMakeLists.txt index 3a1e65a5d570..63a12a2e6c7c 100644 --- a/sql/CMakeLists.txt +++ b/sql/CMakeLists.txt @@ -480,6 +480,7 @@ SET(SQL_SHARED_SOURCES partition_info.cc partitioning/partition_handler.cc persisted_variable.cc + server_status_file.cc protocol_classic.cc psi_memory_key.cc psi_memory_resource.cc diff --git a/sql/dd/impl/bootstrap/bootstrapper.cc b/sql/dd/impl/bootstrap/bootstrapper.cc index 5c39c4b5098e..52254e6acd7a 100644 --- a/sql/dd/impl/bootstrap/bootstrapper.cc +++ b/sql/dd/impl/bootstrap/bootstrapper.cc @@ -71,6 +71,7 @@ #include "sql/mdl.h" #include "sql/mysqld.h" #include "sql/sd_notify.h" // sysd::notify +#include "sql/server_status_file.h" #include "sql/sql_zip_dict.h" #include "sql/thd_raii.h" #include "storage/perfschema/pfs_dd_version.h" // PFS_DD_VERSION @@ -1408,6 +1409,7 @@ bool initialize_dd_properties(THD *thd) { if (bootstrap::DD_bootstrap_ctx::instance().is_dd_upgrade()) { LogErr(SYSTEM_LEVEL, ER_DD_UPGRADE, actual_dd_version, dd::DD_VERSION); sysd::notify("STATUS=Data Dictionary upgrade in progress\n"); + Server_status_file::set_status(Server_status_file::Status::UPGRADING); } if (bootstrap::DD_bootstrap_ctx::instance().is_server_upgrade()) { // This condition is hit only if upgrade has been skipped before diff --git a/sql/dd/impl/upgrade/server.cc b/sql/dd/impl/upgrade/server.cc index 4d96b16bc3a3..a07a996bf51b 100644 --- a/sql/dd/impl/upgrade/server.cc +++ b/sql/dd/impl/upgrade/server.cc @@ -55,8 +55,9 @@ #include "sql/dd/types/tablespace.h" #include "sql/dd_sp.h" // prepare_sp_chistics_from_dd_routine #include "sql/sd_notify.h" // sysd::notify -#include "sql/sp.h" // Stored_routine_creation_ctx -#include "sql/sp_head.h" // sp_head +#include "sql/server_status_file.h" +#include "sql/sp.h" // Stored_routine_creation_ctx +#include "sql/sp_head.h" // sp_head #include "sql/sql_base.h" #include "sql/sql_prepare.h" #include "sql/strfunc.h" @@ -977,11 +978,14 @@ bool upgrade_system_schemas(THD *thd) { LogErr(SYSTEM_LEVEL, ER_SERVER_DOWNGRADE_STATUS, server_version, MYSQL_VERSION_ID, "started"); sysd::notify("STATUS=Server downgrade in progress\n"); + Server_status_file::set_status(Server_status_file::Status::DOWNGRADING); + /* purecov: end */ } else { LogErr(SYSTEM_LEVEL, ER_SERVER_UPGRADE_STATUS, server_version, MYSQL_VERSION_ID, "started"); sysd::notify("STATUS=Server upgrade in progress\n"); + Server_status_file::set_status(Server_status_file::Status::UPGRADING); } bootstrap_error_handler.set_log_error(false); diff --git a/sql/dd/upgrade_57/upgrade.cc b/sql/dd/upgrade_57/upgrade.cc index f020e0cd2e1f..e936f8866bbd 100644 --- a/sql/dd/upgrade_57/upgrade.cc +++ b/sql/dd/upgrade_57/upgrade.cc @@ -74,6 +74,7 @@ #include "sql/log.h" // sql_print_warning #include "sql/mysqld.h" // key_file_sdi #include "sql/sd_notify.h" // sysd::notify +#include "sql/server_status_file.h" #include "sql/sql_class.h" // THD #include "sql/sql_list.h" #include "sql/sql_plugin.h" @@ -904,6 +905,7 @@ bool do_pre_checks_and_initialize_dd(THD *thd) { */ LogErr(SYSTEM_LEVEL, ER_DD_UPGRADE_START); sysd::notify("STATUS=Data Dictionary upgrade from MySQL 5.7 in progress\n"); + Server_status_file::set_status(Server_status_file::Status::UPGRADING); } /* diff --git a/sql/mysqld.cc b/sql/mysqld.cc index 5896fd00da89..3e0b3546fd51 100644 --- a/sql/mysqld.cc +++ b/sql/mysqld.cc @@ -816,6 +816,7 @@ MySQL clients support the protocol: #include "sql/range_optimizer/range_optimizer.h" // range_optimizer_init #include "sql/replication.h" // thd_enter_cond #include "sql/resourcegroups/resource_group_mgr.h" // init, post_init +#include "sql/server_status_file.h" #include "sql/sql_profile.h" #ifdef _WIN32 #include "sql/restart_monitor_win.h" @@ -1448,6 +1449,8 @@ bool avoid_temporal_upgrade; bool persisted_globals_load = true; +bool create_server_state_file = false; + bool opt_keyring_operations = true; bool opt_table_encryption_privilege_check = false; @@ -2773,6 +2776,7 @@ static void unireg_abort(int exit_code) { #endif /* WITH_WSREP */ DBUG_TRACE; + Server_status_file::set_status(Server_status_file::Status::STOPPING); if (errno) { sysd::notify("ERRNO=", errno, "\n"); } @@ -2899,6 +2903,8 @@ static void mysqld_exit(int exit_code) { close_service_status_pipe_in_mysqld(); #endif // _WIN32 + Server_status_file::set_status(Server_status_file::Status::STOPPED); + exit(exit_code); /* purecov: inspected */ } @@ -8424,6 +8430,9 @@ int mysqld_main(int argc, char **argv) strmake(mysql_real_data_home, get_relative_path(MYSQL_DATADIR), sizeof(mysql_real_data_home) - 1); + Server_status_file::init(&argc, &argv); + Server_status_file::set_status(Server_status_file::Status::INITIALIZING); + /* Must be initialized early for comparison of options name */ system_charset_info = &my_charset_utf8mb3_general_ci; @@ -9468,9 +9477,12 @@ int mysqld_main(int argc, char **argv) } mysqld_socket_acceptor->check_and_spawn_admin_connection_handler_thread(); - mysqld_socket_acceptor->connection_event_loop(); + Server_status_file::set_status(Server_status_file::Status::READY); + mysqld_socket_acceptor->connection_event_loop(); // KH: end of limbo state #endif /* _WIN32 */ server_operational_state = SERVER_SHUTTING_DOWN; + Server_status_file::set_status(Server_status_file::Status::STOPPING); + sysd::notify("STOPPING=1\nSTATUS=Server shutdown in progress\n"); DBUG_PRINT("info", ("No longer listening for incoming connections")); diff --git a/sql/mysqld.h b/sql/mysqld.h index f13044a6f00b..c754cc4c5257 100644 --- a/sql/mysqld.h +++ b/sql/mysqld.h @@ -441,6 +441,7 @@ extern ulonglong utility_user_privileges; extern char *utility_user_dynamic_privileges; extern bool persisted_globals_load; +extern bool create_server_state_file; extern bool opt_keyring_operations; extern bool opt_table_encryption_privilege_check; extern char *opt_keyring_migration_user; diff --git a/sql/server_status_file.cc b/sql/server_status_file.cc new file mode 100644 index 000000000000..568b15d57687 --- /dev/null +++ b/sql/server_status_file.cc @@ -0,0 +1,191 @@ +#include "server_status_file.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "my_alloc.h" +#include "my_getopt.h" +#include "my_io.h" + +#include "mysqld_error.h" + +#include +#include +#include "sql/mysqld.h" +#include "sql/raii/sentry.h" // raii::Sentry + +namespace fs = std::filesystem; +using namespace std::chrono_literals; + +namespace Server_status_file { + +static constexpr auto path_separator = + std::filesystem::path::preferred_separator; +static const std::string m_server_state_filename("server.state"); +static bool m_create_server_state_file = false; +static std::string m_server_state_file_path; + +static std::map status2string{ + {Status::INITIALIZING, "INITIALIZING"}, + {Status::DOWNGRADING, "DOWNGRADING"}, + {Status::UPGRADING, "UPGRADING"}, + {Status::READY, "READY"}, + {Status::STOPPING, "STOPPING"}, + {Status::STOPPED, "STOPPED"}, + {Status::UNKNOWN, "UNKNOWN"}}; + +void log(const char *msg) { + /* It is not nice to call from internals, but if we are just + deinitializing the server, log may be disabled already. */ + + if (log_builtins_started()) { + LogErr(WARNING_LEVEL, ER_LOG_PRINTF_MSG, msg); + } +} + +static int delete_status_file() { + return unlink(m_server_state_file_path.c_str()); +} + +/* Try to acquire desired lock on file within a specified time. + Returns 0 in case of success, -1 otherwise. */ +static int flockt(int fd, int operation, std::chrono::milliseconds timeout) { + static const std::chrono::milliseconds loop_wait_time = 500ms; + std::chrono::milliseconds wait_total = 0ms; + + while (flock(fd, operation | LOCK_NB) == -1) { + if (errno == EWOULDBLOCK) { + if (wait_total >= timeout) { + // failed to lock + return -1; + } + std::this_thread::sleep_for(loop_wait_time); + wait_total += loop_wait_time; + } else { + return -1; + } + } + return 0; +} + +static const std::string &get_status_string(Status status) { + if (status2string.find(status) != status2string.end()) { + return status2string[status]; + } + return status2string[Status::UNKNOWN]; +} + +/* Store string representation of status to the server.status file. + If the file does not exist, it is created. + If the file exists, try to acquire exclusive lock with 5s timeout. + Regardless of the lock being acquired or not store the status in file. */ +void set_status(Status status) { + if (!m_create_server_state_file) { + return; + } + + int fd = open(m_server_state_file_path.c_str(), O_WRONLY | O_CREAT, 0644); + if (fd == -1) { + log("Cannot open server.status file"); + return; + } + // close() will release the file lock if acquired + raii::Sentry file_close{[fd] { close(fd); }}; + + /* Let's try acquire exclusive lock on server.status file. + We will give up after a short time. It is to avoid the case when someone + acquires a shared lock on this file without releasing it. That would block + the server forever. + We will update the status file anyway. In worst case the reader will + receive garbage if it reads in the middle of the write, but it is up to + reader to not lock for a long time. */ + int res = flockt(fd, LOCK_EX, 5000ms); + if (res) { + log("Failed to acquire exclusive lock on server.status file. Reporting " + "status anyway."); + } + + // truncate only after acquiring the lock + res = ftruncate(fd, 0); + if (res) { + log("Failed to truncate server.status file. Trying to delete."); + if (delete_status_file()) { + log("Failed to delete server.status file"); + return; + } + } + + const std::string &status_str = get_status_string(status); + size_t s = write(fd, status_str.c_str(), status_str.length()); + if (s != status_str.length()) { + log("Error writing data to server.status file"); + } + + // file is closed by file_close Sentry +} + +int init(int *argc, char ***argv) { + int temp_argc = *argc; + MEM_ROOT alloc{PSI_NOT_INSTRUMENTED, 512}; + char *ptr, **res, *datadir = nullptr; + char local_datadir_buffer[FN_REFLEN] = {0}; + + /* mysql_real_data_home_ptr is initialized in init_common_variables(). + We want to store the status file much earlier. Similar code exists in + Persisted_variables_cache::init() */ + my_option persist_options[] = { + {"create_server_state_file", 0, "", &m_create_server_state_file, + &m_create_server_state_file, nullptr, GET_BOOL, OPT_ARG, 1, 0, 0, + nullptr, 0, nullptr}, + {"datadir", 0, "", &datadir, nullptr, nullptr, GET_STR, OPT_ARG, 0, 0, 0, + nullptr, 0, nullptr}, + {nullptr, 0, nullptr, nullptr, nullptr, nullptr, GET_NO_ARG, NO_ARG, 0, 0, + 0, nullptr, 0, nullptr}}; + + /* create temporary args list and pass it to handle_options */ + if (!(ptr = + (char *)alloc.Alloc(sizeof(alloc) + (*argc + 1) * sizeof(char *)))) + return 1; + memset(ptr, 0, (sizeof(char *) * (*argc + 1))); + res = (char **)(ptr); + memcpy((uchar *)res, (char *)(*argv), (*argc) * sizeof(char *)); + + my_getopt_skip_unknown = true; + if (my_handle_options(&temp_argc, &res, persist_options, nullptr, nullptr, + true)) { + alloc.Clear(); + return 1; + } + my_getopt_skip_unknown = false; + alloc.Clear(); + + if (!datadir) { + // mysql_real_data_home must be initialized at this point + assert(mysql_real_data_home[0]); + /* + mysql_home_ptr should also be initialized at this point. + See calculate_mysql_home_from_my_progname() for details + */ + assert(mysql_home_ptr && mysql_home_ptr[0]); + convert_dirname(local_datadir_buffer, mysql_real_data_home, NullS); + (void)my_load_path(local_datadir_buffer, local_datadir_buffer, + mysql_home_ptr); + datadir = local_datadir_buffer; + } + + m_server_state_file_path = + std::string(datadir) + path_separator + m_server_state_filename; + + // Ignore the error. Server starts and it is possible there is no + // server.status file + delete_status_file(); + + return 0; +} + +} // namespace Server_status_file \ No newline at end of file diff --git a/sql/server_status_file.h b/sql/server_status_file.h new file mode 100644 index 000000000000..5a0b10ccabbc --- /dev/null +++ b/sql/server_status_file.h @@ -0,0 +1,19 @@ +#ifndef SERVER_STATUS_FILE_INCLUDED +#define SERVER_STATUS_FILE_INCLUDED + +namespace Server_status_file { +enum Status { + INITIALIZING, + DOWNGRADING, + UPGRADING, + READY, + STOPPING, + STOPPED, + UNKNOWN +}; + +int init(int *argc, char ***argv); +void set_status(Status status); +} // namespace Server_status_file + +#endif /* SERVER_STATUS_FILE_INCLUDED */ \ No newline at end of file diff --git a/sql/signal_handler.cc b/sql/signal_handler.cc index ceca808eef67..7a6571622b4e 100644 --- a/sql/signal_handler.cc +++ b/sql/signal_handler.cc @@ -41,6 +41,7 @@ #include "my_time.h" #include "mysql_version.h" #include "sql/mysqld.h" +#include "sql/server_status_file.h" #include "sql/sql_class.h" #include "sql/sql_const.h" @@ -227,9 +228,11 @@ void print_fatal_signal(int sig) { extern "C" void handle_fatal_signal(int sig) { if (s_handler_being_processed) { my_safe_printf_stderr("Fatal " SIGNAL_FMT " while backtracing\n", sig); + Server_status_file::set_status(Server_status_file::Status::STOPPED); _exit(MYSQLD_FAILURE_EXIT); /* Quit without running destructors */ } + Server_status_file::set_status(Server_status_file::Status::STOPPING); s_handler_being_processed = true; #ifdef WITH_WSREP @@ -246,6 +249,10 @@ extern "C" void handle_fatal_signal(int sig) { buffered_error_log.write_to_disk(); + // This is the last chance to report in the file. + // Below coredump will exit the process + Server_status_file::set_status(Server_status_file::Status::STOPPED); + if ((test_flags & TEST_CORE_ON_SIGNAL) != 0) { #if HAVE_LIBCOREDUMPER if (opt_libcoredumper) { @@ -290,6 +297,9 @@ void my_server_abort() { static std::atomic_bool abort_processing{false}; /* Broadcast that this thread wants to print the signal info. */ aborts_pending++; + + Server_status_file::set_status(Server_status_file::Status::STOPPING); + /* Wait for the exclusive right to print the signal info. This assures the output is not interleaved. @@ -319,6 +329,8 @@ void my_server_abort() { while (abort_processing.exchange(true)) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } + + Server_status_file::set_status(Server_status_file::Status::STOPPED); abort(); } /* diff --git a/sql/sys_vars.cc b/sql/sys_vars.cc index feeb3afb0f23..4669db91ebe5 100644 --- a/sql/sys_vars.cc +++ b/sql/sys_vars.cc @@ -7868,6 +7868,14 @@ static Sys_var_bool Sys_persisted_globals_load( DEFAULT(true), NO_MUTEX_GUARD, NOT_IN_BINLOG, ON_CHECK(nullptr), ON_UPDATE(nullptr)); +static Sys_var_bool Sys_create_server_state_file( + "create_server_state_file", + "When this option is enabled, server reports its state in server.state " + "file located in datadir.", + READ_ONLY NON_PERSIST GLOBAL_VAR(create_server_state_file), + CMD_LINE(OPT_ARG), DEFAULT(false), NO_MUTEX_GUARD, NOT_IN_BINLOG, + ON_CHECK(nullptr), ON_UPDATE(nullptr)); + static bool sysvar_check_authid_string(sys_var *, THD *thd, set_var *var) { /* Since mandatory_roles is similar to a GRANT role statement without a diff --git a/storage/innobase/ut/ut0dbg.cc b/storage/innobase/ut/ut0dbg.cc index 421a260829e4..af2634182513 100644 --- a/storage/innobase/ut/ut0dbg.cc +++ b/storage/innobase/ut/ut0dbg.cc @@ -40,6 +40,7 @@ this program; if not, write to the Free Software Foundation, Inc., #include "sql/log.h" #endif /* !UNIV_HOTBACKUP */ +#include "sql/server_status_file.h" #include "ut0dbg.h" static std::function assert_callback; @@ -78,6 +79,7 @@ void ut_set_assert_callback(std::function &callback) { to_string(std::this_thread::get_id()).c_str()); #endif /* !UNIV_HOTBACKUP */ + Server_status_file::set_status(Server_status_file::Status::STOPPING); fputs( "InnoDB: We intentionally generate a memory trap.\n" "InnoDB: Submit a detailed bug report"