/* * Copyright (c) 2016 MariaDB Corporation Ab * * Use of this software is governed by the Business Source License included * in the LICENSE.TXT file and at www.mariadb.com/bsl11. * * Change Date: 2023-11-12 * * On the date above, in accordance with the Business Source License, use * of this software will be governed by version 2 or later of the General * Public License. */ /** * @file blr_slave.c - contains code for the router to slave communication * * The binlog router is designed to be used in replication environments to * increase the replication fanout of a master server. It provides a transparant * mechanism to read the binlog entries for multiple slaves while requiring * only a single connection to the actual master to support the slaves. * * The current prototype implement is designed to support MySQL 5.6 and has * a number of limitations. This prototype is merely a proof of concept and * should not be considered production ready. */ #include "blr.hh" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using std::string; using std::vector; /** * This struct is used by sqlite3_exec callback routine * for SHOW BINARY LOGS. * * It stores the next row sequence number, * the last binlog file name read from gtid_maps storage * and the connected client DCB. */ typedef struct { int seq_no; /* Output sequence in result set */ char* last_file; /* Last binlog file found in GTID repo */ const char* binlogdir; /* Binlog files cache dir */ DCB* client; /* Connected client DCB */ bool use_tree; /* Binlog structure type */ size_t n_files; /* How many files */ uint64_t rowid; /* ROWID of router current file*/ } BINARY_LOG_DATA_RESULT; /** Slave file read EOF handling */ typedef enum { SLAVE_EOF_ROTATE = 0, SLAVE_EOF_WARNING, SLAVE_EOF_ERROR } slave_eof_action_t; static char* get_next_token(char* str, const char* delim, char** saveptr); extern int load_mysql_users(Listener* listener); extern void blr_master_close(ROUTER_INSTANCE* router); extern int blr_file_new_binlog(ROUTER_INSTANCE* router, char* file); extern int blr_file_write_master_config(ROUTER_INSTANCE* router, char* error); extern char* blr_extract_column(GWBUF* buf, int col); extern uint32_t extract_field(uint8_t* src, int bits); void blr_extract_header(register uint8_t* ptr, register REP_HEADER* hdr); int blr_file_get_next_binlogname(ROUTER_INSTANCE* router); static void encode_value(unsigned char* data, unsigned int value, int len); static int blr_slave_query(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, GWBUF* queue); static int blr_slave_replay(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, GWBUF* master); static void blr_slave_send_error(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, const char* msg); static int blr_slave_send_timestamp(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave); static int blr_slave_register(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, GWBUF* queue); static int blr_slave_binlog_dump(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, GWBUF* queue); int blr_slave_catchup(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, bool large); uint8_t* blr_build_header(GWBUF* pkt, REP_HEADER* hdr); int blr_slave_callback(DCB* dcb, DCB_REASON reason, void* data); static int blr_slave_fake_rotate(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, BLFILE** filep, const char* new_file); static uint32_t blr_slave_send_fde(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, GWBUF* fde); static int blr_slave_send_maxscale_version(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave); static int blr_slave_send_server_id(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave); static int blr_slave_send_maxscale_variables(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave); static int blr_slave_send_master_status(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave); static int blr_slave_send_slave_status(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, bool all_slaves); static int blr_slave_send_slave_hosts(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave); static int blr_slave_send_fieldcount(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, int count); static int blr_slave_send_columndef(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, const char* name, int type, int len, uint8_t seqno); static int blr_slave_send_eof(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, int seqno); static int blr_slave_send_disconnected_server(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, int server_id, int found); static int blr_slave_disconnect_all(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave); static int blr_slave_disconnect_server(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, int server_id); static int blr_slave_send_ok(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave); static int blr_stop_slave(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave); static int blr_start_slave(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave); static void blr_slave_send_error_packet(ROUTER_SLAVE* slave, const char* msg, unsigned int err_num, const char* status); static int blr_apply_change_master(ROUTER_INSTANCE* router, int index, const ChangeMasterConfig& new_config, char* error); static int blr_handle_change_master(ROUTER_INSTANCE* router, char* command, char* error); static int blr_set_master_hostname(ROUTER_INSTANCE* router, const char* hostname); static int blr_set_master_hostname(ROUTER_INSTANCE* router, const std::string& hostname); static int blr_set_master_port(ROUTER_INSTANCE* router, int port); static char* blr_set_master_logfile(ROUTER_INSTANCE* router, const char* filename, char* error); static void blr_master_get_config(ROUTER_INSTANCE* router, MasterServerConfig* current_master); static void blr_master_restore_config(ROUTER_INSTANCE* router, const MasterServerConfig& current_master); static void blr_master_set_empty_config(ROUTER_INSTANCE* router); static void blr_master_apply_config(ROUTER_INSTANCE* router, const MasterServerConfig& prev_master); static int blr_slave_send_ok_message(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, char* message); static bool blr_get_parsed_command_value(char* input, std::string* output); static std::string* blr_validate_change_master_option(const char* option, ChangeMasterOptions* config); static int blr_set_master_user(ROUTER_INSTANCE* router, const char* user); static int blr_set_master_user(ROUTER_INSTANCE* router, const std::string& user); static int blr_set_master_password(ROUTER_INSTANCE* router, const char* password); static int blr_set_master_password(ROUTER_INSTANCE* router, const std::string& password); static int blr_parse_change_master_command(char* input, char* error_string, ChangeMasterOptions* config); static int blr_handle_change_master_token(char* input, char* error, ChangeMasterOptions* config); static int blr_slave_send_var_value(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, const char* variable, const char* value, int column_type); static int blr_slave_send_variable(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, const char* variable, const char* value, int column_type); static int blr_slave_send_columndef_with_info_schema(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, const char* name, int type, int len, uint8_t seqno); int blr_test_parse_change_master_command(char* input, char* error_string, ChangeMasterOptions* config); char* blr_test_set_master_logfile(ROUTER_INSTANCE* router, const char* filename, char* error); static int blr_slave_handle_variables(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, char* stmt); static int blr_slave_send_warning_message(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, const char* message); static int blr_slave_show_warnings(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave); static int blr_slave_send_status_variable(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, const char* variable, const char* value, int column_type); static int blr_slave_handle_status_variables(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, char* stmt); static int blr_slave_send_columndef_with_status_schema(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, const char* name, int type, int len, uint8_t seqno); static bool blr_send_slave_heartbeat(void* inst); static void blr_slave_send_heartbeat(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave); static int blr_set_master_ssl(ROUTER_INSTANCE* router, const ChangeMasterConfig& config, char* error_message); static int blr_slave_read_ste(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, uint32_t fde_end_pos); static GWBUF* blr_slave_read_fde(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave); static bool blr_handle_simple_select_stmt(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, char* select_stmt); static GWBUF* blr_build_fake_rotate_event(ROUTER_SLAVE* slave, unsigned long pos, const char* filename, unsigned long serverid); static int blr_send_connect_fake_rotate(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave); static bool blr_slave_gtid_request(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, bool req_file, unsigned long req_pos); static int blr_send_fake_gtid_list(ROUTER_SLAVE* slave, const char* gtid, uint32_t serverid); static bool blr_handle_maxwell_stmt(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, const char* maxwell_stmt); static bool blr_handle_show_stmt(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, char* show_stmt); static bool blr_handle_set_stmt(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, char* set_stmt); static bool blr_handle_admin_stmt(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, char* admin_stmt, char* admin_options); extern unsigned int blr_file_get_next_seqno(const char* filename); extern uint32_t blr_slave_get_file_size(const char* filename); static void blr_slave_skip_empty_files(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave); static inline void blr_get_file_fullpath(const char* binlog_file, const char* root_dir, char* full_path, const char* f_prefix); static int blr_show_binary_logs(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, const char* extra_data); extern bool blr_parse_gtid(const char* gtid, MARIADB_GTID_ELEMS* info); static int binary_logs_select_cb(void* data, int cols, char** values, char** names); static GWBUF* blr_create_result_row(const char* name, const char* value, int seq_no); static int blr_slave_send_id_ro(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave); static bool blr_handle_complex_select(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, const char* col1, const char* coln); extern bool blr_is_current_binlog(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave); extern bool blr_compare_binlogs(const ROUTER_INSTANCE* router, const MARIADB_GTID_ELEMS* info, const char* r_file, const char* s_file); static bool blr_purge_binary_logs(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, char* purge_stmt); static int binary_logs_find_file_cb(void* data, int cols, char** values, char** names); static void blr_log_config_changes(ROUTER_INSTANCE* router, const MasterServerConfig& current_master, const ChangeMasterConfig& change_master); extern void blr_log_disabled_heartbeat(const ROUTER_INSTANCE* inst); extern void blr_close_master_in_main(void* data); static bool blr_check_connecting_slave(const ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, enum blr_slave_check check); static void blr_abort_change_master(ROUTER_INSTANCE* router, const MasterServerConfig& current_master, const char* error); static void blr_slave_abort_dump_request(ROUTER_SLAVE* slave, const char* errmsg); static bool blr_binlog_change_check(const ROUTER_INSTANCE* router, const ChangeMasterConfig& new_config, char* error); static bool blr_change_binlog_name(ROUTER_INSTANCE* router, const char* log_file, char** new_logfile, char* error); static bool blr_change_binlog_name(ROUTER_INSTANCE* router, const std::string& log_file, char** new_logfile, char* error); static bool blr_apply_changes(ROUTER_INSTANCE* router, const ChangeMasterConfig& new_config, char* new_logfile, char* error); static void blr_slave_info_save(const MARIADB_GTID_INFO* info, MARIADB_GTID_INFO* save_info, char* save_prefix); static void blr_slave_log_next_file_action(const ROUTER_INSTANCE* router, const ROUTER_SLAVE* slave, const char* c_prefix, const char* next_file, slave_eof_action_t log_action); /** * Process a request packet from the slave server. * * The router can handle a limited subset of requests from the slave, these * include a subset of general SQL queries, a slave registeration command and * the binlog dump command. * * The strategy for responding to these commands is to use caches responses * for the the same commands that have previously been made to the real master * if this is possible, if it is not then the router itself will synthesize a * response. * * @param router The router instance: the master for this replication chain * @param slave The slave specific data * @param queue The incoming request packet */ int blr_slave_request(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, GWBUF* queue) { int rv = 0; if (slave->state < 0 || slave->state > BLRS_MAXSTATE) { MXS_ERROR("Invalid slave state machine state (%d) for binlog router.", slave->state); gwbuf_free(queue); return 0; } slave->stats.n_requests++; switch (MYSQL_COMMAND(queue)) { case COM_QUERY: slave->stats.n_queries++; rv = blr_slave_query(router, slave, queue); break; case COM_REGISTER_SLAVE: /* Continue with slave registration */ rv = blr_slave_register(router, slave, queue); break; case COM_BINLOG_DUMP: /* Check whether binlog server can accept slave requests */ if (!blr_check_connecting_slave(router, slave, BLR_SLAVE_CONNECTING) || /* Check whether connecting slaves can be only MariaDB 10 ones */ !blr_check_connecting_slave(router, slave, BLR_SLAVE_IS_MARIADB10) || /** * If MariaDB 10 GTID master replication is set * only MariaDB 10 GTID slaves can continue the registration. */ !blr_check_connecting_slave(router, slave, BLR_SLAVE_HAS_MARIADB10_GTID)) { dcb_close(slave->dcb); return 1; } /* Request now the binlog records */ rv = blr_slave_binlog_dump(router, slave, queue); /* Check whether to add the heartbeat check for this slave */ if (rv && slave->state == BLRS_DUMPING && router->send_slave_heartbeat && slave->heartbeat > 0 && !router->slave_heartbeat_task_active) { router->slave_heartbeat_task_active = true; char task_name[BLRM_TASK_NAME_LEN + 1] = ""; snprintf(task_name, BLRM_TASK_NAME_LEN, "%s slaves heartbeat send", router->service->name()); /* Add slave heartbeat check task with 1 second frequency */ hktask_add(task_name, blr_send_slave_heartbeat, router, 1); } break; case COM_STATISTICS: rv = blr_statistics(router, slave, queue); break; case COM_PING: rv = blr_ping(router, slave, queue); break; case COM_QUIT: MXS_DEBUG("COM_QUIT received from slave with server_id %d", slave->serverid); rv = 1; break; default: blr_send_custom_error(slave->dcb, 1, 0, "You have an error in your SQL syntax; Check the " "syntax the MaxScale binlog router accepts.", "42000", 1064); MXS_ERROR("Unexpected MySQL Command (%d) received from slave", MYSQL_COMMAND(queue)); break; } return rv; } /* * Return a pointer to where the actual SQL query starts, skipping initial * comments and whitespace characters, if there are any. */ const char* blr_skip_leading_sql_comments(const char* sql_query) { const char* p = sql_query; while (*p) { if (*p == '/' && p[1] == '*') { ++p; // skip '/' ++p; // skip '*' while (*p) { if (*p == '*' && p[1] == '/') { ++p; // skip '*' ++p; // skip '/' break; } else { ++p; } } } else if (isspace(*p)) { ++p; } else { return p; } } return p; } /** * Handle a query from the slave. This is expected to be one of the "standard" * queries we expect as part of the registraton process. Most of these can * be dealt with by replying the stored responses we got from the master * when MaxScale registered as a slave. The exception to the rule is the * request to obtain the current timestamp value of the server. * * The original set added for the registration process has been enhanced in * order to support some commands that are useful for monitoring the binlog * router. * * 16 select statements are currently supported: * SELECT UNIX_TIMESTAMP(); * SELECT @master_binlog_checksum * SELECT @@GLOBAL.GTID_MODE * SELECT VERSION() * SELECT 1 * SELECT @@version_comment limit 1 * SELECT @@hostname * SELECT @@max_allowed_packet * SELECT @@maxscale_version * SELECT @@[GLOBAL.]server_id * SELECT @@version * SELECT @@[GLOBAL.]server_uuid * SELECT USER() * SELECT @@GLOBAL.gtid_domain_id * SELECT @@[GLOBAL].gtid_current_pos * SELECT @@[global.]server_id, @@[global.]read_only * * 10 show commands are supported: * SHOW [GLOBAL] VARIABLES LIKE 'SERVER_ID' * SHOW [GLOBAL] VARIABLES LIKE 'SERVER_UUID' * SHOW [GLOBAL] VARIABLES LIKE 'MAXSCALE%' * SHOW SLAVE STATUS * SHOW MASTER STATUS * SHOW SLAVE HOSTS * SHOW WARNINGS * SHOW [GLOBAL] STATUS LIKE 'Uptime' * SHOW [GLOBAL] STATUS LIKE 'slave_received_heartbeats' * SHOW BINARY LOGS * * 13 set commands are supported: * SET @master_binlog_checksum = @@global.binlog_checksum * SET @master_heartbeat_period=... * SET @slave_slave_uuid=... * SET NAMES latin1 * SET NAMES utf8 * SET NAMES XXX * SET mariadb_slave_capability=... * SET autocommit= * SET @@session.autocommit= * SET @slave_connect_state= * SET @slave_gtid_strict_mode= * SET @slave_gtid_ignore_duplicates= * SET SQL_MODE='' * * 4 administrative commands are supported: * STOP SLAVE * START SLAVE * CHANGE MASTER TO * RESET SLAVE * * @param router The router instance * @param slave The slave specific data * @param queue The incoming request packet * @return Non-zero if data has been sent */ static int blr_slave_query(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, GWBUF* queue) { char* qtext, * query_text; const char* sep = " ,="; char* word, * brkb; int query_len; char* ptr; bool unexpected = true; qtext = (char*)GWBUF_DATA(queue); query_len = extract_field((uint8_t*)qtext, 24) - 1; qtext += MYSQL_HEADER_LEN + 1; // Skip header and first byte of the payload query_text = strndup(qtext, query_len); /* Don't log the full statement containg 'password', just trucate it */ ptr = strcasestr(query_text, "password"); if (ptr != NULL) { char* new_text = MXS_STRDUP_A(query_text); int trucate_at = (ptr - query_text); if (trucate_at > 0) { if ((trucate_at + 3) <= (int)strlen(new_text)) { int i; for (i = 0; i < 3; i++) { new_text[trucate_at + i] = '.'; } new_text[trucate_at + 3] = '\0'; } else { new_text[trucate_at] = '\0'; } } MXS_INFO("Execute statement (truncated, it contains password)" " from the slave '%s'", new_text); MXS_FREE(new_text); } else { MXS_INFO("Execute statement from the slave '%s'", query_text); } /* * Implement a very rudimental "parsing" of the query text by extarcting the * words from the statement and matchng them against the subset of queries we * are expecting from the slave. We already have responses to these commands, * except for the select of UNIX_TIMESTAMP(), that we have saved from MaxScale's * own interaction with the real master. We simply replay these saved responses * to the slave. */ /* - 1 - Check and handle possible Maxwell input statement */ if (blr_handle_maxwell_stmt(router, slave, query_text)) { MXS_FREE(query_text); return 1; } /* - 2 - Handle SELECT, SET, SHOW and Admin commands */ else if ((word = strtok_r(query_text, sep, &brkb)) == NULL) { MXS_ERROR("%s: Incomplete query.", router->service->name()); } else if (strcasecmp(word, "SELECT") == 0) { /* Handle SELECT */ if ((word = strtok_r(NULL, sep, &brkb)) == NULL) { MXS_ERROR("%s: Incomplete select query.", router->service->name()); } else { if (brkb && strlen(brkb) && blr_handle_complex_select(router, slave, word, brkb)) { MXS_FREE(query_text); return 1; } if (blr_handle_simple_select_stmt(router, slave, word)) { MXS_FREE(query_text); return 1; } else { /* Handle a special case */ unexpected = strcasestr(word, "binlog_gtid_pos") == NULL; } } } else if (strcasecmp(word, "SHOW") == 0) { /* Handle SHOW */ if (blr_handle_show_stmt(router, slave, brkb)) { MXS_FREE(query_text); return 1; } } else if (strcasecmp(query_text, "SET") == 0) { /* Handle SET */ if (blr_handle_set_stmt(router, slave, brkb)) { MXS_FREE(query_text); return 1; } } else { /* Handle ADMIN commands */ if (blr_handle_admin_stmt(router, slave, word, brkb)) { MXS_FREE(query_text); return 1; } } /* - 3 - Handle unsuppored statements from client */ MXS_FREE(query_text); query_text = strndup(qtext, query_len); if (unexpected) { MXS_ERROR("Unexpected query from '%s'@'%s': %s", slave->dcb->user, slave->dcb->remote, query_text); } else { MXS_INFO("Unexpected query from '%s'@'%s', possibly a 10.1 slave: %s", slave->dcb->user, slave->dcb->remote, query_text); } MXS_FREE(query_text); blr_slave_send_error(router, slave, "You have an error in your SQL syntax; Check the syntax " "the MaxScale binlog router accepts."); return 1; } /** * Send a reply to a command we have received from the slave. The reply itself * is merely a copy of a previous message we received from the master when we * registered as a slave. Hence we just replay this saved reply. * * @param router The binlog router instance * @param slave The slave server to which we are sending the response * @param master The saved master response * @return Non-zero if data was sent */ static int blr_slave_replay(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, GWBUF* master) { if (router->master_state == BLRM_UNCONFIGURED) { return blr_slave_send_ok(router, slave); } if (!master) { return 0; } /** * Note: The following manipulation of the buffers bypasses the thread-local checks that are present * to prevent cross-thread usage of buffers. As the binlogrouter doesn't care about that, we have to * avoid the checks in order to prevent debug assertions. This is definitely not something that should * be done and a better solution would be to just store the data inside a vector and protect access with * a lock. */ size_t len = 0; for (GWBUF* b = master; b; b = b->next) { len += GWBUF_LENGTH(b); } GWBUF* clone = gwbuf_alloc(len); if (clone) { uint8_t* data = GWBUF_DATA(clone); for (GWBUF* b = master; b; b = b->next) { memcpy(data, GWBUF_DATA(b), GWBUF_LENGTH(b)); data += GWBUF_LENGTH(b); } return MXS_SESSION_ROUTE_REPLY(slave->dcb->session, clone); } else { MXS_ERROR("Failed to clone server response to send to slave."); return 0; } } /** * Construct an error response * * @param router The router instance * @param slave The slave server instance * @param msg The error message to send */ static void blr_slave_send_error(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, const char* msg) { GWBUF* pkt; unsigned char* data; int len; if ((pkt = gwbuf_alloc(strlen(msg) + 13)) == NULL) { return; } data = GWBUF_DATA(pkt); len = strlen(msg) + 9; encode_value(&data[0], len, 24);// Payload length data[3] = 1; // Sequence id // Payload data[4] = 0xff; // Error indicator encode_value(&data[5], 1064, 16); // Error Code memcpy((char*)&data[7], "#42000", 6); memcpy(&data[13], msg, strlen(msg)); // Error Message MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); } /* * Some standard packets that have been captured from a network trace of server * interactions. These packets are the schema definition sent in response to * a SELECT UNIX_TIMESTAMP() statement and the EOF packet that marks the end * of transmission of the result set. */ static uint8_t timestamp_def[] = { 0x01, 0x00, 0x00, 0x01, 0x01, 0x26, 0x00, 0x00, 0x02, 0x03, 0x64, 0x65, 0x66, 0x00, 0x00, 0x00, 0x10, 0x55, 0x4e, 0x49, 0x58, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x53, 0x54, 0x41, 0x4d, 0x50, 0x28, 0x29, 0x00, 0x0c, 0x3f, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x08, 0x81, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x03, 0xfe, 0x00, 0x00, 0x02, 0x00 }; static uint8_t timestamp_eof[] = {0x05, 0x00, 0x00, 0x05, 0xfe, 0x00, 0x00, 0x02, 0x00}; /** * Send a response to a "SELECT UNIX_TIMESTAMP()" request. * This differs from the other * requests since we do not save a copy of the original interaction * with the master and simply replay it. * We want to always send the current time. We have stored a typcial * response, which gives us the schema information normally returned. * This is sent to the * client and then we add a dynamic part that will * insert the current timestamp data. * Finally we send a preprepaed EOF packet to end the response stream. * * @param router The binlog router instance * @param slave The slave server to which we are sending the response * @return Non-zero if data was sent */ static int blr_slave_send_timestamp(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave) { GWBUF* pkt; char timestamp[20]; uint8_t* ptr; int len, ts_len; sprintf(timestamp, "%ld", time(0)); ts_len = strlen(timestamp); len = sizeof(timestamp_def) + sizeof(timestamp_eof) + MYSQL_HEADER_LEN + 1 + ts_len; if ((pkt = gwbuf_alloc(len)) == NULL) { return 0; } ptr = GWBUF_DATA(pkt); memcpy(ptr, timestamp_def, sizeof(timestamp_def)); // Fixed preamble ptr += sizeof(timestamp_def); encode_value(ptr, ts_len + 1, 24); // Add length of data packet ptr += 3; *ptr++ = 0x04; // Sequence number in response *ptr++ = ts_len; // Length of result string memcpy((char*)ptr, timestamp, ts_len); // Result string ptr += ts_len; // EOF packet to terminate result memcpy(ptr, timestamp_eof, sizeof(timestamp_eof)); return MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); } /** * Send a response the the SQL command SELECT @@MAXSCALE_VERSION * * @param router The binlog router instance * @param slave The slave server to which we are sending the response * @return Non-zero if data was sent */ static int blr_slave_send_maxscale_version(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave) { GWBUF* pkt; char version[80] = ""; uint8_t* ptr; int len, vers_len; sprintf(version, "%s", MAXSCALE_VERSION); vers_len = strlen(version); blr_slave_send_fieldcount(router, slave, 1); blr_slave_send_columndef(router, slave, "MAXSCALE_VERSION", BLR_TYPE_STRING, vers_len, 2); blr_slave_send_eof(router, slave, 3); len = MYSQL_HEADER_LEN + 1 + vers_len; if ((pkt = gwbuf_alloc(len)) == NULL) { return 0; } ptr = GWBUF_DATA(pkt); encode_value(ptr, vers_len + 1, 24); // Add length of data packet ptr += 3; *ptr++ = 0x04; // Sequence number in response *ptr++ = vers_len; // Length of result string memcpy((char*)ptr, version, vers_len); // Result string /* ptr += vers_len; Not required unless more data is to be added */ MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); return blr_slave_send_eof(router, slave, 5); } /** * Send a response the the SQL command SELECT @@server_id * * @param router The binlog router instance * @param slave The slave server to which we are sending the response * @return Non-zero if data was sent */ static int blr_slave_send_server_id(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave) { GWBUF* pkt; char server_id[40]; uint8_t* ptr; int len, id_len; sprintf(server_id, "%d", router->masterid); id_len = strlen(server_id); blr_slave_send_fieldcount(router, slave, 1); blr_slave_send_columndef(router, slave, "SERVER_ID", BLR_TYPE_INT, id_len, 2); blr_slave_send_eof(router, slave, 3); len = MYSQL_HEADER_LEN + 1 + id_len; if ((pkt = gwbuf_alloc(len)) == NULL) { return 0; } ptr = GWBUF_DATA(pkt); encode_value(ptr, id_len + 1, 24); // Add length of data packet ptr += 3; *ptr++ = 0x04; // Sequence number in response *ptr++ = id_len; // Length of result string memcpy((char*)ptr, server_id, id_len); // Result string /* ptr += id_len; Not required unless more data is to be added */ MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); return blr_slave_send_eof(router, slave, 5); } /** * Send the response to the SQL command "SHOW VARIABLES LIKE 'MAXSCALE%' * * @param router The binlog router instance * @param slave The slave server to which we are sending the response * @return Non-zero if data was sent */ static int blr_slave_send_maxscale_variables(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave) { GWBUF* pkt; char name[40]; char version[80]; uint8_t* ptr; int len, vers_len, seqno = 2; blr_slave_send_fieldcount(router, slave, 2); blr_slave_send_columndef(router, slave, "Variable_name", BLR_TYPE_STRING, 40, seqno++); blr_slave_send_columndef(router, slave, "Value", BLR_TYPE_STRING, 40, seqno++); blr_slave_send_eof(router, slave, seqno++); sprintf(version, "%s", MAXSCALE_VERSION); vers_len = strlen(version); strcpy(name, "MAXSCALE_VERSION"); len = MYSQL_HEADER_LEN + 1 + vers_len + strlen(name) + 1; if ((pkt = gwbuf_alloc(len)) == NULL) { return 0; } ptr = GWBUF_DATA(pkt); // Add length of data packet encode_value(ptr, vers_len + 2 + strlen(name), 24); ptr += 3; *ptr++ = seqno++; // Sequence number in response *ptr++ = strlen(name); // Length of result string memcpy((char*)ptr, name, strlen(name)); // Result string ptr += strlen(name); *ptr++ = vers_len; // Length of result string memcpy((char*)ptr, version, vers_len); // Result string /* ptr += vers_len; Not required unless more data is to be added */ MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); return blr_slave_send_eof(router, slave, seqno++); } /** * Send the response to the SQL command "SHOW MASTER STATUS" * * @param router The binlog router instance * @param slave The slave server to which we are sending the response * @return Non-zero if data was sent */ static int blr_slave_send_master_status(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave) { GWBUF* pkt; char file[BINLOG_FNAMELEN + 1]; char position[BINLOG_FNAMELEN + 1]; uint8_t* ptr; int len, file_len; blr_slave_send_fieldcount(router, slave, 5); blr_slave_send_columndef(router, slave, "File", BLR_TYPE_STRING, 40, 2); blr_slave_send_columndef(router, slave, "Position", BLR_TYPE_STRING, 40, 3); blr_slave_send_columndef(router, slave, "Binlog_Do_DB", BLR_TYPE_STRING, 40, 4); blr_slave_send_columndef(router, slave, "Binlog_Ignore_DB", BLR_TYPE_STRING, 40, 5); blr_slave_send_columndef(router, slave, "Execute_Gtid_Set", BLR_TYPE_STRING, 40, 6); blr_slave_send_eof(router, slave, 7); snprintf(file, sizeof(file), "%s", router->binlog_name); file_len = strlen(file); snprintf(position, sizeof(position), "%lu", router->binlog_position); len = MYSQL_HEADER_LEN + 1 + file_len + strlen(position) + 1 + 3; if ((pkt = gwbuf_alloc(len)) == NULL) { return 0; } ptr = GWBUF_DATA(pkt); // Add length of data packet encode_value(ptr, len - MYSQL_HEADER_LEN, 24); ptr += 3; *ptr++ = 0x08; // Sequence number in response *ptr++ = strlen(file); // Length of result string memcpy((char*)ptr, file, strlen(file)); // Result string ptr += strlen(file); *ptr++ = strlen(position); // Length of result string // Result string memcpy((char*)ptr, position, strlen(position)); ptr += strlen(position); *ptr++ = 0; // Send 3 empty values *ptr++ = 0; *ptr++ = 0; MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); return blr_slave_send_eof(router, slave, 9); } /* * Columns to send for GTID in "SHOW SLAVE STATUS" command */ static const char* slave_status_columns[] = { "Slave_IO_State", "Master_Host", "Master_User", "Master_Port", "Connect_Retry", "Master_Log_File", "Read_Master_Log_Pos", "Relay_Log_File", "Relay_Log_Pos", "Relay_Master_Log_File", "Slave_IO_Running", "Slave_SQL_Running", "Replicate_Do_DB", "Replicate_Ignore_DB", "Replicate_Do_Table", "Replicate_Ignore_Table", "Replicate_Wild_Do_Table", "Replicate_Wild_Ignore_Table", "Last_Errno", "Last_Error", "Skip_Counter", "Exec_Master_Log_Pos", "Relay_Log_Space", "Until_Condition", "Until_Log_File", "Until_Log_Pos", "Master_SSL_Allowed", "Master_SSL_CA_File", "Master_SSL_CA_Path", "Master_SSL_Cert", "Master_SSL_Cipher", "Master_SSL_Key", "Seconds_Behind_Master", "Master_SSL_Verify_Server_Cert", "Last_IO_Errno", "Last_IO_Error", "Last_SQL_Errno", "Last_SQL_Error", "Replicate_Ignore_Server_Ids", "Master_Server_Id", "Master_UUID", "Master_Info_File", "SQL_Delay", "SQL_Remaining_Delay", "Slave_SQL_Running_State", "Master_Retry_Count", "Master_Bind", "Last_IO_Error_TimeStamp", "Last_SQL_Error_Timestamp", "Master_SSL_Crl", "Master_SSL_Crlpath", NULL }; /* * New columns to send for GTID in "SHOW ALL SLAVES STATUS" command */ static const char* all_slaves_status_columns[] = { "Connection_name", "Slave_SQL_State", NULL }; /* * Columns to send for GTID in "SHOW SLAVE STATUS" MySQL 5.6/7 command */ static const char* mysql_gtid_status_columns[] = { "Retrieved_Gtid_Set", "Executed_Gtid_Set", "Auto_Position", NULL }; /* * Columns to send for GTID in "SHOW SLAVE STATUS" MariaDB 10 command * and SHOW ALL SLAVES STATUS as well */ static const char* mariadb10_gtid_status_columns[] = { "Using_Gtid", "Gtid_IO_Pos", NULL }; /* * Extra Columns to send in "SHOW ALL SLAVES STATUS" MariaDB 10 command */ static const char* mariadb10_extra_status_columns[] = { "Retried_transactions", "Max_relay_log_size", "Executed_log_entries", "Slave_received_heartbeats", "Slave_heartbeat_period", "Gtid_Slave_Pos", NULL }; /** * Send the response to the SQL command "SHOW SLAVE STATUS" or * SHOW ALL SLAVES STATUS * * @param router The binlog router instance * @param slave The slave server to which we are sending the response * @param all_slaves Whether to use SHOW ALL SLAVES STATUS * @return Non-zero if data was sent */ static int blr_slave_send_slave_status(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, bool all_slaves) { GWBUF* pkt; char column[2048] = ""; uint8_t* ptr; int len, actual_len, col_len, seqno, i; char* dyn_column = NULL; int max_column_size = sizeof(column); int ncols = 0; int gtid_cols = 0; /* Count SHOW SLAVE STATUS the columns */ ncols += MXS_ARRAY_NELEMS(slave_status_columns) - 1; /* Add the new SHOW ALL SLAVES STATUS columns */ if (all_slaves) { ncols += MXS_ARRAY_NELEMS(all_slaves_status_columns) - 1; ncols += MXS_ARRAY_NELEMS(mariadb10_extra_status_columns) - 1; } /* Get the right GTID columns array */ const char** gtid_status_columns = router->mariadb10_gtid ? mariadb10_gtid_status_columns : mysql_gtid_status_columns; /* Increment ncols with the right GTID columns */ while (gtid_status_columns[gtid_cols++]) { ncols++; } /* Send number of columns */ blr_slave_send_fieldcount(router, slave, ncols); seqno = 2; if (all_slaves) { /* Send first the column definitions for the all_slaves */ for (i = 0; all_slaves_status_columns[i]; i++) { blr_slave_send_columndef(router, slave, all_slaves_status_columns[i], BLR_TYPE_STRING, 40, seqno++); } } /* Now send column definitions for slave status */ for (i = 0; slave_status_columns[i]; i++) { blr_slave_send_columndef(router, slave, slave_status_columns[i], BLR_TYPE_STRING, 40, seqno++); } /* Send MariaDB 10 or MySQL 5.6/7 GTID columns */ for (i = 0; gtid_status_columns[i]; i++) { blr_slave_send_columndef(router, slave, gtid_status_columns[i], BLR_TYPE_STRING, 40, seqno++); } /* Send extra columns for SHOW ALL SLAVES STATUS */ if (all_slaves) { for (i = 0; mariadb10_extra_status_columns[i]; i++) { blr_slave_send_columndef(router, slave, mariadb10_extra_status_columns[i], BLR_TYPE_STRING, 40, seqno++); } } /* Send EOF for columns def */ blr_slave_send_eof(router, slave, seqno++); // Max length + 250 bytes error message len = MYSQL_HEADER_LEN + 1 + ncols * max_column_size + 250; if ((pkt = gwbuf_alloc(len)) == NULL) { return 0; } ptr = GWBUF_DATA(pkt); // Add length of data packet encode_value(ptr, len - MYSQL_HEADER_LEN, 24); ptr += 3; // Sequence number in response *ptr++ = seqno++; if (all_slaves) { for (i = 0; all_slaves_status_columns[i]; i++) { *ptr++ = 0; // Empty value } } // Slave_IO_State snprintf(column, max_column_size, "%s", blrm_states[router->master_state]); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; // Master_Host snprintf(column, max_column_size, "%s", router->service->dbref->server->address ? router->service->dbref->server->address : ""); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; // Master_User snprintf(column, max_column_size, "%s", router->user ? router->user : ""); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; // Master_Port sprintf(column, "%d", router->service->dbref->server->port); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; sprintf(column, "%d", router->retry_interval); // Connect retry col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; snprintf(column, max_column_size, "%s", router->binlog_name); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; /* if router->trx_safe report current_pos*/ if (router->trx_safe) { sprintf(column, "%lu", router->current_pos); } else { sprintf(column, "%lu", router->binlog_position); } col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; /* We have no relay log, we relay the binlog, so we will send the same data */ snprintf(column, max_column_size, "%s", router->binlog_name); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; sprintf(column, "%ld", router->binlog_position); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; /* We have no relay log, we relay the binlog, so we will send the same data */ snprintf(column, max_column_size, "%s", router->binlog_name); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; if (router->master_state != BLRM_SLAVE_STOPPED) { if (router->master_state < BLRM_BINLOGDUMP) { strcpy(column, "Connecting"); } else { strcpy(column, "Yes"); } } else { strcpy(column, "No"); } col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; if (router->master_state != BLRM_SLAVE_STOPPED) { strcpy(column, "Yes"); } else { strcpy(column, "No"); } col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; *ptr++ = 0; // Send 6 empty values *ptr++ = 0; *ptr++ = 0; *ptr++ = 0; *ptr++ = 0; *ptr++ = 0; /* Last error information */ sprintf(column, "%lu", router->m_errno); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; /* Last error message */ if (router->m_errmsg == NULL) { *ptr++ = 0; } else { dyn_column = (char*)router->m_errmsg; col_len = strlen(dyn_column); if (col_len > 250) { col_len = 250; } *ptr++ = col_len; // Length of result string memcpy((char*)ptr, dyn_column, col_len); // Result string ptr += col_len; } /* Skip_Counter */ sprintf(column, "%d", 0); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; sprintf(column, "%ld", router->binlog_position); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; sprintf(column, "%ld", router->binlog_position); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; strcpy(column, "None"); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; *ptr++ = 0; /* Until_Log_Pos */ sprintf(column, "%d", 0); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; /* Master_SSL_Allowed */ if (router->ssl_enabled) { strcpy(column, "Yes"); } else { strcpy(column, "No"); } col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; /* Check whether to report SSL master connection details */ if (router->ssl_ca && router->ssl_cert && router->ssl_key) { char big_column[250 + 1] = ""; // set Master_SSL_Cert strncpy(big_column, router->ssl_ca, 250); col_len = strlen(big_column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, big_column, col_len);// Result string ptr += col_len; *ptr++ = 0; // Empty Master_SSL_CA_Path column // set Master_SSL_Cert strncpy(big_column, router->ssl_cert, 250); col_len = strlen(big_column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, big_column, col_len);// Result string ptr += col_len; *ptr++ = 0; // Empty Master_SSL_Cipher column // set Master_SSL_Key strncpy(big_column, router->ssl_key, 250); col_len = strlen(big_column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, big_column, col_len);// Result string ptr += col_len; } else { *ptr++ = 0; // Empty SSL columns *ptr++ = 0; *ptr++ = 0; *ptr++ = 0; *ptr++ = 0; } /* Seconds_Behind_Master */ sprintf(column, "%d", 0); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; /* Master_SSL_Verify_Server_Cert */ strcpy(column, "No"); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; /* Last_IO_Error */ sprintf(column, "%d", 0); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; *ptr++ = 0; /* Last_SQL_Error */ sprintf(column, "%d", 0); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; *ptr++ = 0; *ptr++ = 0; /* Master_Server_Id */ sprintf(column, "%d", router->orig_masterid); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; /* Master_server_UUID */ snprintf(column, max_column_size, "%s", router->master_uuid ? router->master_uuid : router->uuid); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; /* Master_info_file */ snprintf(column, max_column_size, "%s/master.ini", router->binlogdir); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; /* SQL_Delay*/ sprintf(column, "%d", 0); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; *ptr++ = 0xfb; // NULL value /* Slave_Running_State */ if (router->master_state == BLRM_SLAVE_STOPPED) { strcpy(column, "Slave stopped"); } else if (!router->m_errno) { strcpy(column, "Slave running"); } else { if (router->master_state < BLRM_BINLOGDUMP) { strcpy(column, "Registering"); } else { strcpy(column, "Error"); } } col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; /* Master_Retry_Count */ sprintf(column, "%d", router->retry_limit); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; *ptr++ = 0; // Send 5 empty values *ptr++ = 0; *ptr++ = 0; *ptr++ = 0; *ptr++ = 0; if (!router->mariadb10_gtid) { // No GTID support send empty values *ptr++ = 0; *ptr++ = 0; *ptr++ = 0; } else { // MariaDB 10 GTID // 1 - Add "Using_Gtid" sprintf(column, "%s", router->mariadb10_master_gtid ? "Slave_pos" : "No"); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy(ptr, column, col_len); // Result string ptr += col_len; // 2 - Add "Gtid_IO_Pos" sprintf(column, "%s", router->last_mariadb_gtid); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy(ptr, column, col_len); // Result string ptr += col_len; } if (all_slaves) { // Retried_transactions sprintf(column, "%d", 0); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; *ptr++ = 0; // Max_relay_log_size *ptr++ = 0; // Executed_log_entries // Slave_received_heartbeats sprintf(column, "%d", router->stats.n_heartbeats); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; // Slave_heartbeat_period sprintf(column, "%lu", router->heartbeat); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy((char*)ptr, column, col_len); // Result string ptr += col_len; // Gtid_Slave_Pos if (!router->mariadb10_gtid) { // No GTID support send empty values *ptr++ = 0; } else { sprintf(column, "%s", router->last_mariadb_gtid); col_len = strlen(column); *ptr++ = col_len; // Length of result string memcpy(ptr, column, col_len); // Result string ptr += col_len; } } *ptr++ = 0; actual_len = ptr - (uint8_t*)GWBUF_DATA(pkt); ptr = GWBUF_DATA(pkt); // Add length of data packet encode_value(ptr, actual_len - MYSQL_HEADER_LEN, 24); // Trim the buffer to the actual size pkt = gwbuf_rtrim(pkt, len - actual_len); MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); return blr_slave_send_eof(router, slave, seqno++); } /** * Send the response to the SQL command "SHOW SLAVE HOSTS" * * @param router The binlog router instance * @param slave The connected slave server * @return Non-zero if data was sent */ static int blr_slave_send_slave_hosts(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave) { GWBUF* pkt; char server_id[40]; char host[40]; char port[40]; char master_id[40]; char slave_uuid[40]; uint8_t* ptr; int len, seqno; ROUTER_SLAVE* sptr; blr_slave_send_fieldcount(router, slave, 5); blr_slave_send_columndef(router, slave, "Server_id", BLR_TYPE_STRING, 40, 2); blr_slave_send_columndef(router, slave, "Host", BLR_TYPE_STRING, 40, 3); blr_slave_send_columndef(router, slave, "Port", BLR_TYPE_STRING, 40, 4); blr_slave_send_columndef(router, slave, "Master_id", BLR_TYPE_STRING, 40, 5); blr_slave_send_columndef(router, slave, "Slave_UUID", BLR_TYPE_STRING, 40, 6); blr_slave_send_eof(router, slave, 7); seqno = 8; pthread_mutex_lock(&router->lock); sptr = router->slaves; while (sptr) { if (sptr->state == BLRS_DUMPING || sptr->state == BLRS_REGISTERED) { sprintf(server_id, "%d", sptr->serverid); sprintf(host, "%s", sptr->hostname ? sptr->hostname : ""); sprintf(port, "%d", sptr->port); sprintf(master_id, "%d", router->serverid); sprintf(slave_uuid, "%s", sptr->uuid ? sptr->uuid : ""); len = MYSQL_HEADER_LEN + strlen(server_id) + strlen(host) + strlen(port) + strlen(master_id) + strlen(slave_uuid) + 5; if ((pkt = gwbuf_alloc(len)) == NULL) { return 0; } ptr = GWBUF_DATA(pkt); encode_value(ptr, len - MYSQL_HEADER_LEN, 24); // Add length of data packet ptr += 3; *ptr++ = seqno++; // Sequence number in response *ptr++ = strlen(server_id); // Length of result string memcpy((char*)ptr, server_id, strlen(server_id)); // Result string ptr += strlen(server_id); *ptr++ = strlen(host); // Length of result string memcpy((char*)ptr, host, strlen(host)); // Result string ptr += strlen(host); *ptr++ = strlen(port); // Length of result string memcpy((char*)ptr, port, strlen(port)); // Result string ptr += strlen(port); *ptr++ = strlen(master_id); // Length of result string memcpy((char*)ptr, master_id, strlen(master_id)); // Result string ptr += strlen(master_id); *ptr++ = strlen(slave_uuid); // Length of result string memcpy((char*)ptr, slave_uuid, strlen(slave_uuid)); // Result string ptr += strlen(slave_uuid); MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); } sptr = sptr->next; } pthread_mutex_unlock(&router->lock); return blr_slave_send_eof(router, slave, seqno); } /** * Process a slave replication registration message. * * We store the various bits of information the slave gives us and generate * a reply message: OK packet. * * @param router The router instance * @param slave The slave server * @param queue The BINLOG_DUMP packet * @return Non-zero if data was sent */ static int blr_slave_register(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, GWBUF* queue) { uint8_t* ptr; int slen; ptr = GWBUF_DATA(queue); ptr += 4; // Skip length and sequence number if (*ptr++ != COM_REGISTER_SLAVE) { return 0; } slave->serverid = extract_field(ptr, 32); ptr += 4; slen = *ptr++; if (slen != 0) { slave->hostname = strndup((char*)ptr, slen); ptr += slen; } else { slave->hostname = NULL; } slen = *ptr++; if (slen != 0) { ptr += slen; slave->user = strndup((char*)ptr, slen); } else { slave->user = NULL; } slen = *ptr++; if (slen != 0) { slave->passwd = strndup((char*)ptr, slen); ptr += slen; } else { slave->passwd = NULL; } slave->port = extract_field(ptr, 16); ptr += 2; slave->rank = extract_field(ptr, 32); slave->state = BLRS_REGISTERED; /* * Send OK response */ return blr_slave_send_ok(router, slave); } /** * Process a COM_BINLOG_DUMP message from the slave. This is the * final step in the process of registration. The new master, MaxScale * must send a response packet and generate a fake BINLOG_ROTATE event * with the binlog file requested by the slave. And then send a * FORMAT_DESCRIPTION_EVENT that has been saved from the real master. * * Once send MaxScale must continue to send binlog events to the slave. * * @param router The router instance * @param slave The slave server * @param queue The BINLOG_DUMP packet * @return The number of bytes written to the slave */ static int blr_slave_binlog_dump(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, GWBUF* queue) { GWBUF* resp; uint8_t* ptr; int len, binlognamelen; REP_HEADER hdr; uint32_t chksum; uint32_t fde_end_pos; uint32_t requested_pos; ptr = GWBUF_DATA(queue); len = extract_field(ptr, 24); binlognamelen = len - 11; ptr += 4; // Skip length and sequence number if (*ptr++ != COM_BINLOG_DUMP) { MXS_ERROR("blr_slave_binlog_dump expected a COM_BINLOG_DUMP but received %d", *(ptr - 1)); slave->state = BLRS_ERRORED; dcb_close(slave->dcb); return 1; } /* Get the current router binlog file */ pthread_mutex_lock(&router->binlog_lock); strcpy(slave->binlog_name, router->binlog_name); pthread_mutex_unlock(&router->binlog_lock); /* Set the safe pos */ slave->binlog_pos = 4; /* Get the requested pos from packet */ requested_pos = extract_field(ptr, 32); /* Go ahead: after 4 bytes pos, 2 bytes flag and 4 bytes serverid */ ptr += 4; uint16_t flags = gw_mysql_get_byte2(ptr); /* Check whether connected slave is asking for ANNOTATE_ROWS events */ if (flags & BLR_REQUEST_ANNOTATE_ROWS_EVENT) { slave->annotate_rows = true; MXS_INFO("Registering slave (server-id %d) asks " "for ANNOTATE_ROWS events.", slave->serverid); } /* Go ahead: after 2 bytes flags and 4 bytes serverid */ ptr += 2; ptr += 4; /* ptr now points to requested filename, if present */ if (binlognamelen) { if (binlognamelen > BINLOG_FNAMELEN) { /* Abort the request */ char req_file[binlognamelen + 1]; char errmsg[BINLOG_ERROR_MSG_LEN + 1]; memcpy(req_file, (char*)ptr, binlognamelen); req_file[binlognamelen] = 0; snprintf(errmsg, BINLOG_ERROR_MSG_LEN, "Requested filename %s is longer than max %d chars.", req_file, BINLOG_FNAMELEN); errmsg[BINLOG_ERROR_MSG_LEN] = '\0'; // ERROR slave->seqno++; blr_slave_abort_dump_request(slave, errmsg); slave->state = BLRS_ERRORED; dcb_close(slave->dcb); return 1; } /* Set the received filename from packet: it could be changed later */ memcpy(slave->binlog_name, (char*)ptr, binlognamelen); slave->binlog_name[binlognamelen] = 0; } /** * Check MariaDB GTID request */ if (slave->mariadb10_compat && slave->mariadb_gtid) { /* Set file and pos accordingly to GTID lookup */ if (!blr_slave_gtid_request(router, slave, binlognamelen > 0, requested_pos)) { // ERROR slave->state = BLRS_ERRORED; dcb_close(slave->dcb); return 1; } } else { /** * Binlog file has been set from packet data. * Now just set the position from packet as well. */ slave->binlog_pos = requested_pos; } /** * Check for a pending transaction and possible unsafe position. * Force slave disconnection if requested position is unsafe. */ if (router->trx_safe) { bool force_disconnect = false; pthread_mutex_lock(&router->binlog_lock); if (router->pending_transaction.state > BLRM_NO_TRANSACTION && blr_is_current_binlog(router, slave) && (slave->binlog_pos > router->binlog_position)) { force_disconnect = true; } pthread_mutex_unlock(&router->binlog_lock); if (force_disconnect) { MXS_ERROR("%s: Slave %s:%i, server-id %d, binlog '%s', blr_slave_binlog_dump failure: " "Requested binlog position %lu. Position is unsafe so disconnecting. " "Latest safe position %lu, end of binlog file %lu", router->service->name(), slave->dcb->remote, dcb_get_port(slave->dcb), slave->serverid, slave->binlog_name, (unsigned long)slave->binlog_pos, router->binlog_position, router->current_pos); slave->state = BLRS_ERRORED; /* * Close the slave session and socket * The slave will try to reconnect */ dcb_close(slave->dcb); return 1; } } MXS_DEBUG("%s: Slave %s:%i, COM_BINLOG_DUMP: binlog name '%s', length %lu, " "from position %lu.", router->service->name(), slave->dcb->remote, dcb_get_port(slave->dcb), slave->binlog_name, strlen(slave->binlog_name), (unsigned long)slave->binlog_pos); /* Check first the requested file exists */ if (!blr_binlog_file_exists(router, &slave->f_info)) { char errmsg[BINLOG_ERROR_MSG_LEN + 1]; snprintf(errmsg, BINLOG_ERROR_MSG_LEN, "Requested file name '%s' doesn't exist", slave->binlog_name); errmsg[BINLOG_ERROR_MSG_LEN] = '\0'; // ERROR slave->seqno++; blr_slave_abort_dump_request(slave, errmsg); slave->state = BLRS_ERRORED; dcb_close(slave->dcb); return 1; } /* First reply starts from seq = 1 */ slave->seqno = 1; /** * Check whether the request file is empty * and try using next file in sequence or next one * based on GTID mpas. * If one or more files have been skipped then * the slave->binlog_pos is set to 4 and * slave->binlogname set to new filename. */ blr_slave_skip_empty_files(router, slave); /* Build and send Fake Rotate Event */ if (!blr_send_connect_fake_rotate(router, slave)) { char errmsg[BINLOG_ERROR_MSG_LEN + 1]; snprintf(errmsg, BINLOG_ERROR_MSG_LEN, "Cannot send Fake Rotate Event for '%s'", slave->binlog_name); errmsg[BINLOG_ERROR_MSG_LEN] = '\0'; // ERROR blr_slave_abort_dump_request(slave, errmsg); slave->state = BLRS_ERRORED; dcb_close(slave->dcb); return 1; } /* set lastEventReceived */ slave->lastEventReceived = ROTATE_EVENT; /* set lastReply for slave heartbeat check */ if (router->send_slave_heartbeat) { slave->lastReply = time(0); } /* Read Format Description Event */ GWBUF* fde = blr_slave_read_fde(router, slave); if (fde == NULL) { char errmsg[BINLOG_ERROR_MSG_LEN + 1]; snprintf(errmsg, BINLOG_ERROR_MSG_LEN, "Cannot read FDE event from file '%s'", slave->binlog_name); errmsg[BINLOG_ERROR_MSG_LEN] = '\0'; // ERROR blr_slave_abort_dump_request(slave, errmsg); slave->state = BLRS_ERRORED; dcb_close(slave->dcb); return 1; } /* FDE ends at pos 4 + FDE size */ fde_end_pos = 4 + GWBUF_LENGTH(fde); /* Send a Fake FORMAT_DESCRIPTION_EVENT */ if (slave->binlog_pos != 4) { if (!blr_slave_send_fde(router, slave, fde)) { char errmsg[BINLOG_ERROR_MSG_LEN + 1]; snprintf(errmsg, BINLOG_ERROR_MSG_LEN, "Cannot send FDE for file '%s'", slave->binlog_name); errmsg[BINLOG_ERROR_MSG_LEN] = '\0'; // ERROR blr_slave_abort_dump_request(slave, errmsg); slave->state = BLRS_ERRORED; dcb_close(slave->dcb); gwbuf_free(fde); return 1; } } /* set lastEventReceived */ slave->lastEventReceived = FORMAT_DESCRIPTION_EVENT; /** * Check for MXS_START_ENCRYPTION_EVENT (after FDE) if * client request pos is greater than 4 * * TODO: If router has binlog encryption take it * otherwise error * If no encryption and event found return error * * If event is found the contest is set into slave struct */ if (slave->binlog_pos != 4) { blr_slave_read_ste(router, slave, fde_end_pos); } /** * Add GTID_LIST Fake Event before sending any new event * Note: slave->binlog_pos must not be 4 */ if (slave->binlog_pos != 4 && slave->mariadb10_compat && slave->mariadb_gtid) { if (!blr_send_fake_gtid_list(slave, slave->mariadb_gtid, router->masterid)) { char errmsg[BINLOG_ERROR_MSG_LEN + 1]; snprintf(errmsg, BINLOG_ERROR_MSG_LEN, "Cannot send Fake GTID List Event for '%s'", slave->binlog_name); errmsg[BINLOG_ERROR_MSG_LEN] = '\0'; // ERROR blr_slave_abort_dump_request(slave, errmsg); slave->state = BLRS_ERRORED; dcb_close(slave->dcb); gwbuf_free(fde); return 1; } slave->lastEventReceived = MARIADB10_GTID_GTID_LIST_EVENT; } /* Set dcb_callback for the events reading routine */ dcb_add_callback(slave->dcb, DCB_REASON_DRAINED, blr_slave_callback, slave); slave->state = BLRS_DUMPING; MXS_NOTICE("%s: Slave [%s]:%d, server id %d requested binlog file %s from position %lu", router->service->name(), slave->dcb->remote, dcb_get_port(slave->dcb), slave->serverid, slave->binlog_name, (unsigned long)slave->binlog_pos); /* Force the slave to call catchup routine */ poll_fake_write_event(slave->dcb); gwbuf_free(fde); return 1; } /** * Encode a value into a number of bits in a MySQL packet * * @param data Pointer to location in target packet * @param value The value to encode into the buffer * @param len Number of bits to encode value into */ static void encode_value(unsigned char* data, unsigned int value, int len) { while (len > 0) { *data++ = value & 0xff; value >>= 8; len -= 8; } } /** * Populate a header structure for a replication message from a GWBUF. * * @param pkt The incoming packet in a GWBUF chain * @param hdr The packet header to populate * @return A pointer to the first byte following the event header */ uint8_t* blr_build_header(GWBUF* pkt, REP_HEADER* hdr) { uint8_t* ptr; ptr = GWBUF_DATA(pkt); encode_value(ptr, hdr->payload_len, 24); ptr += 3; *ptr++ = hdr->seqno; *ptr++ = hdr->ok; encode_value(ptr, hdr->timestamp, 32); ptr += 4; *ptr++ = hdr->event_type; encode_value(ptr, hdr->serverid, 32); ptr += 4; encode_value(ptr, hdr->event_size, 32); ptr += 4; encode_value(ptr, hdr->next_pos, 32); ptr += 4; encode_value(ptr, hdr->flags, 16); ptr += 2; return ptr; } /** * We have a registered slave that is behind the current leading edge of the * binlog. We must replay the log entries to bring this node up to speed. * * There may be a large number of records to send to the slave, the process * is triggered by the slave COM_BINLOG_DUMP message and all the events must * be sent without receiving any new event. This measn there is no trigger into * MaxScale other than this initial message. However, if we simply send all the * events we end up with an extremely long write queue on the DCB and risk * running the server out of resources. * * The slave catchup routine will send a burst of replication events per single * call. The paramter "long" control the number of events in the burst. The * short burst is intended to be used when the master receive an event and * needs to put the slave into catchup mode. This prevents the slave taking * too much time away from the thread that is processing the master events. * * At the end of the burst a fake EPOLLOUT event is added to the poll event * queue. This ensures that the slave callback for processing DCB write drain * will be called and future catchup requests will be handled on another thread. * * @param router The binlog router * @param slave The slave that is behind * @param large Send a long or short burst of events * @return The number of bytes written */ int blr_slave_catchup(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, bool large) { GWBUF* record; REP_HEADER hdr; int rval = 1, burst; int rotating = 0; long burst_size; char read_errmsg[BINLOG_ERROR_MSG_LEN + 1]; MARIADB_GTID_INFO* f_tree = router->storage_type == BLR_BINLOG_STORAGE_TREE ? &slave->f_info : NULL; read_errmsg[BINLOG_ERROR_MSG_LEN] = '\0'; if (large) { burst = router->long_burst; } else { burst = router->short_burst; } burst_size = router->burst_size; int do_return; pthread_mutex_lock(&router->binlog_lock); do_return = 0; /* check for a pending transaction and safe position */ if (router->pending_transaction.state > BLRM_NO_TRANSACTION && blr_is_current_binlog(router, slave) && (slave->binlog_pos > router->binlog_position)) { do_return = 1; } pthread_mutex_unlock(&router->binlog_lock); if (do_return) { pthread_mutex_lock(&slave->catch_lock); slave->cstate &= ~CS_BUSY; slave->cstate |= CS_EXPECTCB; pthread_mutex_unlock(&slave->catch_lock); poll_fake_write_event(slave->dcb); return 0; } BLFILE* file; #ifdef BLFILE_IN_SLAVE file = slave->file; slave->file = NULL; #else file = NULL; #endif // Prefix for BLR_BINLOG_STORAGE_TREE char t_prefix[BINLOG_FILE_EXTRA_INFO] = ""; if (file == NULL) { rotating = router->rotating; if ((file = blr_open_binlog(router, slave->binlog_name, f_tree)) == NULL) { char err_msg[BINLOG_ERROR_MSG_LEN + 1]; err_msg[BINLOG_ERROR_MSG_LEN] = '\0'; if (rotating) { pthread_mutex_lock(&slave->catch_lock); slave->cstate |= CS_EXPECTCB; slave->cstate &= ~CS_BUSY; pthread_mutex_unlock(&slave->catch_lock); poll_fake_write_event(slave->dcb); return rval; } /* Fill the file prefix */ if (f_tree) { sprintf(t_prefix, "%" PRIu32 "/%" PRIu32 "/", f_tree->gtid_elms.domain_id, f_tree->gtid_elms.server_id); } MXS_ERROR("Slave %s:%i, server-id %d, binlog '%s%s': blr_slave_catchup " "failed to open binlog file.", slave->dcb->remote, dcb_get_port(slave->dcb), slave->serverid, t_prefix, slave->binlog_name); slave->cstate &= ~CS_BUSY; slave->state = BLRS_ERRORED; snprintf(err_msg, BINLOG_ERROR_MSG_LEN, "Failed to open binlog '%s'", slave->binlog_name); /* Send error that stops slave replication */ blr_send_custom_error(slave->dcb, slave->seqno++, 0, err_msg, "HY000", BINLOG_FATAL_ERROR_READING); dcb_close(slave->dcb); return 0; } } slave->stats.n_bursts++; #ifdef BLSLAVE_IN_FILE slave->file = file; #endif int events_before = slave->stats.n_events; /* Loop read binlog events from slave binlog file */ while (burst-- && burst_size > 0 && /* Read one binlog event */ (record = blr_read_binlog(router, file, slave->binlog_pos, &hdr, read_errmsg, slave->encryption_ctx)) != NULL) { char binlog_name[BINLOG_FNAMELEN + 1]; uint32_t binlog_pos; uint32_t event_size; /* Get up to date file prefix */ if (f_tree) { sprintf(t_prefix, "%" PRIu32 "/%" PRIu32 "/", slave->f_info.gtid_elms.domain_id, slave->f_info.gtid_elms.server_id); } strcpy(binlog_name, slave->binlog_name); binlog_pos = slave->binlog_pos; /** * Don't sent special events generated by MaxScale * or ANNOTATE_ROWS events if not requested */ if (hdr.event_type == MARIADB10_START_ENCRYPTION_EVENT || hdr.event_type == IGNORABLE_EVENT || (!slave->annotate_rows && hdr.event_type == MARIADB_ANNOTATE_ROWS_EVENT)) { /* In case of file rotation or pos = 4 the events * are sent from position 4 and the new FDE at pos 4 is read. * We need to check whether the first event after FDE * is the MARIADB10_MXS_START_ENCRYPTION_EVENT of the new file. * * Read it if slave->encryption_ctx is NULL and * set the slave->encryption_ctx accordingly */ pthread_mutex_lock(&slave->catch_lock); if (hdr.event_type == MARIADB10_START_ENCRYPTION_EVENT && !slave->encryption_ctx) { /* read it, set slave & file context */ uint8_t* record_ptr = GWBUF_DATA(record); void* mem = MXS_CALLOC(1, sizeof(SLAVE_ENCRYPTION_CTX)); SLAVE_ENCRYPTION_CTX* encryption_ctx; encryption_ctx = static_cast(mem); MXS_ABORT_IF_NULL(encryption_ctx); record_ptr += BINLOG_EVENT_HDR_LEN; encryption_ctx->binlog_crypto_scheme = record_ptr[0]; memcpy(&encryption_ctx->binlog_key_version, record_ptr + 1, BLRM_KEY_VERSION_LENGTH); memcpy(encryption_ctx->nonce, record_ptr + 1 + BLRM_KEY_VERSION_LENGTH, BLRM_NONCE_LENGTH); /* Save current first_enc_event_pos */ encryption_ctx->first_enc_event_pos = hdr.next_pos; /* set the encryption ctx into slave */ slave->encryption_ctx = encryption_ctx; MXS_INFO("Start Encryption event found while reading. " "Binlog '%s%s' is encrypted. First event at %lu", t_prefix, slave->binlog_name, (unsigned long)hdr.next_pos); } /* MARIADB_ANNOTATE_ROWS_EVENT is skipped: just log that */ else if (hdr.event_type == MARIADB_ANNOTATE_ROWS_EVENT) { MXS_INFO("Skipping ANNOTATE_ROWS event [%s] of size %lu while " "reading binlog '%s%s' at %lu", blr_get_event_description(router, hdr.event_type), (unsigned long)hdr.event_size, t_prefix, slave->binlog_name, (unsigned long)slave->binlog_pos); } else { MXS_INFO("Found ignorable event [%s] of size %lu while " "reading binlog '%s%s' at %lu", blr_get_event_description(router, hdr.event_type), (unsigned long)hdr.event_size, t_prefix, slave->binlog_name, (unsigned long)slave->binlog_pos); } /* set next pos */ slave->binlog_pos = hdr.next_pos; pthread_mutex_unlock(&slave->catch_lock); gwbuf_free(record); record = NULL; break; } /* Handle ROTATE_EVENT */ if (hdr.event_type == ROTATE_EVENT) { int64_t beat1 = mxs_clock(); blr_close_binlog(router, file); int64_t beat2 = mxs_clock(); if (beat2 - beat1 > 1) { MXS_ERROR("blr_close_binlog took %ld maxscale beats", beat2 - beat1); } /* Set new file in slave->binlog_name */ blr_slave_rotate(router, slave, GWBUF_DATA(record)); /* reset the encryption context */ MXS_FREE(slave->encryption_ctx); slave->encryption_ctx = NULL; beat1 = mxs_clock(); #ifdef BLFILE_IN_SLAVE if ((slave->file = blr_open_binlog(router, slave->binlog_name, f_tree)) == NULL) #else if ((file = blr_open_binlog(router, slave->binlog_name, f_tree)) == NULL) #endif { char err_msg[BINLOG_ERROR_MSG_LEN + 1]; err_msg[BINLOG_ERROR_MSG_LEN] = '\0'; if (rotating) { pthread_mutex_lock(&slave->catch_lock); slave->cstate |= CS_EXPECTCB; slave->cstate &= ~CS_BUSY; pthread_mutex_unlock(&slave->catch_lock); poll_fake_write_event(slave->dcb); return rval; } /* Refresh file prefix */ if (f_tree) { sprintf(t_prefix, "%" PRIu32 "/%" PRIu32 "/", slave->f_info.gtid_elms.domain_id, slave->f_info.gtid_elms.server_id); } MXS_ERROR("Slave %s:%i, server-id %d, binlog '%s%s': blr_slave_catchup " "failed to open binlog file in rotate event", slave->dcb->remote, dcb_get_port(slave->dcb), slave->serverid, t_prefix, slave->binlog_name); slave->state = BLRS_ERRORED; snprintf(err_msg, BINLOG_ERROR_MSG_LEN, "Failed to open binlog '%s' in rotate event", slave->binlog_name); /* Send error that stops slave replication */ blr_send_custom_error(slave->dcb, slave->seqno, 0, err_msg, "HY000", BINLOG_FATAL_ERROR_READING); gwbuf_free(record); record = NULL; slave->state = BLRS_ERRORED; dcb_close(slave->dcb); return 0; } #ifdef BLFILE_IN_SLAVE file = slave->file; #endif if (mxs_clock() - beat1 > 1) { MXS_ERROR("blr_open_binlog took %lu beats", mxs_clock() - beat1); } } /* Send the binlog event */ if (blr_send_event(BLR_THREAD_ROLE_SLAVE, binlog_name, binlog_pos, slave, &hdr, (uint8_t*)record->start)) { if (hdr.event_type != ROTATE_EVENT) { slave->binlog_pos = hdr.next_pos; } slave->stats.n_events++; burst_size -= hdr.event_size; } else { MXS_WARNING("Slave %s:%i, server-id %d, binlog '%s%s', position %u: " "Slave-thread could not send event to slave, " "closing connection.", slave->dcb->remote, dcb_get_port(slave->dcb), slave->serverid, t_prefix, binlog_name, binlog_pos); #ifndef BLFILE_IN_SLAVE blr_close_binlog(router, file); #endif slave->state = BLRS_ERRORED; dcb_close(slave->dcb); return 0; } gwbuf_free(record); record = NULL; /* set lastReply for slave heartbeat check */ if (router->send_slave_heartbeat) { slave->lastReply = time(0); } } /** * End of while reading * * Note: * - the reading routine can set 'record' pointer to NULL * - after reading & sending it is always set to NULL * - if the above loop ends due to burst and burstsize values, * the 'record' is NULL as well (no reads at all or at least * one succesfull read). * * Now checking read error indicator. */ mxb_assert(record == NULL); /* Refresh file prefix */ if (f_tree) { sprintf(t_prefix, "%" PRIu32 "/%" PRIu32 "/", slave->f_info.gtid_elms.domain_id, slave->f_info.gtid_elms.server_id); } if (hdr.ok != SLAVE_POS_READ_OK) { slave->stats.n_failed_read++; if (hdr.ok == SLAVE_POS_BAD_FD) { MXS_ERROR("%s Slave %s:%i, server-id %d, binlog '%s%s', %s", router->service->name(), slave->dcb->remote, dcb_get_port(slave->dcb), slave->serverid, t_prefix, slave->binlog_name, read_errmsg); } if (hdr.ok == SLAVE_POS_BEYOND_EOF) { MXS_ERROR("%s Slave %s:%i, server-id %d, binlog '%s%s', %s", router->service->name(), slave->dcb->remote, dcb_get_port(slave->dcb), slave->serverid, t_prefix, slave->binlog_name, read_errmsg); /* * Close the slave session and socket * The slave will try to reconnect */ dcb_close(slave->dcb); #ifndef BLFILE_IN_SLAVE blr_close_binlog(router, file); #endif return 0; } if (hdr.ok == SLAVE_POS_READ_ERR) { MXS_ERROR("%s Slave %s:%i, server-id %d, binlog '%s%s', %s", router->service->name(), slave->dcb->remote, dcb_get_port(slave->dcb), slave->serverid, t_prefix, slave->binlog_name, read_errmsg); pthread_mutex_lock(&slave->catch_lock); slave->state = BLRS_ERRORED; pthread_mutex_unlock(&slave->catch_lock); /* * Send an error that will stop slave replication */ blr_send_custom_error(slave->dcb, slave->seqno++, 0, read_errmsg, "HY000", BINLOG_FATAL_ERROR_READING); dcb_close(slave->dcb); #ifndef BLFILE_IN_SLAVE blr_close_binlog(router, file); #endif return 0; } if (hdr.ok == SLAVE_POS_READ_UNSAFE) { MXS_NOTICE("%s: Slave %s:%i, server-id %d, binlog '%s%s', read %d events, " "current committed transaction event being sent: %lu, %s", router->service->name(), slave->dcb->remote, dcb_get_port(slave->dcb), slave->serverid, t_prefix, slave->binlog_name, slave->stats.n_events - events_before, router->current_safe_event, read_errmsg); hdr.ok = SLAVE_POS_READ_OK; } } /* Remove BUSY state */ pthread_mutex_lock(&slave->catch_lock); slave->cstate &= ~CS_BUSY; pthread_mutex_unlock(&slave->catch_lock); mxb_assert(hdr.ok == SLAVE_POS_READ_OK); /** * Check now slave position with read indicator = SLAVE_POS_READ_OK * * Two cases handled: * (1) The slave is Up To Date * (2) The slave is at EOF of a file which is not the current router file * */ if (slave->binlog_pos == router->binlog_position && blr_is_current_binlog(router, slave)) { /** * (1) Same name and pos as current router file: aka Up To Date */ pthread_mutex_lock(&router->binlog_lock); pthread_mutex_lock(&slave->catch_lock); /* * Now check again since we hold the router->binlog_lock * and slave->catch_lock. */ if (slave->binlog_pos != router->binlog_position || !blr_is_current_binlog(router, slave)) { slave->cstate |= CS_EXPECTCB; pthread_mutex_unlock(&slave->catch_lock); pthread_mutex_unlock(&router->binlog_lock); /* Force slave to read events via catchup routine */ poll_fake_write_event(slave->dcb); } else { /** * The slave server is up to date! * * set the CS_WAIT_DATA: this allows notification * when new events are received from master server, * the call back routine will be called later. */ slave->cstate |= CS_WAIT_DATA; pthread_mutex_unlock(&slave->catch_lock); pthread_mutex_unlock(&router->binlog_lock); } } else { /** * (2) Checking End Of File of the slave binlog file * and current router file */ if (slave->binlog_pos >= blr_file_size(file) && router->rotating == 0 && !blr_is_current_binlog(router, slave)) { /** * This is end of current slave file * which is not the current router binlog file */ char next_file[BINLOG_FNAMELEN + 1] = ""; MARIADB_GTID_INFO current_info; char c_prefix[BINLOG_FILE_EXTRA_INFO] = ""; bool have_heartbeat = router->send_slave_heartbeat && (slave->heartbeat > 0); /** * Save current MARIADB_GTID_INFO detail because * calling blr_file_next_exists() overwrites that */ if (f_tree) { pthread_mutex_lock(&slave->catch_lock); blr_slave_info_save(&slave->f_info, ¤t_info, c_prefix); pthread_mutex_unlock(&slave->catch_lock); } /** * Check now whether the next file exists and it's readable * * If not, handle some cases * if found issue a fake_rotate event */ if (!blr_file_next_exists(router, slave, next_file)) { /** * The next binlog file to read doesn't exist * or it's not set. */ if (router->mariadb10_master_gtid && router->master_state == BLRM_SLAVE_STOPPED && !router->binlog_name[0]) { /** * (1) Don't care about empty router->binlogname in * BLRM_SLAVE_STOPPED state when GTID * registration is on: * set CS_WAIT_DATA and return. */ pthread_mutex_lock(&slave->catch_lock); if (f_tree) { /** * We need to deal with current slave file: * restore first the GTID info into slave->f_info */ memcpy(&slave->f_info, ¤t_info, sizeof(MARIADB_GTID_INFO)); } /** * We force cachtup state to CS_WAIT_DATA now: * * The slave can be called by any new master * event received (no matter which is the binlog file) * or by an heartbeat event. */ slave->cstate = CS_WAIT_DATA; pthread_mutex_unlock(&slave->catch_lock); #ifndef BLFILE_IN_SLAVE /* Close file */ blr_close_binlog(router, file); #endif return rval; } /** * (2) The next file is not available/existent, actions: * * If router state is BLRM_BINLOGDUMP * - abort slave connection if MISSING_FILE_READ_RETRIES is hit * or * - just log a warning message * * Note: in any other router state we don't log messages */ if (router->master_state == BLRM_BINLOGDUMP) { pthread_mutex_lock(&slave->catch_lock); /* Router state is BLRM_BINLOGDUMP (aka replicating) */ if (slave->stats.n_failed_read < MISSING_FILE_READ_RETRIES) { slave->stats.n_failed_read++; pthread_mutex_unlock(&slave->catch_lock); /* Log warning for missing file */ blr_slave_log_next_file_action(router, slave, c_prefix, next_file, SLAVE_EOF_WARNING); } else { /** * Force error and disconnect * when exceeding error counter limit */ slave->state = BLRS_ERRORED; pthread_mutex_unlock(&slave->catch_lock); /* Log error for missing file */ blr_slave_log_next_file_action(router, slave, c_prefix, next_file, SLAVE_EOF_ERROR); /* Send error that stops slave replication */ blr_send_custom_error(slave->dcb, slave->seqno++, 0, "next binlog file to read doesn't exist", "HY000", BINLOG_FATAL_ERROR_READING); #ifndef BLFILE_IN_SLAVE /* Close file */ blr_close_binlog(router, file); #endif /* Disconnect client */ dcb_close(slave->dcb); return 0; } } // No else branch: no further actions /** * We need to deal with current slave file: * restore first the GTID info into slave->f_info */ pthread_mutex_lock(&slave->catch_lock); if (f_tree) { memcpy(&slave->f_info, ¤t_info, sizeof(MARIADB_GTID_INFO)); } /** * We force cachtup state to CS_WAIT_DATA now: * * The slave can be called by any new master * event received (no matter which is the binlog file) * or by an heartbeat event. */ slave->cstate = CS_WAIT_DATA; pthread_mutex_unlock(&slave->catch_lock); } else { /* We may have reached the end of file of a non-current * binlog file. * * Note if the master is rotating there is a window during * which the rotate event has been written to the old binlog * but the new binlog file has not yet been created. Therefore * we ignore these issues during the rotate processing. * * We send a fake_rotate_event to 'next_file' * Note: * slave->f_info updated by previous call to * blr_file_next_exists() */ blr_slave_log_next_file_action(router, slave, c_prefix, next_file, SLAVE_EOF_ROTATE); /* Reset encryption context */ MXS_FREE(slave->encryption_ctx); slave->encryption_ctx = NULL; /* Now pass the next_file to blr_slave_fake_rotate() */ #ifdef BLFILE_IN_SLAVE if (blr_slave_fake_rotate(router, slave, &slave->file, next_file)) #else if (blr_slave_fake_rotate(router, slave, &file, next_file)) #endif { pthread_mutex_lock(&slave->catch_lock); slave->cstate |= CS_EXPECTCB; pthread_mutex_unlock(&slave->catch_lock); /** * Note: * Fake rotate just written to client, * no need to call poll_fake_write_event() */ // blr_slave_fake_rotate closes the file on success file = NULL; } else { /* Set ERROR */ slave->state = BLRS_ERRORED; /* Disconnect client */ dcb_close(slave->dcb); #ifndef BLFILE_IN_SLAVE /* Close file */ blr_close_binlog(router, file); #endif return 0; } } } else { /** * Still reading from current slave file but * nothing has been written to client right now * (perhaps some ignorable / skipped events) * just retry to read again. */ pthread_mutex_lock(&slave->catch_lock); slave->cstate |= CS_EXPECTCB; pthread_mutex_unlock(&slave->catch_lock); poll_fake_write_event(slave->dcb); } } #ifndef BLFILE_IN_SLAVE if (file) { /* Close file */ blr_close_binlog(router, file); } #endif return rval; } /** * The DCB callback used by the slave to obtain DCB_REASON_LOW_WATER callbacks * when the server sends all the the queue data for a DCB. This is the mechanism * that is used to implement the flow control mechanism for the sending of * large quantities of binlog records during the catchup process. * * @param dcb The DCB of the slave connection * @param reason The reason the callback was called * @param data The user data, in this case the server structure */ int blr_slave_callback(DCB* dcb, DCB_REASON reason, void* data) { ROUTER_SLAVE* slave = (ROUTER_SLAVE*)data; ROUTER_INSTANCE* router = slave->router; if (NULL == dcb->session->router_session) { /* * The following processing will fail if there is no router session, * because the "data" parameter will not contain meaningful data, * so we have no choice but to stop here. */ return 0; } if (reason == DCB_REASON_DRAINED) { if (slave->state == BLRS_DUMPING) { pthread_mutex_lock(&slave->catch_lock); if (slave->cstate & CS_BUSY) { pthread_mutex_unlock(&slave->catch_lock); return 0; } slave->cstate &= ~(CS_EXPECTCB); slave->cstate |= CS_BUSY; pthread_mutex_unlock(&slave->catch_lock); slave->stats.n_dcb++; blr_slave_catchup(router, slave, true); } else { MXS_DEBUG("Ignored callback due to slave state %s", blrs_states[slave->state]); } } if (reason == DCB_REASON_LOW_WATER) { if (slave->state == BLRS_DUMPING) { slave->stats.n_cb++; blr_slave_catchup(router, slave, true); } else { slave->stats.n_cbna++; } } return 0; } /** * Rotate the slave to the new binlog file * * @param router The router instance * @param slave The slave instance * @param ptr The rotate event (minus header and OK byte) */ void blr_slave_rotate(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, uint8_t* ptr) { int len = EXTRACT24(ptr + 9); // Extract the event length // Remove length of header and position len = len - (BINLOG_EVENT_HDR_LEN + 8); if (router->master_chksum) { len -= MYSQL_HEADER_LEN; } if (len > BINLOG_FNAMELEN) { len = BINLOG_FNAMELEN; } ptr += BINLOG_EVENT_HDR_LEN; // Skip header slave->binlog_pos = extract_field(ptr, 32); slave->binlog_pos += (((uint64_t)extract_field(ptr + 4, 32)) << 32); memcpy(slave->binlog_name, ptr + 8, len); slave->binlog_name[len] = 0; } /** * Generate an internal rotate event that we can use to cause * the slave to move beyond a binlog file * that is missisng the rotate event at the end. * * The curret binlog file is only closed on success. * * @param router The router instance * @param slave The slave to rotate * @return Non-zero if the rotate took place */ static int blr_slave_fake_rotate(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, BLFILE** filep, const char* new_file) { const char* sptr; int filenum; GWBUF* r_event; MARIADB_GTID_INFO* f_tree = router->storage_type == BLR_BINLOG_STORAGE_TREE ? &slave->f_info : NULL; if ((sptr = strrchr(new_file, '.')) == NULL) { return 0; } /* Set Pos = 4 */ slave->binlog_pos = 4; /* Set Filename */ strcpy(slave->binlog_name, new_file); if ((*filep = blr_open_binlog(router, new_file, f_tree)) == NULL) { return 0; } /* Build Fake Rotate Event */ r_event = blr_build_fake_rotate_event(slave, slave->binlog_pos, new_file, router->masterid); int ret = r_event ? MXS_SESSION_ROUTE_REPLY(slave->dcb->session, r_event) : 0; /* Close binlog file on success */ if (ret) { blr_close_binlog(router, *filep); } return ret; } /** * Read the format description event FDE from current slave logfile * * @param router The router instance * @param slave The slave to send the event to * @return The read FDE event on success or NULL on error */ static GWBUF* blr_slave_read_fde(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave) { BLFILE* file; REP_HEADER hdr; GWBUF* record, * head; uint8_t* ptr; uint32_t chksum; char err_msg[BINLOG_ERROR_MSG_LEN + 1]; MARIADB_GTID_INFO* f_tree = router->storage_type == BLR_BINLOG_STORAGE_TREE ? &slave->f_info : NULL; err_msg[BINLOG_ERROR_MSG_LEN] = '\0'; memset(&hdr, 0, BINLOG_EVENT_HDR_LEN); if ((file = blr_open_binlog(router, slave->binlog_name, f_tree)) == NULL) { return NULL; } /* FDE, at pos 4, is not encrypted, pass NULL to last parameter */ if ((record = blr_read_binlog(router, file, 4, &hdr, err_msg, NULL)) == NULL) { if (hdr.ok != SLAVE_POS_READ_OK) { MXS_ERROR("Slave %s:%i, server-id %d, binlog '%s', " "blr_read_binlog failure: %s", slave->dcb->remote, dcb_get_port(slave->dcb), slave->serverid, slave->binlog_name, err_msg); } blr_close_binlog(router, file); return NULL; } blr_close_binlog(router, file); return record; } /** * Send a "fake" format description event to the newly connected slave * * @param router The router instance * @param slave The slave to send the event to * @return The FDE event size on success or 0 on error */ static uint32_t blr_slave_send_fde(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, GWBUF* fde) { GWBUF* event; uint8_t* ptr; uint32_t chksum; uint32_t event_size; uint8_t* event_ptr; if (fde == NULL) { return 0; } event_size = GWBUF_LENGTH(fde); if ((event = gwbuf_alloc(MYSQL_HEADER_LEN + 1 + event_size)) == NULL) { return 0; } ptr = GWBUF_DATA(event); event_size = GWBUF_LENGTH(fde); /* Set payload to event_size + 1 (the ok/err byte) */ encode_value(ptr, event_size + 1, 32); ptr += 3; *ptr++ = slave->seqno++; *ptr++ = 0; // OK/ERR byte // Copy FDE data memcpy(ptr, GWBUF_DATA(fde), event_size); encode_value(ptr, time(0), 32); // Overwrite timestamp ptr += 13; // 4 time + 1 type + 4 server_id + 4 event_size /* event_ptr points to position of the next event */ encode_value(ptr, 0, 32); // Set next position to 0 /* * Since we have changed the timestamp we must recalculate the CRC * * Position ptr to the start of the event header, * calculate a new checksum * and write it into the header */ ptr = GWBUF_DATA(event) + MYSQL_HEADER_LEN + 1 + event_size - BINLOG_EVENT_CRC_SIZE; chksum = crc32(0L, NULL, 0); chksum = crc32(chksum, GWBUF_DATA(event) + MYSQL_HEADER_LEN + 1, event_size - BINLOG_EVENT_CRC_SIZE); encode_value(ptr, chksum, 32); return MXS_SESSION_ROUTE_REPLY(slave->dcb->session, event); } /** * Send the field count packet in a response packet sequence. * * @param router The router * @param slave The slave connection * @param count Number of columns in the result set * @return Non-zero on success */ static int blr_slave_send_fieldcount(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, int count) { GWBUF* pkt; uint8_t* ptr; if ((pkt = gwbuf_alloc(MYSQL_HEADER_LEN + 1)) == NULL) { return 0; } ptr = GWBUF_DATA(pkt); encode_value(ptr, 1, 24); // Add length of data packet ptr += 3; *ptr++ = 0x01; // Sequence number in response *ptr++ = count; // Number of columns return MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); } /** * Send the column definition packet in a response packet sequence. * * @param router The router * @param slave The slave connection * @param name Name of the column * @param type Column type * @param len Column length * @param seqno Packet sequence number * @return Non-zero on success */ static int blr_slave_send_columndef(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, const char* name, int type, int len, uint8_t seqno) { GWBUF* pkt; uint8_t* ptr; if ((pkt = gwbuf_alloc(26 + strlen(name))) == NULL) { return 0; } ptr = GWBUF_DATA(pkt); encode_value(ptr, 22 + strlen(name), 24); // Add length of data packet ptr += 3; *ptr++ = seqno; // Sequence number in response *ptr++ = 3; // Catalog is always def *ptr++ = 'd'; *ptr++ = 'e'; *ptr++ = 'f'; *ptr++ = 0; // Schema name length *ptr++ = 0; // virtual table name length *ptr++ = 0; // Table name length *ptr++ = strlen(name); // Column name length; while (*name) { *ptr++ = *name++; // Copy the column name } *ptr++ = 0; // Orginal column name *ptr++ = 0x0c; // Length of next fields always 12 *ptr++ = 0x3f; // Character set *ptr++ = 0; encode_value(ptr, len, 32); // Add length of column ptr += 4; *ptr++ = type; *ptr++ = 0x81; // Two bytes of flags if (type == 0xfd) { *ptr++ = 0x1f; } else { *ptr++ = 0x00; } *ptr++ = 0; *ptr++ = 0; *ptr++ = 0; return MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); } /** * Send an EOF packet in a response packet sequence. * * @param router The router * @param slave The slave connection * @param seqno The sequence number of the EOF packet * @return Non-zero on success */ static int blr_slave_send_eof(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, int seqno) { GWBUF* pkt; uint8_t* ptr; if ((pkt = gwbuf_alloc(9)) == NULL) { return 0; } ptr = GWBUF_DATA(pkt); encode_value(ptr, 5, 24); // Add length of data packet ptr += 3; *ptr++ = seqno; // Sequence number in response *ptr++ = 0xfe; // Length of result string encode_value(ptr, 0, 16); // No errors ptr += 2; encode_value(ptr, 2, 16); // Autocommit enabled return MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); } /** * Send the reply only to the SQL command "DISCONNECT SERVER $server_id' * * @param router The binlog router instance * @param slave The slave server to which we are sending the response * @return Non-zero if data was sent */ static int blr_slave_send_disconnected_server(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, int server_id, int found) { GWBUF* pkt; char state[40]; char serverid[40]; uint8_t* ptr; int len, id_len, seqno = 2; sprintf(serverid, "%d", server_id); if (found) { strcpy(state, "disconnected"); } else { strcpy(state, "not found"); } id_len = strlen(serverid); len = MYSQL_HEADER_LEN + (1 + id_len) + (1 + strlen(state)); if ((pkt = gwbuf_alloc(len)) == NULL) { return 0; } blr_slave_send_fieldcount(router, slave, 2); blr_slave_send_columndef(router, slave, "server_id", BLR_TYPE_INT, 40, seqno++); blr_slave_send_columndef(router, slave, "state", BLR_TYPE_STRING, 40, seqno++); blr_slave_send_eof(router, slave, seqno++); ptr = GWBUF_DATA(pkt); encode_value(ptr, len - MYSQL_HEADER_LEN, 24); // Add length of data packet ptr += 3; *ptr++ = seqno++; // Sequence number in response *ptr++ = id_len; // Length of result string memcpy((char*)ptr, serverid, id_len); // Result string ptr += id_len; *ptr++ = strlen(state); // Length of result string memcpy((char*)ptr, state, strlen(state)); // Result string /* ptr += strlen(state); Not required unless more data is to be added */ MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); return blr_slave_send_eof(router, slave, seqno++); } /** * Send the response to the SQL command "DISCONNECT SERVER $server_id' * and close the connection to that server * * @param router The binlog router instance * @param slave The connected slave server * @param server_id The slave server_id to disconnect * @return Non-zero if data was sent to the client */ static int blr_slave_disconnect_server(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, int server_id) { ROUTER_SLAVE* sptr; int n; int server_found = 0; pthread_mutex_lock(&router->lock); sptr = router->slaves; /* look for server_id among all registered slaves */ while (sptr) { /* don't examine slaves with state = 0 */ if ((sptr->state == BLRS_REGISTERED || sptr->state == BLRS_DUMPING) && sptr->serverid == server_id) { /* server_id found */ server_found = 1; MXS_NOTICE("%s: Slave %s, server id %d, disconnected by %s@%s", router->service->name(), sptr->dcb->remote, server_id, slave->dcb->user, slave->dcb->remote); /* send server_id with disconnect state to client */ n = blr_slave_send_disconnected_server(router, slave, server_id, 1); sptr->state = BLRS_UNREGISTERED; dcb_close_in_owning_thread(sptr->dcb); break; } else { sptr = sptr->next; } } pthread_mutex_unlock(&router->lock); /** * Server id was not found * send server_id with not found state to the client */ if (!server_found) { n = blr_slave_send_disconnected_server(router, slave, server_id, 0); } if (n == 0) { MXS_ERROR("gwbuf memory allocation in " "DISCONNECT SERVER server_id [%d]", sptr->serverid); blr_slave_send_error(router, slave, "Memory allocation error for DISCONNECT SERVER"); } return 1; } /** * Send the response to the SQL command "DISCONNECT ALL' * and close the connection to all slave servers * * @param router The binlog router instance * @param slave The connected slave server * @return Non-zero if data was sent to the client */ static int blr_slave_disconnect_all(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave) { ROUTER_SLAVE* sptr; char server_id[40]; char state[40]; uint8_t* ptr; int len, seqno = 2; GWBUF* pkt; /* preparing output result */ blr_slave_send_fieldcount(router, slave, 2); blr_slave_send_columndef(router, slave, "server_id", BLR_TYPE_INT, 40, seqno++); blr_slave_send_columndef(router, slave, "state", BLR_TYPE_STRING, 40, seqno++); blr_slave_send_eof(router, slave, seqno++); pthread_mutex_lock(&router->lock); sptr = router->slaves; while (sptr) { /* skip servers with state = 0 */ if (sptr->state == BLRS_REGISTERED || sptr->state == BLRS_DUMPING) { sprintf(server_id, "%d", sptr->serverid); sprintf(state, "disconnected"); len = MYSQL_HEADER_LEN + 1 + strlen(server_id) + strlen(state) + 1; if ((pkt = gwbuf_alloc(len)) == NULL) { MXS_ERROR("gwbuf memory allocation in " "DISCONNECT ALL for [%s], server_id [%d]", sptr->dcb->remote, sptr->serverid); pthread_mutex_unlock(&router->lock); blr_slave_send_error(router, slave, "Memory allocation error for DISCONNECT ALL"); return 1; } MXS_NOTICE("%s: Slave %s, server id %d, disconnected by %s@%s", router->service->name(), sptr->dcb->remote, sptr->serverid, slave->dcb->user, slave->dcb->remote); ptr = GWBUF_DATA(pkt); encode_value(ptr, len - MYSQL_HEADER_LEN, 24); // Add length of data packet ptr += 3; *ptr++ = seqno++; // Sequence number in response *ptr++ = strlen(server_id); // Length of result string memcpy((char*)ptr, server_id, strlen(server_id)); // Result string ptr += strlen(server_id); *ptr++ = strlen(state); // Length of result string memcpy((char*)ptr, state, strlen(state)); // Result string ptr += strlen(state); MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); sptr->state = BLRS_UNREGISTERED; dcb_close_in_owning_thread(sptr->dcb); } sptr = sptr->next; } pthread_mutex_unlock(&router->lock); blr_slave_send_eof(router, slave, seqno); return 1; } /** * Send a MySQL OK packet to the connected client * * @param router The binlog router instance * @param slave The slave server to which we are sending data * * @return Result of a write call, non-zero if successful */ static int blr_slave_send_ok(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave) { GWBUF* pkt; uint8_t ok_packet[] = { 7, 0, 0,// Payload length 1, // Seqno, 0, // OK, 0, 0, 2, 0, 0, 0 }; if ((pkt = gwbuf_alloc(sizeof(ok_packet))) == NULL) { return 0; } memcpy(GWBUF_DATA(pkt), ok_packet, sizeof(ok_packet)); return MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); } /** * Send a MySQL OK packet with a message to the client * * @param router The binlog router instance * @param message The message to send * @param slave The slave server to which we are sending data * * @return The write call result: non-zero on success */ static int blr_slave_send_ok_message(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, char* message) { GWBUF* pkt; uint8_t* ptr; if ((pkt = gwbuf_alloc(11 + strlen(message) + 1)) == NULL) { return 0; } ptr = GWBUF_DATA(pkt); *ptr++ = 7 + strlen(message) + 1; // Payload length *ptr++ = 0; *ptr++ = 0; *ptr++ = 1; // Seqno *ptr++ = 0; // ok *ptr++ = 0; *ptr++ = 0; *ptr++ = 2; *ptr++ = 0; if (strlen(message) == 0) { *ptr++ = 0; *ptr++ = 0; } else { *ptr++ = 1; *ptr++ = 0; *ptr++ = strlen(message); strcpy((char*)ptr, message); } return MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); } /** * Stop current replication from master * * @param router The binlog router instance * @param slave The slave server to which we are sending the response * @return Always 1 for error, for send_ok the bytes sent * */ static int blr_stop_slave(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave) { /* if unconfigured return an error */ if (router->master_state == BLRM_UNCONFIGURED) { blr_slave_send_warning_message(router, slave, "1255:Slave already has been stopped"); return 1; } /* if already stopped return an error */ if (router->master_state == BLRM_SLAVE_STOPPED) { blr_slave_send_warning_message(router, slave, "1255:Slave already has been stopped"); return 1; } if (router->master) { if (router->master->fd != -1 && router->master->state == DCB_STATE_POLLING) { blr_close_master_in_main(router); } } pthread_mutex_lock(&router->lock); router->master_state = BLRM_SLAVE_STOPPED; /* set last_safe_pos */ router->last_safe_pos = router->binlog_position; /** * Set router->prevbinlog to router->binlog_name * The FDE event with current filename may arrive * after STOP SLAVE is received */ if (strcmp(router->binlog_name, router->prevbinlog) != 0) { strcpy(router->prevbinlog, router->binlog_name); // Same size } if (router->client) { if (router->client->fd != -1 && router->client->state == DCB_STATE_POLLING) { // Is this dead code? dcb->fd for internal DCBs is always -1 dcb_close(router->client); router->client = NULL; } } /* Now it is safe to unleash other threads on this router instance */ router->reconnect_pending = 0; router->active_logs = 0; pthread_mutex_unlock(&router->lock); MXS_NOTICE("%s: STOP SLAVE executed by %s@%s. Disconnecting from master [%s]:%d, " "read up to log %s, pos %lu, transaction safe pos %lu", router->service->name(), slave->dcb->user, slave->dcb->remote, router->service->dbref->server->address, router->service->dbref->server->port, router->binlog_name, router->current_pos, router->binlog_position); if (router->trx_safe && router->pending_transaction.state > BLRM_NO_TRANSACTION) { char message[BINLOG_ERROR_MSG_LEN + 1] = ""; snprintf(message, BINLOG_ERROR_MSG_LEN, "1105:Stopped slave mid-transaction in binlog file %s, " "pos %lu, incomplete transaction starts at pos %lu", router->binlog_name, router->current_pos, router->binlog_position); return blr_slave_send_warning_message(router, slave, message); } else { return blr_slave_send_ok(router, slave); } } /** * Start replication from current configured master * * @param router The binlog router instance * @param slave The slave server to which we are sending the response * @return Always 1 for error, for send_ok the bytes sent * */ static int blr_start_slave(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave) { /* if unconfigured return an error */ if (router->master_state == BLRM_UNCONFIGURED) { blr_slave_send_error_packet(slave, "The server is not configured as slave; " "fix in config file or with CHANGE MASTER TO", (unsigned int)1200, NULL); return 1; } /* if running return an error */ if (router->master_state != BLRM_UNCONNECTED && router->master_state != BLRM_SLAVE_STOPPED && router->master_state != BLRM_CONNECTING) { blr_slave_send_warning_message(router, slave, "1254:Slave is already running"); return 1; } pthread_mutex_lock(&router->lock); router->master_state = BLRM_UNCONNECTED; router->retry_count = 0; router->config_index = 0; // Always start from the default configuration. pthread_mutex_unlock(&router->lock); /** * Check whether to create the new binlog (router->binlog_name) * * File handling happens only if mariadb10_master_gtid is off: * with Master GTID the first file will be created/opened * by the fake Rotate Event. */ /* Check first for incomplete transaction */ if (strlen(router->prevbinlog) && strcmp(router->prevbinlog, router->binlog_name) != 0) { if (router->trx_safe && router->pending_transaction.state > BLRM_NO_TRANSACTION) { char msg[BINLOG_ERROR_MSG_LEN + 1] = ""; char file[PATH_MAX + 1] = ""; struct stat statb; unsigned long filelen = 0; char t_prefix[BINLOG_FILE_EXTRA_INFO] = ""; // Add file prefix if (router->storage_type == BLR_BINLOG_STORAGE_TREE) { sprintf(t_prefix, "%" PRIu32 "/%" PRIu32 "/", router->mariadb10_gtid_domain, router->orig_masterid); } // Router current file snprintf(file, PATH_MAX, "%s/%s%s", router->binlogdir, t_prefix, router->prevbinlog); /* Get file size */ if (stat(file, &statb) == 0) { filelen = statb.st_size; } /* Prepare warning message */ snprintf(msg, BINLOG_ERROR_MSG_LEN, "1105:Truncated partial transaction in file %s%s, " "starting at pos %lu, " "ending at pos %lu. File %s now has length %lu.", t_prefix, router->prevbinlog, router->last_safe_pos, filelen, router->prevbinlog, router->last_safe_pos); /* Truncate previous binlog file to last_safe pos */ if (truncate(file, router->last_safe_pos) == -1) { MXS_ERROR("Failed to truncate file: %d, %s", errno, mxs_strerror(errno)); } /* Log it */ MXS_WARNING("A transaction is still opened at pos %lu" " File %s%s will be truncated. " "Next binlog file is %s at pos %d, " "START SLAVE is required again.", router->last_safe_pos, t_prefix, router->prevbinlog, router->binlog_name, 4); pthread_mutex_lock(&router->lock); router->pending_transaction.state = BLRM_NO_TRANSACTION; router->last_safe_pos = 0; router->master_state = BLRM_UNCONNECTED; router->current_pos = 4; router->binlog_position = 4; router->current_safe_event = 4; pthread_mutex_unlock(&router->lock); /* Send warning message to mysql command */ blr_slave_send_warning_message(router, slave, msg); return 1; } /* No pending transaction */ else { /** * If router->mariadb10_master_gtid is Off then * handle file create/append. * This means the domain_id and server_id * are not taken into account for filename prefix. */ if (!router->mariadb10_master_gtid) { /* If the router file is not open, create a new binlog file */ if (router->binlog_fd == -1) { blr_file_new_binlog(router, router->binlog_name); } else { /* A new binlog file has been created and opened * by CHANGE MASTER TO: use it */ blr_file_append(router, router->binlog_name); } } } } /** Start replication from master */ blr_start_master_in_main(router); MXS_NOTICE("%s: START SLAVE executed by %s@%s. Trying connection to master [%s]:%d, " "binlog %s, pos %lu, transaction safe pos %lu", router->service->name(), slave->dcb->user, slave->dcb->remote, router->service->dbref->server->address, router->service->dbref->server->port, router->binlog_name, router->current_pos, router->binlog_position); /* Try reloading new users and update cached credentials */ service_refresh_users(router->service); return blr_slave_send_ok(router, slave); } /** * Construct an error packet reply with specified code and status * * @param slave The slave server instance * @param msg The error message to send * @param err_num The error number to send * @param status The error status */ static void blr_slave_send_error_packet(ROUTER_SLAVE* slave, const char* msg, unsigned int err_num, const char* status) { GWBUF* pkt; unsigned char* data; int len; unsigned int mysql_errno = 0; const char* mysql_state; if ((pkt = gwbuf_alloc(strlen(msg) + 13)) == NULL) { return; } if (status != NULL) { mysql_state = status; } else { mysql_state = "HY000"; } if (err_num > 0) { mysql_errno = err_num; } else { mysql_errno = (unsigned int)2003; } data = GWBUF_DATA(pkt); len = strlen(msg) + 9; encode_value(&data[0], len, 24); // Payload length data[3] = 1; // Sequence id data[4] = 0xff; // Error indicator encode_value(&data[5], mysql_errno, 16); // Error Code data[7] = '#'; // Status message first char memcpy((char*)&data[8], mysql_state, 5);// Status message memcpy(&data[13], msg, strlen(msg)); // Error Message MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); } bool ChangeMasterOptions::validate(ROUTER_INSTANCE* router, char* error, ChangeMasterConfig* config) { /* Abort if MASTER_USE_GTID is in use and * router->mariadb10_master_gtid is not set */ if (!router->mariadb10_master_gtid && !this->use_mariadb10_gtid.empty()) { snprintf(error, BINLOG_ERROR_MSG_LEN, "Cannot use MASTER_USE_GTID. " "Enable 'mariadb10_master_gtid' option first."); MXS_ERROR("%s: %s", router->service->name(), error); return false; } int heartbeat_period = -1; if (!this->heartbeat_period.empty()) { heartbeat_period = (int)strtol(this->heartbeat_period.c_str(), NULL, 10); if (heartbeat_period < 0 || (errno == ERANGE) || heartbeat_period > BLR_HEARTBEAT_MAX_INTERVAL) { snprintf(error, BINLOG_ERROR_MSG_LEN, "The requested value for the heartbeat period is " "either negative or exceeds the maximum allowed " "(%d seconds).", BLR_HEARTBEAT_MAX_INTERVAL); MXS_ERROR("%s: %s", router->service->name(), error); return false; } } int connect_retry = -1; if (!this->connect_retry.empty()) { connect_retry = (int)strtol(this->connect_retry.c_str(), NULL, 10); if (connect_retry <= 0 || (errno == ERANGE)) { snprintf(error, BINLOG_ERROR_MSG_LEN, "The requested value for MASTER_CONNECT_RETRY " "interval is not valid: %s.", this->connect_retry.c_str()); MXS_ERROR("%s: %s", router->service->name(), error); return false; } } int port = -1; if (!this->port.empty()) { port = (int)strtol(this->port.c_str(), NULL, 10); if ((port < 0) || (port > std::numeric_limits::max())) { snprintf(error, BINLOG_ERROR_MSG_LEN, "The specified value for MASTER_PORT " "is not valid: %s.", this->port.c_str()); MXS_ERROR("%s: %s", router->service->name(), error); return false; } } config->connection_name = this->connection_name; config->host = this->host; config->port = port; config->binlog_file = this->binlog_file; config->binlog_pos = this->binlog_pos; config->user = this->user; config->password = this->password; config->ssl_key = this->ssl_key; config->ssl_cert = this->ssl_cert; config->ssl_ca = this->ssl_ca; config->ssl_enabled = this->ssl_enabled.empty() ? false : atoi(this->ssl_enabled.c_str()); config->ssl_version = this->ssl_version; config->use_mariadb10_gtid = this->use_mariadb10_gtid; config->heartbeat_period = heartbeat_period; config->connect_retry = connect_retry; return true; } namespace { int validate_connection_name(ROUTER_INSTANCE* router, const std::string& name, char* error) { static const char DEFAULT_MESSAGE[] = "If a connection name is provided, it must be of the format ':N' where N " "is an integer larger than 1."; int index = -1; char custom_message[BINLOG_ERROR_MSG_LEN]; const char* message = DEFAULT_MESSAGE; if (name.length() == 0) { index = 0; message = nullptr; } else if (name.length() == 1) { // We are fine with 'index' == -1, and the message being the default message. } else // name.length() >= 2. { if (name.front() == ':') { string tail = name.substr(1); int n = strtol(tail.c_str(), NULL, 10); if ((n > 1) && (std::to_string(n) == tail)) // Nothing funky in the string { if (router->configs.size() == static_cast(n - 1)) { index = n - 1; message = nullptr; } else if (router->configs.size() == 0) { snprintf(custom_message, BINLOG_ERROR_MSG_LEN, "The provided connection name '%s' is not valid. Currently " "no default connection exists and it must be specified using " "a 'CHANGE MASTER TO ...' command without a connection name.", name.c_str()); message = custom_message; } else { snprintf(custom_message, BINLOG_ERROR_MSG_LEN, "The provided connection name '%s' is not valid. Currently " "the default connection and %d secondary connections have " "been specified and the next valid name for an secondary " "connection is ':%d'.", name.c_str(), (int)router->configs.size() - 1, (int)router->configs.size() + 1); message = custom_message; } } } } if (message) { snprintf(error, BINLOG_ERROR_MSG_LEN, "%s", message); } return index; } } namespace { int blr_apply_change_master_0(ROUTER_INSTANCE* router, const ChangeMasterConfig& new_config, char* error) { mxb_assert(new_config.connection_name.empty()); MasterServerConfig current_master; /* save current config option data */ blr_master_get_config(router, ¤t_master); /* Change the heartbeat */ if (new_config.heartbeat_period != -1) { if (new_config.heartbeat_period == 0) { blr_log_disabled_heartbeat(router); } router->heartbeat = new_config.heartbeat_period; } /* Change the connect retry */ if (new_config.connect_retry != -1) { router->retry_interval = new_config.connect_retry; } /* Change the replication user */ blr_set_master_user(router, new_config.user); /* Change the replication password */ blr_set_master_password(router, new_config.password); /* Change the master name/address */ blr_set_master_hostname(router, new_config.host); /* Change the master port */ blr_set_master_port(router, new_config.port); /* Handle SSL options */ int ssl_error; ssl_error = blr_set_master_ssl(router, new_config, error); if (ssl_error != -1 && // No CA cert is defined or only one of CERT or KEY is defined (new_config.ssl_ca.empty() || new_config.ssl_cert.empty() != new_config.ssl_key.empty())) { if (new_config.ssl_enabled) { snprintf(error, BINLOG_ERROR_MSG_LEN, "MASTER_SSL=1 but some required options are missing: " "check that at least MASTER_SSL_CA is defined"); ssl_error = -1; } } if (ssl_error == -1) { blr_abort_change_master(router, current_master, error); return -1; } /** * Check if binlog file can be changed * * Change the binlog filename as from MASTER_LOG_FILE * New binlog file could be the next one or current one * or empty if router->mariadb10_master_gtid is set. */ char* master_logfile = NULL; if (!blr_binlog_change_check(router, new_config, error) || !blr_change_binlog_name(router, new_config.binlog_file, &master_logfile, error) || !blr_apply_changes(router, new_config, master_logfile, error)) { blr_abort_change_master(router, current_master, error); MXS_FREE(master_logfile); return -1; } /* Log config changes (without passwords) */ blr_log_config_changes(router, current_master, new_config); MXS_FREE(master_logfile); int change_binlog = 0; if (router->master_state == BLRM_UNCONFIGURED) { change_binlog = 1; } return change_binlog; } } static int blr_apply_change_master(ROUTER_INSTANCE* router, int index, const ChangeMasterConfig& config, char* error) { int rc = 0; pthread_mutex_lock(&router->lock); if (index == static_cast(router->configs.size())) { router->configs.push_back(config); } else { mxb_assert(index < static_cast(router->configs.size())); router->configs[index] = config; } if (index == 0) { rc = blr_apply_change_master_0(router, config, error); } pthread_mutex_unlock(&router->lock); return rc; } /** * Get the connection name from a CHANGE MASTER command. * * @param command What follows "CHANGE MASTER", that is " ['connection-name'] TO ..." * @param [out] pConnection_name The connection name, if provided. * * @return The string following "TO". */ static char* get_connection_name(char* command, std::string* pConnection_name) { command = mxb::ltrim(command); char* to = strcasestr(command, "TO"); if (!to) { // No "TO", can't be valid. command = nullptr; } else if (to == command) { // No connection name. command = to + 2; } else { // We may have the case "'connection-name' TO" char quote = *command; if ((quote == '\'') || (quote == '"')) { // At least there was a quote. ++command; char* end = strchr(command, quote); if (!end || (end > to)) { // No closing quote. command = nullptr; } else { *pConnection_name = std::string(command, end); command = to + 2; } } else { // No, must be an invalid command. command = nullptr; } } return command; } /** * handle a 'change master' operation * * @param router The router instance * @param command The change master SQL command * @param error The error message, preallocated * BINLOG_ERROR_MSG_LEN + 1 bytes * @return 0 on success, * 1 on success with new binlog, -1 on failure */ static int blr_handle_change_master(ROUTER_INSTANCE* router, char* command, char* error) { std::string connection_name; command = get_connection_name(command, &connection_name); if (!command) { static const char MESSAGE[] = "statement doesn't have the CHANGE MASTER TO syntax"; mxb_assert(sizeof(MESSAGE) <= BINLOG_ERROR_MSG_LEN); strcpy(error, MESSAGE); return -1; } int index = validate_connection_name(router, connection_name, error); if (index == -1) { // An error was already generated. return -1; } std::vector cmd_string(command, command + strlen(command) + 1); // Include the NULL /* Parse SQL command and populate the change_master struct */ ChangeMasterOptions options(connection_name); if (index < static_cast(router->configs.size())) { // An existing configuration, pick defaults from it. options.set_defaults(router->configs[index]); } else if (index != 0) { mxb_assert(index == static_cast(router->configs.size())); // A new configuration, pick defaults from the default configuration. options.set_defaults(router->configs[0]); options.host.clear(); } string host = options.host; options.host.clear(); // So that we can detect whether it is set, even to the same value. if (blr_parse_change_master_command(&cmd_string.front(), error, &options) != 0) { MXS_ERROR("%s CHANGE MASTER TO parse error: %s", router->service->name(), error); return -1; } string use_gtid = options.use_mariadb10_gtid; if (!use_gtid.empty() && (strcasecmp(use_gtid.c_str(), "slave_pos") != 0)) { static const char MESSAGE[] = "Only MASTER_USE_GTID=Slave_pos is allowed."; mxb_assert(sizeof(MESSAGE) <= BINLOG_ERROR_MSG_LEN); strcpy(error, MESSAGE); return -1; } ChangeMasterConfig config; if (!options.validate(router, error, &config)) { return -1; } if (config.host.empty()) // Empty, if it was not specified in the options. { config.host = host; } if ((index == 0) && !options.host.empty()) { // If we are manipulating the default configuration and a host is specified, // even if it would be the same, then we reset the setup. router->configs.clear(); } return blr_apply_change_master(router, index, config, error); } /* * Set new master hostname * * @param router Current router instance * @param hostname The hostname to set * @return 1 for applied change, 0 otherwise */ static int blr_set_master_hostname(ROUTER_INSTANCE* router, const char* hostname) { if (hostname) { mxb_assert((*hostname != '\'') && (*hostname != '"')); router->service->dbref->server->server_update_address(hostname); MXS_INFO("%s: New MASTER_HOST is [%s]", router->service->name(), router->service->dbref->server->address); return 1; } return 0; } static int blr_set_master_hostname(ROUTER_INSTANCE* router, const std::string& hostname) { return blr_set_master_hostname(router, hostname.empty() ? nullptr : hostname.c_str()); } /** * Set new master port * * @param router Current router instance * @param port The server TCP port * @return 1 for applied change, 0 otherwise */ static int blr_set_master_port(ROUTER_INSTANCE* router, int port) { if (port > 0) { router->service->dbref->server->update_port(port); MXS_INFO("%s: New MASTER_PORT is [%i]", router->service->name(), router->service->dbref->server->port); return 1; } return 0; } /* * Set new master binlog file * * The routing must be called holding router->lock * * @param router Current router instance * @param filename Binlog file name * @param error The error msg for command, * pre-allocated BINLOG_ERROR_MSG_LEN + 1 bytes * @return New binlog file or NULL on error */ static char* blr_set_master_logfile(ROUTER_INSTANCE* router, const char* filename, char* error) { char* new_binlog_file = NULL; if (filename) { mxb_assert((*filename != '\'') && (*filename != '"')); long next_binlog_seqname; char* file_ptr; char* end; file_ptr = (char*)filename; /* check binlog filename format */ end = strchr(file_ptr, '.'); if (!end) { snprintf(error, BINLOG_ERROR_MSG_LEN, "Selected binlog [%s] is not in the format" " '%s.yyyyyy'", file_ptr, router->fileroot); return NULL; } end++; if (router->master_state == BLRM_UNCONFIGURED) { char* stem_end; stem_end = strrchr(file_ptr, '.'); /* set filestem */ if (stem_end) { if (router->fileroot) { MXS_FREE(router->fileroot); } router->fileroot = strndup(file_ptr, stem_end - file_ptr); } } else { /* get next binlog file name, assuming filestem is the same */ next_binlog_seqname = blr_file_get_next_binlogname(router); if (!next_binlog_seqname) { snprintf(error, BINLOG_ERROR_MSG_LEN, "Cannot get the next MASTER_LOG_FILE name " "from current binlog [%s]", router->binlog_name); return NULL; } /* Compare binlog file name with current one */ if (strcmp(router->binlog_name, file_ptr) == 0) { /** * No binlog name change, * a new position will be checked later */ } else { /** * This is a new binlog file request * If file is not the next one return an error */ if (atoi(end) != next_binlog_seqname) { snprintf(error, BINLOG_ERROR_MSG_LEN, "Can not set MASTER_LOG_FILE to %s: " "Permitted binlog file names are " "%s or %s.%06li. Current master_log_file=%s, " "master_log_pos=%lu", file_ptr, router->binlog_name, router->fileroot, next_binlog_seqname, router->binlog_name, router->current_pos); return NULL; } /* Binlog file name succesfully changed */ } } if (strlen(file_ptr) <= BINLOG_FNAMELEN) { new_binlog_file = MXS_STRDUP(file_ptr); } else { snprintf(error, BINLOG_ERROR_MSG_LEN, "Can not set MASTER_LOG_FILE to %s: Maximum length is %d.", file_ptr, BINLOG_FNAMELEN); } } return new_binlog_file; } static char* blr_set_master_logfile(ROUTER_INSTANCE* router, const std::string& filename, char* error) { return blr_set_master_logfile(router, filename.empty() ? nullptr : filename.c_str(), error); } /** * Get master configuration store it * * @param router Current router instance * @param curr_master Preallocated struct to fill */ static void blr_master_get_config(ROUTER_INSTANCE* router, MasterServerConfig* curr_master) { curr_master->port = router->service->dbref->server->port; curr_master->host = router->service->dbref->server->address; curr_master->pos = router->current_pos; curr_master->safe_pos = router->binlog_position; curr_master->logfile = router->binlog_name; curr_master->user = router->user; curr_master->password = router->password; curr_master->filestem = router->fileroot; /* SSL options */ auto server_ssl = router->service->dbref->server->ssl().config(); if (server_ssl && !server_ssl->empty()) { curr_master->ssl_enabled = router->ssl_enabled; if (router->ssl_version) { curr_master->ssl_version = router->ssl_version; } if (!server_ssl->key.empty()) { curr_master->ssl_key = server_ssl->key; } if (!server_ssl->cert.empty()) { curr_master->ssl_cert = server_ssl->cert; } if (!server_ssl->ca.empty()) { curr_master->ssl_ca = server_ssl->ca; } } /* Connect options */ curr_master->heartbeat = router->heartbeat; } /** * Restore master configuration values for host and port * * @param router Current router instance * @param prev_master Previous saved master configuration */ static void blr_master_restore_config(ROUTER_INSTANCE* router, const MasterServerConfig& prev_master) { router->service->dbref->server->server_update_address(prev_master.host); router->service->dbref->server->update_port(prev_master.port); router->ssl_enabled = prev_master.ssl_enabled; if (!prev_master.ssl_version.empty()) { MXS_FREE(router->ssl_version); router->ssl_version = MXS_STRDUP_A(prev_master.ssl_version.c_str()); } router->heartbeat = prev_master.heartbeat; } /** * Set all the master configuration fields to empty values * * @param router Current router instance */ static void blr_master_set_empty_config(ROUTER_INSTANCE* router) { router->service->dbref->server->server_update_address("none"); router->service->dbref->server->update_port(3306); router->current_pos = 4; router->binlog_position = 4; router->current_safe_event = 4; strcpy(router->binlog_name, ""); strcpy(router->prevbinlog, ""); /* Set Empty master id */ router->orig_masterid = 0; /* Set Default GTID domain */ router->mariadb10_gtid_domain = BLR_DEFAULT_GTID_DOMAIN_ID; } /** * Restore all master configuration values * * @param router Current router instance * @param prev_master Previous saved master configuration */ static void blr_master_apply_config(ROUTER_INSTANCE* router, const MasterServerConfig& prev_master) { router->service->dbref->server->server_update_address(prev_master.host); router->service->dbref->server->update_port(prev_master.port); router->current_pos = prev_master.pos; router->binlog_position = prev_master.safe_pos; router->current_safe_event = prev_master.safe_pos; strcpy(router->binlog_name, prev_master.logfile.c_str()); if (router->user) { MXS_FREE(router->user); router->user = MXS_STRDUP_A(prev_master.user.c_str()); } if (router->password) { MXS_FREE(router->password); router->password = MXS_STRDUP_A(prev_master.password.c_str()); } if (router->fileroot) { MXS_FREE(router->fileroot); router->fileroot = MXS_STRDUP_A(prev_master.filestem.c_str()); } router->heartbeat = prev_master.heartbeat; } /** * Change the replication user * * @param router Current router instance * @param user The userto set * @return 1 for applied change, 0 otherwise */ static int blr_set_master_user(ROUTER_INSTANCE* router, const char* user) { if (user != NULL) { mxb_assert((*user != '\'') && (*user != '"')); if (router->user) { MXS_FREE(router->user); } router->user = MXS_STRDUP_A(user); MXS_INFO("%s: New MASTER_USER is [%s]", router->service->name(), router->user); return 1; } return 0; } static int blr_set_master_user(ROUTER_INSTANCE* router, const std::string& user) { return blr_set_master_user(router, user.empty() ? nullptr : user.c_str()); } /** * Change the replication password * * @param router Current router instance * @param password The password to set * @return 1 for applied change, 0 otherwise */ static int blr_set_master_password(ROUTER_INSTANCE* router, const char* password) { if (password != NULL) { mxb_assert((*password != '\'') && (*password != '"')); if (router->password) { MXS_FREE(router->password); } router->password = MXS_STRDUP_A(password); /* don't log new password */ return 1; } return 0; } static int blr_set_master_password(ROUTER_INSTANCE* router, const std::string& password) { return blr_set_master_password(router, password.empty() ? nullptr : password.c_str()); } /** * Get next token * * Works exactly like strtok_t except that a delim character which appears * anywhere within quotes is ignored. For instance, if delim is "," then * a string like "MASTER_USER='maxscale_repl_user',MASTER_PASSWORD='a,a'" * will be tokenized into the following two tokens: * * MASTER_USER='maxscale_repl_user' * MASTER_PASSWORD='a,a' * * @see strtok_r */ static char* get_next_token(char* str, const char* delim, char** saveptr) { if (str) { *saveptr = str; } if (!*saveptr) { return NULL; } bool delim_found = true; // Skip any delims in the beginning. while (**saveptr && delim_found) { const char* d = delim; while (*d) { if (*d == **saveptr) { break; } ++d; } if (*d == 0) { delim_found = false; } else { ++*saveptr; } } if (!**saveptr) { return NULL; } delim_found = false; char* token = *saveptr; char* p = *saveptr; char quote = 0; while (*p && !delim_found) { switch (*p) { case '\'': case '"': case '`': if (!quote) { quote = *p; } else if (quote == *p) { quote = 0; } break; default: if (!quote) { const char* d = delim; while (*d && !delim_found) { if (*p == *d) { delim_found = true; *p = 0; } else { ++d; } } } } ++p; } if (*p == 0) { *saveptr = NULL; } else if (delim_found) { *saveptr = p; delim_found = true; while (**saveptr && delim_found) { const char* d = delim; while (*d) { if (**saveptr == *d) { break; } else { ++d; } } if (*d == 0) { delim_found = false; } else { ++*saveptr; } } } return token; } /** * Parse a CHANGE MASTER TO SQL command * * @param input The command to be parsed * @param error_string Pre-allocated string for error message, * BINLOG_ERROR_MSG_LEN + 1 bytes * @param config master option struct to fill * @return 0 on success, 1 on failure */ static int blr_parse_change_master_command(char* input, char* error_string, ChangeMasterOptions* config) { const char* sep = ","; char* word, * brkb; if ((word = get_next_token(input, sep, &brkb)) == NULL) { snprintf(error_string, BINLOG_ERROR_MSG_LEN, "Unable to parse query [%s]", input); return 1; } else { /* parse options key=val */ if (blr_handle_change_master_token(word, error_string, config)) { return 1; } } while ((word = get_next_token(NULL, sep, &brkb)) != NULL) { /* parse options key=val */ if (blr_handle_change_master_token(word, error_string, config)) { return 1; } } return 0; } /** * Validate option and set the value for a change master option * * @param input Current option with value * @param error pre-allocted string for error message, * BINLOG_ERROR_MSG_LEN + 1 bytes * @param config master option struct to fill * @return 0 on success, 1 on error */ static int blr_handle_change_master_token(char* input, char* error, ChangeMasterOptions* config) { /* space+TAB+= */ const char* sep = " \t="; char* word, * brkb; if ((word = get_next_token(input, sep, &brkb)) == NULL) { snprintf(error, BINLOG_ERROR_MSG_LEN, "error parsing %s", brkb); return 1; } else { std::string* option_field; if ((option_field = blr_validate_change_master_option(word, config)) == NULL) { snprintf(error, BINLOG_ERROR_MSG_LEN, "option '%s' is not supported", word); return 1; } std::string value; if (!blr_get_parsed_command_value(brkb, &value)) { snprintf(error, BINLOG_ERROR_MSG_LEN, "missing value for '%s'", word); return 1; } else { *option_field = value; } } return 0; } /** * Get value of a change master option * * @param input Current option with value * @return The new allocated option value or NULL */ static bool blr_get_parsed_command_value(char* input, std::string* output) { bool ret = false; if (input && *input) { char value[strlen(input) + 1]; strcpy(value, input); /* space+TAB+= */ const char* sep = " \t="; char* word; if ((word = get_next_token(NULL, sep, &input)) != NULL) { /* remove trailing spaces */ char* ptr = value + strlen(value) - 1; while (ptr > value && isspace(*ptr)) { *ptr-- = 0; } // Remove surrounding quotes. char* p = strstr(value, word); if (*p == '\'' || *p == '"') { char quote = *p; ++p; int len = strlen(p); if (len > 0 && p[len - 1] == quote) { p[len - 1] = 0; } } *output = p; ret = true; } } return ret; } /** * Validate a change master option * * @param option The option to check * @param config The option structure * @return A pointer to the field in the option strucure or NULL */ static std::string* blr_validate_change_master_option(const char* option, ChangeMasterOptions* config) { if (strcasecmp(option, "master_host") == 0) { return &config->host; } else if (strcasecmp(option, "master_port") == 0) { return &config->port; } else if (strcasecmp(option, "master_log_file") == 0) { return &config->binlog_file; } else if (strcasecmp(option, "master_log_pos") == 0) { return &config->binlog_pos; } else if (strcasecmp(option, "master_user") == 0) { return &config->user; } else if (strcasecmp(option, "master_password") == 0) { return &config->password; } else if (strcasecmp(option, "master_ssl") == 0) { return &config->ssl_enabled; } else if (strcasecmp(option, "master_ssl_key") == 0) { return &config->ssl_key; } else if (strcasecmp(option, "master_ssl_cert") == 0) { return &config->ssl_cert; } else if (strcasecmp(option, "master_ssl_ca") == 0) { return &config->ssl_ca; } else if (strcasecmp(option, "master_ssl_version") == 0 || strcasecmp(option, "master_tls_version") == 0) { return &config->ssl_version; } else if (strcasecmp(option, "master_use_gtid") == 0) { return &config->use_mariadb10_gtid; } else if (strcasecmp(option, "master_heartbeat_period") == 0) { return &config->heartbeat_period; } else if (strcasecmp(option, "master_connect_retry") == 0) { return &config->connect_retry; } else { return NULL; } } /** * Send a MySQL protocol response for selected variable * * @param router The binlog router instance * @param slave The connected slave server * @param variable The variable name * @param value The variable value * @param column_type The variable value type (string or int) * @return Non-zero if data was sent */ static int blr_slave_send_var_value(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, const char* variable, const char* value, int column_type) { GWBUF* pkt; uint8_t* ptr; int len, vers_len; if (value == NULL) { return blr_slave_send_ok(router, slave); } vers_len = strlen(value); blr_slave_send_fieldcount(router, slave, 1); blr_slave_send_columndef(router, slave, variable, column_type, vers_len, 2); blr_slave_send_eof(router, slave, 3); len = MYSQL_HEADER_LEN + (1 + vers_len); if ((pkt = gwbuf_alloc(len)) == NULL) { return 0; } ptr = GWBUF_DATA(pkt); encode_value(ptr, vers_len + 1, 24); // Add length of data packet ptr += 3; *ptr++ = 0x04; // Sequence number in response *ptr++ = vers_len; // Length of result string memcpy((char*)ptr, value, vers_len);// Result string /* ptr += vers_len; Not required unless more data is added */ MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); return blr_slave_send_eof(router, slave, 5); } /** * Send the response to the SQL command "SHOW VARIABLES LIKE 'xxx' * * @param router The binlog router instance * @param slave The connected slave server * @param variable The variable name * @param value The variable value * @param column_type The variable value type (string or int) * @return Non-zero if data was sent */ static int blr_slave_send_variable(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, const char* variable, const char* value, int column_type) { GWBUF* pkt; uint8_t* ptr; int len, vers_len, seqno = 2; char* p = MXS_STRDUP_A(variable); int var_len; char* old_ptr = p; if (value == NULL) { return 0; } /* Remove heading and trailing "'" */ if (*p == '\'') { p++; } if (p[strlen(p) - 1] == '\'') { p[strlen(p) - 1] = '\0'; } var_len = strlen(p); /* force lowercase */ for (int i = 0; i < var_len; i++) { p[i] = tolower(p[i]); } blr_slave_send_fieldcount(router, slave, 2); blr_slave_send_columndef_with_info_schema(router, slave, "Variable_name", BLR_TYPE_STRING, 40, seqno++); blr_slave_send_columndef_with_info_schema(router, slave, "Value", column_type, 40, seqno++); blr_slave_send_eof(router, slave, seqno++); vers_len = strlen(value); len = MYSQL_HEADER_LEN + (1 + vers_len) + (1 + var_len); if ((pkt = gwbuf_alloc(len)) == NULL) { return 0; } ptr = GWBUF_DATA(pkt); encode_value(ptr, vers_len + 2 + var_len, 24); // Add length of data packet ptr += 3; *ptr++ = seqno++; // Sequence number in response *ptr++ = var_len; // Length of result string memcpy((char*)ptr, p, var_len); // Result string with var name ptr += var_len; *ptr++ = vers_len; // Length of result string memcpy((char*)ptr, value, vers_len);// Result string with var value /* ptr += vers_len; Not required unless more data is added */ MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); MXS_FREE(old_ptr); return blr_slave_send_eof(router, slave, seqno++); } /** * Send the column definition packet for a variable * in a response packet sequence. * * It adds information_schema and variables and variable_name * * @param router The router * @param slave The slave connection * @param name Name of the column * @param type Column type * @param len Column length * @param seqno Packet sequence number * @return Non-zero on success */ static int blr_slave_send_columndef_with_info_schema(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, const char* name, int type, int len, uint8_t seqno) { GWBUF* pkt; uint8_t* ptr; int info_len = strlen("information_schema"); int virtual_table_name_len = strlen("VARIABLES"); int table_name_len = strlen("VARIABLES"); int column_name_len = strlen(name); int orig_column_name_len = strlen("VARIABLE_NAME"); int packet_data_len = 22 + strlen(name) + info_len + virtual_table_name_len + table_name_len + orig_column_name_len; if ((pkt = gwbuf_alloc(MYSQL_HEADER_LEN + packet_data_len)) == NULL) { return 0; } ptr = GWBUF_DATA(pkt); encode_value(ptr, packet_data_len, 24); // Add length of data packet ptr += 3; *ptr++ = seqno; // Sequence number in response *ptr++ = 3; // Catalog is always def *ptr++ = 'd'; *ptr++ = 'e'; *ptr++ = 'f'; *ptr++ = info_len; // Schema name length strcpy((char*)ptr, "information_schema"); ptr += info_len; *ptr++ = virtual_table_name_len; // virtual table name length strcpy((char*)ptr, "VARIABLES"); ptr += virtual_table_name_len; *ptr++ = table_name_len; // Table name length strcpy((char*)ptr, "VARIABLES"); ptr += table_name_len; *ptr++ = column_name_len; // Column name length; while (*name) { *ptr++ = *name++; // Copy the column name } *ptr++ = orig_column_name_len; // Orginal column name strcpy((char*)ptr, "VARIABLE_NAME"); ptr += orig_column_name_len; *ptr++ = 0x0c; // Length of next fields always 12 *ptr++ = 0x3f; // Character set *ptr++ = 0; encode_value(ptr, len, 32); // Add length of column ptr += 4; *ptr++ = type; *ptr++ = 0x81; // Two bytes of flags if (type == 0xfd) { *ptr++ = 0x1f; } else { *ptr++ = 0x00; } *ptr++ = 0; *ptr++ = 0; *ptr++ = 0; return MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); } static char* bypass_change_master(char* input) { // Very simple, basically ASSUMES there is a "CHANGE MASTER" at the beginning. static const char CHANGE[] = "change"; char* p = strcasestr(input, CHANGE); if (p) { input = p + sizeof(CHANGE) - 1; static const char MASTER[] = "master"; p = strcasestr(input, MASTER); if (p) { input = p + sizeof(MASTER) - 1; } } return input; } /** * Interface for testing blr_parse_change_master_command() * * @param input The command to be parsed * @param error_string Pre-allocated string for error message, * BINLOG_ERROR_MSG_LEN + 1 bytes * @param config master option struct to fill * @return 0 on success, 1 on failure */ int blr_test_parse_change_master_command(char* input, char* error_string, ChangeMasterOptions* config) { return blr_parse_change_master_command(bypass_change_master(input), error_string, config); } /* * Interface for testing set new master binlog file * * * @param router Current router instance * @param filename Binlog file name * @param error The error msg for command, * pre-allocated BINLOG_ERROR_MSG_LEN + 1 bytes * @return New binlog file or NULL on error */ char* blr_test_set_master_logfile(ROUTER_INSTANCE* router, const char* filename, char* error) { return blr_set_master_logfile(router, filename, error); } /** * Interface for testing a 'change master' operation * * @param router The router instance * @param command The change master SQL command * @param error The error message, * preallocated BINLOG_ERROR_MSG_LEN + 1 bytes * @return 0 on success, 1 on success with new binlog, -1 on failure */ int blr_test_handle_change_master(ROUTER_INSTANCE* router, char* command, char* error) { return blr_handle_change_master(router, bypass_change_master(command), error); } /** * Handle the response to the SQL command * "SHOW GLOBAL VARIABLES LIKE or SHOW VARIABLES LIKE * * @param router The binlog router instance * @param slave The connected slave server * @param stmt The SQL statement * @return Non-zero if the variable is handled, * 0 if variable is unknown, -1 for syntax error */ static int blr_slave_handle_variables(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, char* stmt) { char* brkb; char* word; /* SPACE,TAB,= */ const char* sep = " ,="; if ((word = strtok_r(stmt, sep, &brkb)) == NULL) { return -1; } else if (strcasecmp(word, "LIKE") == 0) { if ((word = strtok_r(NULL, sep, &brkb)) == NULL) { MXS_ERROR("%s: Missing LIKE clause in SHOW [GLOBAL] VARIABLES.", router->service->name()); return -1; } else if (strcasecmp(word, "'SERVER_ID'") == 0) { if (router->set_master_server_id) { char server_id[40]; sprintf(server_id, "%d", router->masterid); return blr_slave_send_variable(router, slave, "'SERVER_ID'", server_id, BLR_TYPE_INT); } else { return blr_slave_replay(router, slave, router->saved_master.server_id); } } else if (strcasecmp(word, "'SERVER_UUID'") == 0) { if (router->set_master_uuid) { return blr_slave_send_variable(router, slave, "'SERVER_UUID'", router->master_uuid, BLR_TYPE_STRING); } else { return blr_slave_replay(router, slave, router->saved_master.uuid); } } else if (strcasecmp(word, "'MAXSCALE%'") == 0) { return blr_slave_send_maxscale_variables(router, slave); } else { return 0; } } else { return -1; } } /** * Send a MySQL OK packet with a warning flag to the slave backend * and set the warning message in slave structure * The message should be retrieved by SHOW WARNINGS command * * @param router The binlog router instance * @param message The message to send * @param slave The slave server to which we are sending the response * * @return The write call result: non-zero if write was successful */ static int blr_slave_send_warning_message(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, const char* message) { GWBUF* pkt; uint8_t* ptr; if ((pkt = gwbuf_alloc(11)) == NULL) { return 0; } ptr = GWBUF_DATA(pkt); *ptr++ = 7; // Payload length *ptr++ = 0; *ptr++ = 0; *ptr++ = 1; // Seqno *ptr++ = 0; // ok *ptr++ = 0; *ptr++ = 0; *ptr++ = 2; *ptr++ = 0; if (strlen(message) == 0) { *ptr++ = 0; *ptr++ = 0; } else { *ptr++ = 1; /* warning byte set to 1 */ *ptr++ = 0; } /* set the new warning in this slave connection */ if (slave->warning_msg) { MXS_FREE(slave->warning_msg); } slave->warning_msg = MXS_STRDUP_A(message); return MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); } /** * Send a MySQL SHOW WARNINGS packet with a message * that has been stored in slave struct. * * If there is no warning message an OK packet is sent * * @param router The binlog router instance * @param message The message to send * @param slave The slave server to which we are sending the response * * @return The write call result: non-zero if write was successful */ static int blr_slave_show_warnings(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave) { GWBUF* pkt; uint8_t* ptr; int len; int msg_len = 0; int code_len = 0; int level_len = 0; /* check whether a warning message is available */ if (slave->warning_msg) { const char* level = "Warning"; char* msg_ptr; char err_code[16 + 1] = ""; msg_ptr = strchr(slave->warning_msg, ':'); if (msg_ptr) { size_t len = (msg_ptr - slave->warning_msg > 16) ? 16 : (msg_ptr - slave->warning_msg); memcpy(err_code, slave->warning_msg, len); err_code[len] = 0; code_len = strlen(err_code); msg_ptr++; } else { msg_ptr = slave->warning_msg; } msg_len = strlen(msg_ptr); level_len = strlen(level); blr_slave_send_fieldcount(router, slave, 3); // 3 columns blr_slave_send_columndef(router, slave, "Level", BLR_TYPE_STRING, 40, 2); blr_slave_send_columndef(router, slave, "Code", BLR_TYPE_STRING, 40, 3); blr_slave_send_columndef(router, slave, "Message", BLR_TYPE_STRING, 80, 4); blr_slave_send_eof(router, slave, 5); len = MYSQL_HEADER_LEN + (1 + level_len) + (1 + code_len) + (1 + msg_len); if ((pkt = gwbuf_alloc(len)) == NULL) { return blr_slave_send_ok(router, slave); } ptr = GWBUF_DATA(pkt); encode_value(ptr, len - MYSQL_HEADER_LEN, 24); // Add length of data packet ptr += 3; *ptr++ = 0x06; // Sequence number in response *ptr++ = level_len; // Length of result string memcpy((char*)ptr, level, level_len); // Result string ptr += level_len; *ptr++ = code_len; // Length of result string if (code_len) { memcpy((char*)ptr, err_code, code_len); // Result string ptr += code_len; } *ptr++ = msg_len; // Length of result string if (msg_len) { memcpy((char*)ptr, msg_ptr, msg_len); // Result string /* ptr += msg_len; Not required unless more data is added */ } MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); return blr_slave_send_eof(router, slave, 7); } else { return blr_slave_send_ok(router, slave); } } /** * Handle the response to the SQL command * "SHOW [GLOBAL] STATUS LIKE or SHOW STATUS LIKE * * @param router The binlog router instance * @param slave The slave server to which we are sending the response * @param stmt The SQL statement * @return Non-zero if the variable is handled, * 0 if variable is unknown, -1 for syntax errors. */ static int blr_slave_handle_status_variables(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, char* stmt) { char* brkb = NULL; char* word = NULL; /* SPACE,TAB,= */ const char* sep = " ,="; if ((word = strtok_r(stmt, sep, &brkb)) == NULL) { return -1; } else if (strcasecmp(word, "LIKE") == 0) { if ((word = strtok_r(NULL, sep, &brkb)) == NULL) { MXS_ERROR("%s: Missing LIKE clause in SHOW [GLOBAL] STATUS.", router->service->name()); return -1; } else if (strcasecmp(word, "'Uptime'") == 0) { char uptime[41] = ""; snprintf(uptime, 40, "%d", maxscale_uptime()); return blr_slave_send_status_variable(router, slave, "Uptime", uptime, BLR_TYPE_INT); } else if (strcasecmp(word, "'slave_received_heartbeats'") == 0) { char hkbeats[41] = ""; snprintf(hkbeats, 40, "%d", router->stats.n_heartbeats); return blr_slave_send_status_variable(router, slave, "Slave_received_heartbeats", hkbeats, BLR_TYPE_INT); } else { return 0; } } else { return -1; } } /** * Send the response to the SQL command "SHOW [GLOBAL] STATUS LIKE 'xxx' * * @param router The binlog router instance * @param slave The connected slave server * @param variable The variable name * @param value The variable value * @param column_type The variable value type (string or int) * @return Non-zero if data was sent */ static int blr_slave_send_status_variable(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, const char* variable, const char* value, int column_type) { GWBUF* pkt; uint8_t* ptr; int len, vers_len, seqno = 2; char* p = MXS_STRDUP_A(variable); int var_len; char* old_ptr = p; /* Remove heading and trailing "'" */ if (*p == '\'') { p++; } if (p[strlen(p) - 1] == '\'') { p[strlen(p) - 1] = '\0'; } var_len = strlen(p); /* force lowercase */ for (int i = 0; i < var_len; i++) { p[i] = tolower(p[i]); } /* First char is uppercase */ p[0] = toupper(p[0]); blr_slave_send_fieldcount(router, slave, 2); blr_slave_send_columndef_with_status_schema(router, slave, "Variable_name", BLR_TYPE_STRING, 40, seqno++); blr_slave_send_columndef_with_status_schema(router, slave, "Value", column_type, 40, seqno++); blr_slave_send_eof(router, slave, seqno++); vers_len = strlen(value); len = MYSQL_HEADER_LEN + (1 + vers_len) + (1 + var_len); if ((pkt = gwbuf_alloc(len)) == NULL) { return 0; } ptr = GWBUF_DATA(pkt); // Add length of data packet encode_value(ptr, vers_len + 2 + var_len, 24); ptr += 3; // Sequence number in response *ptr++ = seqno++; // Length of result string *ptr++ = var_len; // Result string with var name memcpy((char*)ptr, p, var_len); ptr += var_len; // Length of result string *ptr++ = vers_len; // Result string with var value memcpy((char*)ptr, value, vers_len); /* ptr += vers_len; Not required unless more data is added */ MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); MXS_FREE(old_ptr); return blr_slave_send_eof(router, slave, seqno++); } /** * Send the column definition packet for a STATUS variable * in a response packet sequence. * * It adds information_schema.STATUS and variables and variable_name * * @param router The router * @param slave The slave connection * @param name Name of the column * @param type Column type * @param len Column length * @param seqno Packet sequence number * @return Non-zero on success */ static int blr_slave_send_columndef_with_status_schema(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, const char* name, int type, int len, uint8_t seqno) { GWBUF* pkt; uint8_t* ptr; int info_len = strlen("information_schema"); int virtual_table_name_len = strlen("STATUS"); int table_name_len = strlen("STATUS"); int column_name_len = strlen(name); int orig_column_name_len = strlen("VARIABLE_NAME"); int packet_data_len = 0; const char* ptr_name_start = name; if (strcasecmp(ptr_name_start, "value") == 0) { orig_column_name_len = strlen("VARIABLE_VALUE"); } packet_data_len = 22 + strlen(name) + info_len + virtual_table_name_len + table_name_len + orig_column_name_len; if ((pkt = gwbuf_alloc(MYSQL_HEADER_LEN + packet_data_len)) == NULL) { return 0; } ptr = GWBUF_DATA(pkt); encode_value(ptr, packet_data_len, 24); // Add length of data packet ptr += 3; *ptr++ = seqno; // Sequence number in response *ptr++ = 3; // Catalog is always def *ptr++ = 'd'; *ptr++ = 'e'; *ptr++ = 'f'; *ptr++ = info_len; // Schema name length strcpy((char*)ptr, "information_schema"); ptr += info_len; *ptr++ = virtual_table_name_len; // virtual table name length strcpy((char*)ptr, "STATUS"); ptr += virtual_table_name_len; *ptr++ = table_name_len; // Table name length strcpy((char*)ptr, "STATUS"); ptr += table_name_len; *ptr++ = column_name_len; // Column name length; while (*name) { *ptr++ = *name++; // Copy the column name } *ptr++ = orig_column_name_len; // Orginal column name if (strcasecmp(ptr_name_start, "value") == 0) { strcpy((char*)ptr, "VARIABLE_VALUE"); } else { strcpy((char*)ptr, "VARIABLE_NAME"); } ptr += orig_column_name_len; *ptr++ = 0x0c; // Length of next fields always 12 *ptr++ = 0x3f; // Character set *ptr++ = 0; encode_value(ptr, len, 32); // Add length of column ptr += 4; *ptr++ = type; *ptr++ = 0x81; // Two bytes of flags if (type == 0xfd) { *ptr++ = 0x1f; } else { *ptr++ = 0x00; } *ptr++ = 0; *ptr++ = 0; *ptr++ = 0; return MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); } /** * The heartbeat check function called * from the housekeeper for registered slaves. * * @param router Current router instance */ static bool blr_send_slave_heartbeat(void* inst) { ROUTER_SLAVE* sptr = NULL; ROUTER_INSTANCE* router = (ROUTER_INSTANCE*) inst; time_t t_now = time(0); pthread_mutex_lock(&router->lock); sptr = router->slaves; while (sptr) { /* skip servers with state = 0 */ if ((sptr->state == BLRS_DUMPING) && (sptr->heartbeat > 0) && ((t_now + 1 - sptr->lastReply) >= sptr->heartbeat)) { MXS_NOTICE("Sending Heartbeat to slave server-id %d. " "Heartbeat interval is %d, last event time is %lu", sptr->serverid, sptr->heartbeat, (unsigned long)sptr->lastReply); blr_slave_send_heartbeat(router, sptr); /* Set last event */ sptr->lastEventReceived = HEARTBEAT_EVENT; /* Set last time */ sptr->lastReply = t_now; } sptr = sptr->next; } pthread_mutex_unlock(&router->lock); return true; } /** * Create and send an hearbeat packet to be sent to a registered slave server * * @param router The current route rinstance * @param slave The current slave connection * @return Number of bytes sent or 0 in case of failure */ static void send_heartbeat(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave) { REP_HEADER hdr; GWBUF* h_event; uint8_t* ptr; int len = BINLOG_EVENT_HDR_LEN; uint32_t chksum; int filename_len = strlen(slave->binlog_name); /* Add CRC32 4 bytes */ if (!slave->nocrc) { len += BINLOG_EVENT_CRC_SIZE; } /* add binlogname to data content len */ len += filename_len; /** * Alloc buffer for network binlog stream: * * 4 bytes header (3 for pkt len + 1 seq.no) * 1 byte for Ok / ERR * n bytes data content * * Total = 5 bytes + len */ h_event = gwbuf_alloc(MYSQL_HEADER_LEN + 1 + len); /* The OK/Err byte is part of payload */ hdr.payload_len = len + 1; /* Add sequence no */ hdr.seqno = slave->seqno++; /* Add OK */ hdr.ok = 0; /* Add timestamp: 0 */ hdr.timestamp = 0L; /* Set Event Type */ hdr.event_type = HEARTBEAT_EVENT; /* Add master server id */ hdr.serverid = router->masterid; /* Add event size */ hdr.event_size = len; /* Add Next Pos */ hdr.next_pos = slave->binlog_pos; /* Add Artificial flags */ hdr.flags = 0x20; /* point just after the header */ ptr = blr_build_header(h_event, &hdr); /* Copy binlog name */ memcpy(ptr, slave->binlog_name, filename_len); ptr += filename_len; /* Add the CRC32 */ if (!slave->nocrc) { chksum = crc32(0L, NULL, 0); chksum = crc32(chksum, GWBUF_DATA(h_event) + MYSQL_HEADER_LEN + 1, hdr.event_size - BINLOG_EVENT_CRC_SIZE); encode_value(ptr, chksum, 32); } /* Write the packet */ mxs::RoutingWorker* worker = static_cast(slave->dcb->owner); worker->execute([slave, h_event]() { MXS_SESSION_ROUTE_REPLY(slave->dcb->session, h_event); }, mxs::RoutingWorker::EXECUTE_AUTO); } static void blr_slave_send_heartbeat(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave) { mxs::RoutingWorker* worker = static_cast(slave->dcb->owner); worker->execute([router, slave]() { send_heartbeat(router, slave); }, mxs::RoutingWorker::EXECUTE_AUTO); } /** * Change the replication SSL options * * @param router Current router instance * @param config The current config * @param error_message Pre-allocated string for error message is * BINLOG_ERROR_MSG_LEN + 1 bytes * @return 1 for applied change, * 0 no changes and -1 for errors */ static int blr_set_master_ssl(ROUTER_INSTANCE* router, const ChangeMasterConfig& config, char* error_message) { bool updated = 0; if (config.ssl_enabled) { router->ssl_enabled = config.ssl_enabled; } if (router->ssl_enabled && !config.ssl_ca.empty() && !config.ssl_key.empty() && !config.ssl_cert.empty()) { MXS_CONFIG_PARAMETER params; params.set_from_list({ {CN_SSL, CN_REQUIRED}, {CN_SSL_KEY, config.ssl_key}, {CN_SSL_CERT, config.ssl_cert}, {CN_SSL_CA_CERT, config.ssl_ca}, {CN_SSL_CERT_VERIFY_DEPTH, "9"}, {CN_SSL_VERIFY_PEER_CERTIFICATE, "true"} }); if (!config.ssl_version.empty()) { mxb_assert((config.ssl_version.front() != '\'') && (config.ssl_version.front() != '"')); params.set(CN_SSL_VERSION, config.ssl_version); MXS_FREE(router->ssl_version); router->ssl_version = MXS_STRDUP_A(config.ssl_version.c_str()); } /* Update options in router fields */ mxb_assert((config.ssl_key.front() != '\'') && (config.ssl_key.front() != '"')); MXS_FREE(router->ssl_key); router->ssl_key = MXS_STRDUP_A(config.ssl_key.c_str()); mxb_assert((config.ssl_ca.front() != '\'') && (config.ssl_ca.front() != '"')); MXS_FREE(router->ssl_ca); router->ssl_ca = MXS_STRDUP_A(config.ssl_ca.c_str()); mxb_assert((config.ssl_cert.front() != '\'') && (config.ssl_cert.front() != '"')); MXS_FREE(router->ssl_cert); router->ssl_cert = MXS_STRDUP_A(config.ssl_cert.c_str()); std::unique_ptr ssl(mxs::SSLContext::create(params)); if (ssl) { updated = 1; router->service->dbref->server->ssl().set_context(std::move(ssl)); } else { updated = -1; } } return updated; } /** * Notify a waiting slave that new events are stored in binlog file * * @param slave The current connected slave * @return True if slave has been notified * */ bool blr_notify_waiting_slave(ROUTER_SLAVE* slave) { bool ret = false; pthread_mutex_lock(&slave->catch_lock); if (slave->cstate & CS_WAIT_DATA) { ret = true; /* Add fake event that will call the blr_slave_callback routine */ poll_fake_write_event(slave->dcb); slave->cstate &= ~CS_WAIT_DATA; } pthread_mutex_unlock(&slave->catch_lock); return ret; } /** * Read MXS_START_ENCRYPTION_EVENT, after FDE * * @param router The router instance * @param slave The connected slave server * @param fde_end_pos The position of MXS_START_ENCRYPTION_EVENT, after FDE * @return Non zero on success */ static int blr_slave_read_ste(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, uint32_t fde_end_pos) { REP_HEADER hdr; GWBUF* record, * head; uint8_t* ptr; uint32_t chksum; char err_msg[BINLOG_ERROR_MSG_LEN + 1]; MARIADB_GTID_INFO* f_tree = router->storage_type == BLR_BINLOG_STORAGE_TREE ? &slave->f_info : NULL; err_msg[BINLOG_ERROR_MSG_LEN] = '\0'; memset(&hdr, 0, BINLOG_EVENT_HDR_LEN); BLFILE* file; if ((file = blr_open_binlog(router, slave->binlog_name, f_tree)) == NULL) { return 0; } /* Start Encryption Event is not encrypted, we pass NULL to last param */ if ((record = blr_read_binlog(router, file, fde_end_pos, &hdr, err_msg, NULL)) == NULL) { if (hdr.ok != SLAVE_POS_READ_OK) { MXS_ERROR("Slave %s:%i, server-id %d, binlog '%s', " "blr_read_binlog failure: %s", slave->dcb->remote, dcb_get_port(slave->dcb), slave->serverid, slave->binlog_name, err_msg); } blr_close_binlog(router, file); return 0; } blr_close_binlog(router, file); /* check for MXS_START_ENCRYPTION_EVENT */ if (hdr.event_type == MARIADB10_START_ENCRYPTION_EVENT) { uint8_t* record_ptr = GWBUF_DATA(record); void* mem = MXS_CALLOC(1, sizeof(SLAVE_ENCRYPTION_CTX)); SLAVE_ENCRYPTION_CTX* new_encryption_ctx = static_cast(mem); if (!new_encryption_ctx) { gwbuf_free(record); return 0; } record_ptr += BINLOG_EVENT_HDR_LEN; // Set schema, 1 Byte new_encryption_ctx->binlog_crypto_scheme = record_ptr[0]; // Set key version memcpy(&new_encryption_ctx->binlog_key_version, record_ptr + 1, BLRM_KEY_VERSION_LENGTH); // Set nonce memcpy(new_encryption_ctx->nonce, record_ptr + 1 + BLRM_KEY_VERSION_LENGTH, BLRM_NONCE_LENGTH); /* Set the pos of first encrypted event */ new_encryption_ctx->first_enc_event_pos = fde_end_pos + hdr.event_size; pthread_mutex_lock(&slave->catch_lock); SLAVE_ENCRYPTION_CTX* old_encryption_ctx = slave->encryption_ctx; /* Set the new encryption ctx into slave */ slave->encryption_ctx = new_encryption_ctx; pthread_mutex_unlock(&slave->catch_lock); /* Free previous encryption ctx */ MXS_FREE(old_encryption_ctx); MXS_INFO("Start Encryption event found. Binlog %s is encrypted. " "First event at %lu", slave->binlog_name, (unsigned long)fde_end_pos + hdr.event_size); /** * Note: if the requested pos is equal to MXS_START_ENCRYPTION_EVENT pos * the event will be skipped by blr_read_binlog() routine */ gwbuf_free(record); return 1; } gwbuf_free(record); return 0; } /** * Handle received SELECT statements from clients * * if a SELECT statement is one of suported one * a proper reply to the connected client is done * * @param router Router instance * @param slave Connected client/slave server * @param select_stmt The SELECT statement * @return True for handled queries, False otherwise */ static bool blr_handle_simple_select_stmt(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, char* select_stmt) { char* word; char* brkb; const char* sep = " \t,="; if ((word = strtok_r(select_stmt, sep, &brkb)) == NULL) { MXS_ERROR("%s: Incomplete select query.", router->service->name()); return false; } else if (strcasecmp(word, "UNIX_TIMESTAMP()") == 0) { blr_slave_send_timestamp(router, slave); return true; } else if (strcasecmp(word, "@master_binlog_checksum") == 0 || strcasecmp(word, "@@global.binlog_checksum") == 0) { blr_slave_replay(router, slave, router->saved_master.chksum2); return true; } else if (strcasecmp(word, "@@GLOBAL.GTID_MODE") == 0) { if (router->saved_master.gtid_mode) { blr_slave_replay(router, slave, router->saved_master.gtid_mode); } else { blr_slave_send_var_value(router, slave, "@@GLOBAL.GTID_MODE", "OFF", BLR_TYPE_STRING); } return true; } else if (strcasecmp(word, "1") == 0) { blr_slave_replay(router, slave, router->saved_master.select1); return true; } else if (strcasecmp(word, "VERSION()") == 0) { if (router->set_master_version) { blr_slave_send_var_value(router, slave, "VERSION()", router->set_master_version, BLR_TYPE_STRING); return true; } else { blr_slave_replay(router, slave, router->saved_master.selectver); return true; } } else if (strcasecmp(word, "USER()") == 0) { /* Return user@host */ char user_host[MYSQL_USER_MAXLEN + 1 + MYSQL_HOST_MAXLEN + 1] = ""; snprintf(user_host, sizeof(user_host), "%s@%s", slave->dcb->user, slave->dcb->remote); blr_slave_send_var_value(router, slave, "USER()", user_host, BLR_TYPE_STRING); return true; } else if (strcasecmp(word, "@@version") == 0) { if (router->set_master_version) { blr_slave_send_var_value(router, slave, "@@version", router->set_master_version, BLR_TYPE_STRING); return true; } else { char* version = blr_extract_column(router->saved_master.selectver, 1); blr_slave_send_var_value(router, slave, "@@version", version == NULL ? "" : version, BLR_TYPE_STRING); return true; } } else if (strcasecmp(word, "@@version_comment") == 0) { if (!router->saved_master.selectvercom) /** * This allows mysql client to get in when * @@version_comment is not available */ { blr_slave_send_ok(router, slave); return true; } else { blr_slave_replay(router, slave, router->saved_master.selectvercom); return true; } } else if (strcasecmp(word, "@@hostname") == 0) { if (router->set_master_hostname) { blr_slave_send_var_value(router, slave, "@@hostname", router->set_master_hostname, BLR_TYPE_STRING); return true; } else { blr_slave_replay(router, slave, router->saved_master.selecthostname); return true; } } else if ((strcasecmp(word, "@@server_uuid") == 0) || (strcasecmp(word, "@@global.server_uuid") == 0)) { /* to ensure we match the case in query and response */ char heading[40]; strcpy(heading, word); if (router->set_master_uuid) { blr_slave_send_var_value(router, slave, heading, router->master_uuid, BLR_TYPE_STRING); return true; } else { char* master_uuid = blr_extract_column(router->saved_master.uuid, 2); blr_slave_send_var_value(router, slave, heading, master_uuid == NULL ? "" : master_uuid, BLR_TYPE_STRING); MXS_FREE(master_uuid); return true; } } else if (strcasecmp(word, "@@max_allowed_packet") == 0) { blr_slave_replay(router, slave, router->saved_master.map); return true; } else if (strcasecmp(word, "@@maxscale_version") == 0) { blr_slave_send_maxscale_version(router, slave); return true; } else if ((strcasecmp(word, "@@server_id") == 0) || (strcasecmp(word, "@@global.server_id") == 0)) { char server_id[40]; /* to ensure we match the case in query and response */ char heading[40]; sprintf(server_id, "%d", router->masterid); strcpy(heading, word); blr_slave_send_var_value(router, slave, heading, server_id, BLR_TYPE_INT); return true; } /* Handle MariaDB 10 GTID vars */ else if ((strcasecmp(word, "@@gtid_current_pos") == 0) || (strcasecmp(word, "@@global.gtid_current_pos") == 0) || (strcasecmp(word, "@@gtid_binlog_pos") == 0) || (strcasecmp(word, "@@global.gtid_binlog_pos") == 0) || (strcasecmp(word, "@@gtid_slave_pos") == 0) || (strcasecmp(word, "@@global.gtid_slave_pos") == 0)) { char heading[40]; char mariadb_gtid[GTID_MAX_LEN + 1]; mariadb_gtid[0] = 0; strcpy(heading, word); if (router->mariadb10_compat && router->mariadb10_gtid) { pthread_mutex_lock(&router->binlog_lock); strcpy(mariadb_gtid, router->last_mariadb_gtid); pthread_mutex_unlock(&router->binlog_lock); } /* Return empty gtid_slave_pos if master GTID registration is off */ if (!router->mariadb10_master_gtid && strcasestr(word, "gtid_slave_pos") != NULL) { mariadb_gtid[0] = 0; } blr_slave_send_var_value(router, slave, heading, mariadb_gtid, BLR_TYPE_INT); return true; } else if (strcasecmp(word, "@@GLOBAL.gtid_domain_id") == 0) { /* If not mariadb10 mastergtid an error message will be returned */ if (slave->mariadb10_compat && router->mariadb10_gtid) { char heading[40]; char gtid_domain[40]; sprintf(gtid_domain, "%" PRIu32 "", router->mariadb10_gtid_domain); strcpy(heading, word); blr_slave_send_var_value(router, slave, heading, gtid_domain, BLR_TYPE_INT); return true; } } else if ((strcasecmp(word, "@@global.max_connections") == 0) || (strcasecmp(word, "@@max_connections") == 0)) { char max_conns[40]; /* to ensure we match the case in query and response */ char heading[40]; sprintf(max_conns, "%d", !router->service->max_connections ? BLR_DEFAULT_MAX_CONNS : router->service->max_connections); strcpy(heading, word); blr_slave_send_var_value(router, slave, heading, max_conns, BLR_TYPE_INT); } else if ((strcasecmp(word, "@@global.read_only") == 0) || (strcasecmp(word, "@@read_only") == 0)) { /* to ensure we match the case in query and response */ char heading[40]; strcpy(heading, word); blr_slave_send_var_value(router, slave, heading, "0", BLR_TYPE_INT); } else if ((strcasecmp(word, "@@global.log_bin") == 0) || (strcasecmp(word, "@@log_bin") == 0)) { /* to ensure we match the case in query and response */ char heading[40]; strcpy(heading, word); blr_slave_send_var_value(router, slave, heading, "1", BLR_TYPE_INT); } return false; } /** * Build and send a Fake Rotate event to the new client * * @param router The router instance * @param slave The new connected client * @return Non-zero if the rotate was sent */ static int blr_send_connect_fake_rotate(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave) { /* Build Fake Rotate Event */ GWBUF* r_event = blr_build_fake_rotate_event(slave, slave->binlog_pos, slave->binlog_name, router->masterid); /* Send Fake Rotate Event or return 0*/ return r_event ? MXS_SESSION_ROUTE_REPLY(slave->dcb->session, r_event) : 0; } /** * Build a fake rotate event * * @param slave The current connected client * @param pos The position to set in the event * @param filename The filename to set in the event * @param serverid The serverid to set in the event * @return A GWBUF with the binlog event or NULL */ static GWBUF* blr_build_fake_rotate_event(ROUTER_SLAVE* slave, unsigned long pos, const char* filename, unsigned long serverid) { GWBUF* r_event; uint8_t* ptr; int len; int flen; REP_HEADER hdr; uint32_t chksum; flen = strlen(filename); /* Event size: header + 8 bytes pos + filename */ len = BINLOG_EVENT_HDR_LEN + 8 + flen; /* Add CRC32 bytes if needed */ len += slave->nocrc ? 0 : BINLOG_EVENT_CRC_SIZE; /* Allocate space for packet header, status and data */ if ((r_event = gwbuf_alloc(MYSQL_HEADER_LEN + 1 + len)) == NULL) { return NULL; } /* Add 1 byte to paylod for status indicator */ hdr.payload_len = len + 1; /* Add sequence and increment it */ hdr.seqno = slave->seqno++; /* Set status indicator byte to OK */ hdr.ok = 0; /* No timestamp is required */ hdr.timestamp = 0L; /* Rotate Event Type */ hdr.event_type = ROTATE_EVENT; hdr.serverid = serverid; hdr.event_size = len; /* Next pos is not needed */ hdr.next_pos = 0; /* Artificial Event Flag */ hdr.flags = 0x20; /* Add replication hdr to resp */ ptr = blr_build_header(r_event, &hdr); /* Add 8 bytes pos */ encode_value(ptr, pos, 64); ptr += 8; /* Add binlog filename, no trailing 0 */ memcpy(ptr, filename, flen); ptr += flen; /* Now add the CRC to the fake binlog rotate event */ if (!slave->nocrc) { /* * First checksum of an empty buffer * then the checksum of the event portion of the message: * we do not include the len, seq number and ok byte that are part of * first 5 bytes of the message. * We also do not include the 4 byte checksum itself. */ chksum = crc32(0L, NULL, 0); chksum = crc32(chksum, GWBUF_DATA(r_event) + MYSQL_HEADER_LEN + 1, hdr.event_size - BINLOG_EVENT_CRC_SIZE); encode_value(ptr, chksum, 32); } return r_event; } /** * Look for a MariaDB GTID in the gtid maps database * * The caller specifies the position from COM_BINLOG_DUMP * packet and if a filename is present or not in the request. * * Default position is 4, default file is router->binlog_file. * * If req_file is false then the file to read data from * could be either router->binlog_file or the file the GTID * belongs to. * * Note: rmpty GTID means send data from router->binlog_file pos 4. * * @param router The router instance * @param slave The current slave server connected * @param req_file Using binlog filename or not * @param req_pos The requested file pos * @return False if GTID is not found and slave * is connectig with gtid_strict_mode=1, * other errors. * True otherwise. */ static bool blr_slave_gtid_request(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, bool req_file, unsigned long req_pos) { MARIADB_GTID_INFO f_gtid = {}; uint32_t router_pos; char router_curr_file[BINLOG_FNAMELEN + 1]; char last_gtid[GTID_MAX_LEN + 1]; memset(&f_gtid, 0, sizeof(f_gtid)); pthread_mutex_lock(&router->binlog_lock); // Set gtid as current router gtid strcpy(last_gtid, router->last_mariadb_gtid); // Set file as router current file strcpy(router_curr_file, router->binlog_name); // Set safe postion of current ruter file router_pos = router->binlog_position; // Set domain_id, server_id in case of empty/not found GTID if (router->storage_type == BLR_BINLOG_STORAGE_TREE) { f_gtid.gtid_elms.domain_id = router->mariadb10_gtid_domain; f_gtid.gtid_elms.server_id = router->orig_masterid; } pthread_mutex_unlock(&router->binlog_lock); MXS_INFO("Slave %lu is registering with MariaDB GTID '%s'", (unsigned long)slave->serverid, slave->mariadb_gtid); if (!slave->mariadb_gtid[0]) { /** * Empty GTID: * Sending data from the router current file and pos 4 */ char t_prefix[BINLOG_FILE_EXTRA_INFO] = ""; // Add file prefix if (router->storage_type == BLR_BINLOG_STORAGE_TREE) { sprintf(t_prefix, "%" PRIu32 "/%" PRIu32 "/", f_gtid.gtid_elms.domain_id, f_gtid.gtid_elms.server_id); } strcpy(slave->binlog_name, router_curr_file); slave->binlog_pos = 4; MXS_INFO("Slave %d is registering with empty GTID:" " sending events from current binlog file %s%s," " pos %" PRIu32 "", slave->serverid, t_prefix, slave->binlog_name, slave->binlog_pos); /* Add GTID details to slave struct */ memcpy(&slave->f_info, &f_gtid, sizeof(MARIADB_GTID_INFO)); return true; } else { char dbpath[PATH_MAX + 1]; snprintf(dbpath, sizeof(dbpath), "/%s/%s", router->binlogdir, GTID_MAPS_DB); /* Open GTID maps read-only database */ if (sqlite3_open_v2(dbpath, &slave->gtid_maps, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) { char errmsg[BINLOG_ERROR_MSG_LEN + sizeof(dbpath) + 1]; snprintf(errmsg, sizeof(errmsg), "Slave %lu: failed to open GTID maps db '%s': %s", (unsigned long)slave->serverid, dbpath, sqlite3_errmsg(slave->gtid_maps)); errmsg[BINLOG_ERROR_MSG_LEN] = '\0'; MXS_ERROR("%s", errmsg); strcpy(slave->binlog_name, ""); slave->binlog_pos = 0; blr_send_custom_error(slave->dcb, slave->seqno + 1, 0, "Cannot open GTID maps storage.", "HY000", BINLOG_FATAL_ERROR_READING); slave->gtid_maps = NULL; return false; } else { /* Fetch the GTID from the maps storage */ blr_fetch_mariadb_gtid(slave, slave->mariadb_gtid, &f_gtid); /* Close GTID maps database */ sqlite3_close_v2(slave->gtid_maps); slave->gtid_maps = NULL; } /* Requested GTID Not Found */ if (!f_gtid.gtid[0]) { char errmsg[BINLOG_ERROR_MSG_LEN + 1]; snprintf(errmsg, BINLOG_ERROR_MSG_LEN, "Requested MariaDB GTID '%s' by server %lu" " not found. GTID_STRICT_MODE=%s", slave->mariadb_gtid, (unsigned long)slave->serverid, slave->gtid_strict_mode ? "ON" : "OFF"); errmsg[BINLOG_ERROR_MSG_LEN] = '\0'; /* Check strict mode */ if (slave->gtid_strict_mode) { MXS_ERROR("%s", errmsg); strcpy(slave->binlog_name, ""); slave->binlog_pos = 0; blr_send_custom_error(slave->dcb, slave->seqno + 1, 0, "connecting slave requested to start" " from non existent GTID.", "HY000", BINLOG_FATAL_ERROR_READING); return false; } else { /* No strict mode: */ MXS_WARNING("%s", errmsg); // - 1 -Set request GTID as current master one MXS_FREE(slave->mariadb_gtid); slave->mariadb_gtid = MXS_STRDUP_A(last_gtid); // - 2 - Use current router file and position strcpy(slave->binlog_name, router_curr_file); slave->binlog_pos = router_pos; // - 3 Set GTID details for filename if (router->storage_type == BLR_BINLOG_STORAGE_TREE) { memcpy(&slave->f_info, &f_gtid, sizeof(MARIADB_GTID_INFO)); } } } else { /* GTID has been found */ MXS_INFO("Found GTID '%s' for slave %" PRIu32 "" " at %" PRIu32 "/%" PRIu32 "/%s:%" PRIu64 "" ". Next event at %" PRIu64 "", slave->mariadb_gtid, slave->serverid, f_gtid.gtid_elms.domain_id, f_gtid.gtid_elms.server_id, f_gtid.binlog_name, f_gtid.start, f_gtid.end); /** * Checks: * a) GTID request has no binlog file at all: * use GTID info file * b) binlog file & position: * if the requested binlog file is equal to GTID info file use it. */ if (!req_file || (strcmp(slave->binlog_name, f_gtid.binlog_name) == 0)) { /* Set binlog file to the GTID one */ strcpy(slave->binlog_name, f_gtid.binlog_name); /* Set pos to GTID next event pos */ slave->binlog_pos = f_gtid.end; } else { /** * The requested binlog file is not the GTID info file. * The binlog file could be different due to: * a rotate event or other non GTID events written * after that GTID. * If file exists and pos >=4, events will be sent * from requested file@pos, otherwise from GTID file & pos. */ // Add tree prefix char t_prefix[BINLOG_FILE_EXTRA_INFO] = ""; char file_path[PATH_MAX + 1] = ""; if (router->storage_type == BLR_BINLOG_STORAGE_TREE) { sprintf(t_prefix, "%" PRIu32 "/%" PRIu32 "/", f_gtid.gtid_elms.domain_id, f_gtid.gtid_elms.server_id); } // Get binlog filename full-path blr_get_file_fullpath(slave->binlog_name, router->binlogdir, file_path, t_prefix[0] ? t_prefix : NULL); // File size is >=4 read: set pos. if (blr_slave_get_file_size(file_path) >= 4) { slave->binlog_pos = req_pos; } else { /* Set binlog file to the GTID one */ strcpy(slave->binlog_name, f_gtid.binlog_name); /* Set pos to GTID next event pos */ slave->binlog_pos = f_gtid.end; } } /* Set GTID details in f_info */ memcpy(&slave->f_info, &f_gtid, sizeof(MARIADB_GTID_INFO)); } } return true; } /** * Create a Fake GTID_LIST event * * The routine creates a Fake GTID_LIST event * * @param slave The connected client * @param gtid The requested GTID from client * @param serverid The router server_id to add * in the replication event header * @return Fake GTID_LIST event on success or NULL * */ static GWBUF* blr_build_fake_gtid_list_event(ROUTER_SLAVE* slave, const char* gtid, uint32_t serverid) { int len; GWBUF* gl_event; uint8_t* ptr; REP_HEADER hdr; uint32_t chksum; MARIADB_GTID_ELEMS req_gtid = {}; if (!blr_parse_gtid(gtid, &req_gtid)) { return NULL; } /** * We only support one GTID in the GTID_LIST event * * Paylod is: * BINLOG_EVENT_HDR_LEN + 4 bytes GTID count + 1 GTID */ len = BINLOG_EVENT_HDR_LEN + 4 + 1 * (4 + 4 + 8); /* Add CRC32 bytes if needed */ len += slave->nocrc ? 0 : BINLOG_EVENT_CRC_SIZE; /* Allocate space for packet header, status and data */ if ((gl_event = gwbuf_alloc(MYSQL_HEADER_LEN + 1 + len)) == NULL) { return NULL; } /* Add 1 byte to paylod for status indicator */ hdr.payload_len = len + 1; /* Add sequence and increment it */ hdr.seqno = slave->seqno++; /* Set status indicator byte to OK */ hdr.ok = 0; /* No timestamp is required */ hdr.timestamp = 0L; /* GTID Event Type */ hdr.event_type = MARIADB10_GTID_GTID_LIST_EVENT; hdr.serverid = serverid; hdr.event_size = len; /* Next pos is set */ hdr.next_pos = slave->binlog_pos; /* Artificial Event Flag */ hdr.flags = 0x20; /* Add replication hdr to resp */ ptr = blr_build_header(gl_event, &hdr); /* * Add 4 bytes count * Note: We set only 1 GTID in GTID_LIST Event */ encode_value(ptr, 1, 32); ptr += 4; /* Add 4 bytes domain id */ encode_value(ptr, req_gtid.domain_id, 32); ptr += 4; /* Add 4 bytes server id*/ encode_value(ptr, req_gtid.server_id, 32); ptr += 4; /* Add 8 bytes sequence */ encode_value(ptr, req_gtid.seq_no, 64); ptr += 8; /* Now add the CRC to the fake binlog rotate event */ if (!slave->nocrc) { /* * First checksum of an empty buffer * then the checksum of the event portion of the message: * we do not include the len, seq number and ok byte that are part of * first 5 bytes of the message. * We also do not include the 4 byte checksum itself. */ chksum = crc32(0L, NULL, 0); chksum = crc32(chksum, GWBUF_DATA(gl_event) + MYSQL_HEADER_LEN + 1, hdr.event_size - BINLOG_EVENT_CRC_SIZE); encode_value(ptr, chksum, 32); } return gl_event; } /** * Create and send a Fake GTID_LIST event * * @param slave Current slave server * @param gtid The requested GTID from client * @oaram serverid The server_id to use in replication header * @return Non-zero if data has been sent */ static int blr_send_fake_gtid_list(ROUTER_SLAVE* slave, const char* gtid, uint32_t serverid) { /* Build Fake GTID_LIST Event */ GWBUF* gl_event = blr_build_fake_gtid_list_event(slave, gtid, serverid); /* Send Fake GTID_LIST Event or return 0*/ return gl_event ? MXS_SESSION_ROUTE_REPLY(slave->dcb->session, gl_event) : 0; } /** * Handle received Maxwell statements from clients * * if a Maxwell statement is suported * a proper reply to the connected client is done * * @param router Router instance * @param slave Connected client/slave server * @param maxwell_stmt The admin command options * @return True for handled queries, False otherwise */ static bool blr_handle_maxwell_stmt(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, const char* maxwell_stmt) { static const char mysql_connector_results_charset_query[] = "SET character_set_results = NULL"; static const char maxwell_server_id_query[] = "SELECT @@server_id as server_id"; static const char maxwell_log_bin_query[] = "SHOW VARIABLES LIKE 'log_bin'"; static const char maxwell_binlog_format_query[] = "SHOW VARIABLES LIKE 'binlog_format'"; static const char maxwell_binlog_row_image_query[] = "SHOW VARIABLES LIKE 'binlog_row_image'"; static const char maxwell_lower_case_tables_query[] = "select @@lower_case_table_names"; if (strcmp(blr_skip_leading_sql_comments(maxwell_stmt), MYSQL_CONNECTOR_SERVER_VARS_QUERY) == 0) { int rc = blr_slave_replay(router, slave, router->saved_master.server_vars); if (!rc) { MXS_ERROR("Error sending mysql-connector-j server variables"); } return true; } else if (router->maxwell_compat && strcmp(maxwell_stmt, mysql_connector_results_charset_query) == 0) { blr_slave_send_ok(router, slave); return true; } else if (router->maxwell_compat && strcmp(maxwell_stmt, MYSQL_CONNECTOR_SQL_MODE_QUERY) == 0) { blr_slave_send_ok(router, slave); return true; } else if (strcmp(maxwell_stmt, maxwell_server_id_query) == 0) { char server_id[40]; sprintf(server_id, "%d", router->masterid); blr_slave_send_var_value(router, slave, "server_id", server_id, BLR_TYPE_STRING); return true; } else if (strcmp(maxwell_stmt, maxwell_log_bin_query) == 0) { char* log_bin = blr_extract_column(router->saved_master.binlog_vars, 1); blr_slave_send_var_value(router, slave, "Value", log_bin == NULL ? "" : log_bin, BLR_TYPE_STRING); MXS_FREE(log_bin); return true; } else if (strcmp(maxwell_stmt, maxwell_binlog_format_query) == 0) { char* binlog_format = blr_extract_column(router->saved_master.binlog_vars, 2); blr_slave_send_var_value(router, slave, "Value", binlog_format == NULL ? "" : binlog_format, BLR_TYPE_STRING); MXS_FREE(binlog_format); return true; } else if (strcmp(maxwell_stmt, maxwell_binlog_row_image_query) == 0) { char* binlog_row_image = blr_extract_column(router->saved_master.binlog_vars, 3); blr_slave_send_var_value(router, slave, "Value", binlog_row_image == NULL ? "" : binlog_row_image, BLR_TYPE_STRING); MXS_FREE(binlog_row_image); return true; } else if (strcmp(maxwell_stmt, maxwell_lower_case_tables_query) == 0) { int rc = blr_slave_replay(router, slave, router->saved_master.lower_case_tables); if (!rc) { MXS_ERROR("Error sending lower_case_tables query response"); } return true; } return false; } /** * Handle received SHOW statements from clients * * if a SHOW statement is one of suported one * a proper reply to the connected client is done * * @param router Router instance * @param slave Connected client/slave server * @param show_stmt The SHOW statement * @return True for handled queries, False otherwise */ static bool blr_handle_show_stmt(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, char* show_stmt) { char* word; char* brkb; const char* sep = " \t,="; if ((word = strtok_r(show_stmt, sep, &brkb)) == NULL) { MXS_ERROR("%s: Incomplete show query.", router->service->name()); return false; } else if (strcasecmp(word, "WARNINGS") == 0) { blr_slave_show_warnings(router, slave); return true; } else if (strcasecmp(word, "BINARY") == 0) { if (router->mariadb10_gtid) { blr_show_binary_logs(router, slave, word); } else { const char* errmsg = "SHOW [FULL] BINARY LOGS needs the" " 'mariadb10_slave_gtid' option to be set."; MXS_ERROR("%s: %s", errmsg, router->service->name()); blr_slave_send_error_packet(slave, errmsg, 1198, NULL); } return true; } else if (strcasecmp(word, "GLOBAL") == 0) { if (router->master_state == BLRM_UNCONFIGURED) { blr_slave_send_ok(router, slave); return true; } if ((word = strtok_r(NULL, sep, &brkb)) == NULL) { MXS_ERROR("%s: Expected VARIABLES in SHOW GLOBAL", router->service->name()); return false; } else if (strcasecmp(word, "VARIABLES") == 0) { int rc = blr_slave_handle_variables(router, slave, brkb); /* if no var found, send empty result set */ if (rc == 0) { blr_slave_send_ok(router, slave); } if (rc >= 0) { return true; } else { MXS_ERROR("%s: Expected LIKE clause in SHOW GLOBAL VARIABLES.", router->service->name()); return false; } } else if (strcasecmp(word, "STATUS") == 0) { int rc = blr_slave_handle_status_variables(router, slave, brkb); /* if no var found, send empty result set */ if (rc == 0) { blr_slave_send_ok(router, slave); } if (rc >= 0) { return true; } else { MXS_ERROR("%s: Expected LIKE clause in SHOW GLOBAL STATUS.", router->service->name()); return false; } } } else if (strcasecmp(word, "VARIABLES") == 0) { int rc; if (router->master_state == BLRM_UNCONFIGURED) { blr_slave_send_ok(router, slave); return true; } rc = blr_slave_handle_variables(router, slave, brkb); /* if no var found, send empty result set */ if (rc == 0) { blr_slave_send_ok(router, slave); } if (rc >= 0) { return true; } else { MXS_ERROR("%s: Expected LIKE clause in SHOW VARIABLES.", router->service->name()); return false; } } else if (strcasecmp(word, "MASTER") == 0) { if ((word = strtok_r(NULL, sep, &brkb)) == NULL) { MXS_ERROR("%s: Expected SHOW MASTER STATUS command", router->service->name()); return false; } else if (strcasecmp(word, "STATUS") == 0) { /* if state is BLRM_UNCONFIGURED return empty result */ if (router->master_state > BLRM_UNCONFIGURED) { blr_slave_send_master_status(router, slave); } else { blr_slave_send_ok(router, slave); } return true; } } /* Added support for SHOW ALL SLAVES STATUS */ else if (strcasecmp(word, "SLAVE") == 0 || (strcasecmp(word, "ALL") == 0)) { if ((word = strtok_r(NULL, sep, &brkb)) == NULL) { MXS_ERROR("%s: Expected SHOW SLAVE STATUS command", router->service->name()); return false; } else if (strcasecmp(word, "STATUS") == 0 || (strcasecmp(word, "SLAVES") == 0 && strcasecmp(brkb, "STATUS") == 0)) { /* if state is BLRM_UNCONFIGURED return empty result */ if (router->master_state > BLRM_UNCONFIGURED) { bool s_all = strcasecmp(word, "SLAVES") == 0 ? true : false; blr_slave_send_slave_status(router, slave, s_all); } else { blr_slave_send_ok(router, slave); } return true; } else if (strcasecmp(word, "HOSTS") == 0) { /* if state is BLRM_UNCONFIGURED return empty result */ if (router->master_state > BLRM_UNCONFIGURED) { blr_slave_send_slave_hosts(router, slave); } else { blr_slave_send_ok(router, slave); } return true; } } else if (strcasecmp(word, "STATUS") == 0) { int rc = blr_slave_handle_status_variables(router, slave, brkb); /* if no var found, send empty result set */ if (rc == 0) { blr_slave_send_ok(router, slave); } if (rc >= 0) { return true; } else { MXS_ERROR("%s: Expected LIKE clause in SHOW STATUS.", router->service->name()); return false; } } return false; } /** * Handle received SET statements from clients * * if a SHOW statement is one of suported one * a proper reply to the connected client is done * * @param router Router instance * @param slave Connected client/slave server * @param set_stmt The SET statement * @return True for handled queries, False otherwise */ static bool blr_handle_set_stmt(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, char* set_stmt) { char* word; char* brkb; const char* sep = " \t,="; if ((word = strtok_r(set_stmt, sep, &brkb)) == NULL) { MXS_ERROR("%s: Incomplete set command.", router->service->name()); return false; } else if ((strcasecmp(word, "autocommit") == 0) || (strcasecmp(word, "@@session.autocommit") == 0)) { blr_slave_send_ok(router, slave); return true; } else if (strcasecmp(word, "@master_heartbeat_period") == 0) { int v_len = 0; word = strtok_r(NULL, sep, &brkb); if (word) { char* new_val; v_len = strlen(word); if (v_len > 6) { new_val = mxb_strndup_a(word, v_len - 6); slave->heartbeat = atoi(new_val) / 1000; } else { new_val = mxb_strndup_a(word, v_len); slave->heartbeat = atoi(new_val) / 1000000; } MXS_FREE(new_val); } blr_slave_replay(router, slave, router->saved_master.heartbeat); return true; } else if (strcasecmp(word, "@mariadb_slave_capability") == 0) { /* mariadb10 compatibility is set for the slave */ slave->mariadb10_compat = true; if (router->mariadb10_compat) { blr_slave_replay(router, slave, router->saved_master.mariadb10); } else { blr_slave_send_ok(router, slave); } return true; } else if (strcasecmp(word, "@master_binlog_checksum") == 0) { word = strtok_r(NULL, sep, &brkb); if (word && (strcasecmp(word, "'none'") == 0)) { slave->nocrc = 1; } else if (word && (strcasecmp(word, "@@global.binlog_checksum") == 0)) { slave->nocrc = !router->master_chksum; } else { slave->nocrc = 0; } blr_slave_replay(router, slave, router->saved_master.chksum1); return true; } else if (strcasecmp(word, "@slave_uuid") == 0) { if ((word = strtok_r(NULL, sep, &brkb)) != NULL) { int len = strlen(word); char* word_ptr = word; if (len) { if (word[len - 1] == '\'') { word[len - 1] = '\0'; } if (word[0] == '\'') { word[0] = '\0'; word_ptr++; } } /* Free previous value */ MXS_FREE(slave->uuid); slave->uuid = MXS_STRDUP_A(word_ptr); } if (router->saved_master.setslaveuuid) { blr_slave_replay(router, slave, router->saved_master.setslaveuuid); } else { blr_slave_send_ok(router, slave); } return true; } else if (strcasecmp(word, "@@global.gtid_slave_pos") == 0) { if (slave->serverid != 0) { MXS_ERROR("Master GTID registration can be sent only" " via administration connection"); blr_slave_send_error_packet(slave, "Master GTID registration cannot be" " issued by a registrating slave.", 1198, NULL); return false; } if (router->master_state != BLRM_SLAVE_STOPPED && router->master_state != BLRM_UNCONFIGURED) { const char* err_msg_u = "configured replication: Issue CHANGE MASTER TO first."; const char* err_msg_s = "stopped replication: issue STOP SLAVE first."; char error_string[BINLOG_ERROR_MSG_LEN + 1] = ""; MXS_ERROR("GTID registration without %s", router->master_state == BLRM_SLAVE_STOPPED ? err_msg_s : err_msg_u); snprintf(error_string, BINLOG_ERROR_MSG_LEN, "Cannot use Master GTID registration without %s", router->master_state == BLRM_SLAVE_STOPPED ? err_msg_s : err_msg_u); blr_slave_send_error_packet(slave, error_string, 1198, NULL); return true; } /* If not mariadb GTID an error message will be returned */ if (router->mariadb10_master_gtid) { if ((word = strtok_r(NULL, sep, &brkb)) != NULL) { char heading[GTID_MAX_LEN + 1]; MARIADB_GTID_ELEMS gtid_elms = {}; // TODO: gtid_strip_chars routine for this strcpy(heading, word + 1); heading[strlen(heading) - 1] = '\0'; MXS_INFO("Requesting GTID (%s) from Master server.", !heading[0] ? "empty value" : heading); /* Parse the non empty GTID value */ if (heading[0] && !blr_parse_gtid(heading, >id_elms)) { const char err_fmt[] = "Invalid format for GTID ('%s')" " set request; use 'X-Y-Z'"; char err_msg[sizeof(err_fmt) + GTID_MAX_LEN + 1]; sprintf(err_msg, err_fmt, heading); MXS_ERROR("%s", err_msg); /* Stop Master registration */ blr_slave_send_error_packet(slave, err_msg, 1198, NULL); } else { strcpy(router->last_mariadb_gtid, heading); blr_slave_send_ok(router, slave); } return true; } } else { MXS_ERROR("Master GTID registration needs 'mariadb10_master_gtid'" " option to be set."); blr_slave_send_error_packet(slave, "Master GTID registration needs" " 'mariadb10_master_gtid' option" " to be set first.", 1198, NULL); return true; } } else if (strcasestr(word, "@slave_connect_state") != NULL) { /* If not mariadb an error message will be returned */ if (slave->mariadb10_compat && router->mariadb10_gtid && (word = strtok_r(NULL, sep, &brkb)) != NULL) { char heading[GTID_MAX_LEN + 1]; MXS_DEBUG("Received GTID request '%s' from slave %u", word, slave->serverid); strcpy(heading, word + 1); heading[strlen(heading) - 1] = '\0'; /** * Set the GTID string, it could be an empty * in case of a fresh new setup. */ MXS_FREE(slave->mariadb_gtid); slave->mariadb_gtid = MXS_STRDUP_A(heading); blr_slave_send_ok(router, slave); return true; } else { MXS_ERROR("GTID Master registration is not enabled"); return false; } } else if (strcasecmp(word, "@slave_gtid_strict_mode") == 0) { /* If not mariadb an error message will be returned */ if (slave->mariadb10_compat && router->mariadb10_gtid && (word = strtok_r(NULL, sep, &brkb)) != NULL) { /* Set strict mode */ slave->gtid_strict_mode = atoi(word); blr_slave_send_ok(router, slave); return true; } } else if (strcasecmp(word, "@slave_gtid_ignore_duplicates") == 0) { /* If not mariadb an error message will be returned */ if (slave->mariadb10_compat && router->mariadb10_gtid) { blr_slave_send_ok(router, slave); return true; } } else if (strcasecmp(word, "NAMES") == 0) { if ((word = strtok_r(NULL, sep, &brkb)) == NULL) { MXS_ERROR("%s: Truncated SET NAMES command.", router->service->name()); return false; } else if (strcasecmp(word, "latin1") == 0) { blr_slave_replay(router, slave, router->saved_master.setnames); return true; } else if (strcasecmp(word, "utf8") == 0) { blr_slave_replay(router, slave, router->saved_master.utf8); return true; } else { blr_slave_send_ok(router, slave); return true; } } else if (strcasecmp(word, "SQL_MODE") == 0) { blr_slave_send_ok(router, slave); return true; } return false; } /** * Handle received admin statements from clients * * if an admin statement is one of suported one * a proper reply to the connected client is done * * @param router Router instance * @param slave Connected client/slave server * @param admin_stmt The admin statement * @param admin_opts The admin command options * @return True for handled queries, False otherwise */ static bool blr_handle_admin_stmt(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, char* admin_stmt, char* admin_opts) { char* word; char* brkb; const char* sep = " \t,="; if (admin_opts == NULL || !admin_opts[0]) { MXS_ERROR("%s: Incomplete admin command.", router->service->name()); return false; } /* Handle PURGE command */ else if (strcasecmp(admin_stmt, "PURGE") == 0) { if (router->master_state != BLRM_SLAVE_STOPPED) { blr_slave_send_error_packet(slave, "Cannot execute PURGE BINARY LOGS " "with a running slave; " "run STOP SLAVE first.", 1198, NULL); return true; } /* Check for GTID support */ if (router->mariadb10_gtid) { blr_purge_binary_logs(router, slave, admin_opts); } else { const char* errmsg = "PURGE BINARY LOGS needs the " "'mariadb10_slave_gtid' option to be set."; MXS_ERROR("%s: %s", errmsg, router->service->name()); blr_slave_send_error_packet(slave, errmsg, 1198, NULL); } return true; } /* Handle RESET command */ else if (strcasecmp(admin_stmt, "RESET") == 0) { if ((word = strtok_r(admin_opts, sep, &brkb)) == NULL) { MXS_ERROR("%s: Incomplete RESET command.", router->service->name()); return false; } /* RESET the current configured master cfg */ else if (strcasecmp(word, "SLAVE") == 0) { if (router->master_state == BLRM_SLAVE_STOPPED) { char error_string[BINLOG_ERROR_MSG_LEN + 1] = ""; MasterServerConfig current_master; int removed_cfg = 0; /* get current data */ blr_master_get_config(router, ¤t_master); MXS_NOTICE("%s: 'RESET SLAVE executed'. Previous state MASTER_HOST='%s', " "MASTER_PORT=%i, MASTER_LOG_FILE='%s', MASTER_LOG_POS=%lu, " "MASTER_USER='%s'", router->service->name(), current_master.host.c_str(), current_master.port, current_master.logfile.c_str(), current_master.pos, current_master.user.c_str()); /* remove master.ini */ static const char MASTER_INI[] = "/master.ini"; char path[strlen(router->binlogdir) + sizeof(MASTER_INI)]; strcpy(path, router->binlogdir); strcat(path, MASTER_INI); /* remove master.ini */ removed_cfg = unlink(path); if (removed_cfg == -1) { snprintf(error_string, BINLOG_ERROR_MSG_LEN, "Error removing %s, %s, errno %u", path, mxs_strerror(errno), errno); MXS_ERROR("%s: %s", router->service->name(), error_string); } pthread_mutex_lock(&router->lock); /* Set the BLRM_UNCONFIGURED state */ router->master_state = BLRM_UNCONFIGURED; blr_master_set_empty_config(router); /* Remove any error message and errno */ free(router->m_errmsg); router->m_errmsg = NULL; router->m_errno = 0; pthread_mutex_unlock(&router->lock); if (removed_cfg == -1) { blr_slave_send_error_packet(slave, error_string, 1201, NULL); } else { blr_slave_send_ok(router, slave); } return true; } else { if (router->master_state == BLRM_UNCONFIGURED) { blr_slave_send_ok(router, slave); } else { blr_slave_send_error_packet(slave, "This operation cannot be performed " "with a running slave; run STOP SLAVE first", 1198, NULL); } return true; } } } /* Start replication from the current configured master */ else if (strcasecmp(admin_stmt, "START") == 0) { if ((word = strtok_r(admin_opts, sep, &brkb)) == NULL) { MXS_ERROR("%s: Incomplete START command.", router->service->name()); return false; } else if (strcasecmp(word, "SLAVE") == 0) { blr_start_slave(router, slave); return true; } } /* Stop replication from the current master*/ else if (strcasecmp(admin_stmt, "STOP") == 0) { if ((word = strtok_r(admin_opts, sep, &brkb)) == NULL) { MXS_ERROR("%s: Incomplete STOP command.", router->service->name()); return false; } else if (strcasecmp(word, "SLAVE") == 0) { blr_stop_slave(router, slave); return true; } } /* Change the server to replicate from */ else if (strcasecmp(admin_stmt, "CHANGE") == 0) { if ((word = strtok_r(admin_opts, sep, &brkb)) == NULL) { MXS_ERROR("%s: Incomplete CHANGE command.", router->service->name()); return false; } else if (strcasecmp(word, "MASTER") == 0) { if (router->master_state != BLRM_SLAVE_STOPPED && router->master_state != BLRM_UNCONFIGURED) { blr_slave_send_error_packet(slave, "Cannot change master with a running slave; " "run STOP SLAVE first", (unsigned int)1198, NULL); return true; } else { int rc; char error_string[BINLOG_ERROR_MSG_LEN + 1 + BINLOG_ERROR_MSG_LEN + 1] = ""; // TODO: Why is this without a lock, but blr_master_apply_config() below with // TODO: a lock. One of them must be wrong. MasterServerConfig current_master; blr_master_get_config(router, ¤t_master); vector configs = router->configs; rc = blr_handle_change_master(router, brkb, error_string); if (rc < 0) { /* CHANGE MASTER TO has failed */ blr_slave_send_error_packet(slave, error_string, 1234, "42000"); return true; } else { int ret; char error[BINLOG_ERROR_MSG_LEN + 1]; /* Write/Update master config into master.ini file */ ret = blr_file_write_master_config(router, error); if (ret != 0) { /* file operation failure: restore config */ pthread_mutex_lock(&router->lock); blr_master_apply_config(router, current_master); pthread_mutex_unlock(&router->lock); snprintf(error_string, sizeof(error_string), "Error writing into %s/master.ini: %s", router->binlogdir, error); MXS_ERROR("%s: %s", router->service->name(), error_string); blr_slave_send_error_packet(slave, error_string, 1201, NULL); return true; } /* Mark as active the master server struct */ pthread_mutex_lock(&router->lock); if (!router->service->dbref->server->is_active) { router->service->dbref->server->is_active = true; router->service->dbref->active = true; } pthread_mutex_unlock(&router->lock); /** * check if router is BLRM_UNCONFIGURED * and change state to BLRM_SLAVE_STOPPED */ if (rc == 1 || router->master_state == BLRM_UNCONFIGURED) { pthread_mutex_lock(&router->lock); router->master_state = BLRM_SLAVE_STOPPED; pthread_mutex_unlock(&router->lock); /* * The binlog server has just been configured * master.ini file written in router->binlogdir. * * Create the binlog_name specified in MASTER_LOG_FILE * only if MariaDB GTID 'mariadb10_master_gtid' is Off */ if (!router->mariadb10_master_gtid && blr_file_new_binlog(router, router->binlog_name)) { MXS_INFO("%s: 'master.ini' created, binlog file '%s' created", router->service->name(), router->binlog_name); } blr_slave_send_ok(router, slave); return true; } if (router->trx_safe && router->pending_transaction.state > BLRM_NO_TRANSACTION) { if (strcmp(router->binlog_name, router->prevbinlog) != 0) { char message[BINLOG_ERROR_MSG_LEN + 1] = ""; snprintf(message, BINLOG_ERROR_MSG_LEN, "1105:Partial transaction in file %s starting at pos %lu, " "ending at pos %lu will be lost with next START SLAVE command", current_master.logfile.c_str(), current_master.safe_pos, current_master.pos); blr_slave_send_warning_message(router, slave, message); return true; } } /* * The CHAMGE MASTER command might specify a new binlog file. * Let's create the binlog_name specified in MASTER_LOG_FILE * only if MariaDB GTID 'mariadb10_master_gtid' is Off */ if (!router->mariadb10_master_gtid && (strlen(router->prevbinlog) && strcmp(router->prevbinlog, router->binlog_name) != 0)) { if (blr_file_new_binlog(router, router->binlog_name)) { MXS_INFO("%s: created new binlog file '%s' by " "'CHANGE MASTER TO' command", router->service->name(), router->binlog_name); } } blr_slave_send_ok(router, slave); return true; } } } } /* Discnnect conneted client(s) */ else if (strcasecmp(admin_stmt, "DISCONNECT") == 0) { if ((word = strtok_r(admin_opts, sep, &brkb)) == NULL) { MXS_ERROR("%s: Incomplete DISCONNECT command.", router->service->name()); return false; } else if (strcasecmp(word, "ALL") == 0) { blr_slave_disconnect_all(router, slave); return true; } else if (strcasecmp(word, "SERVER") == 0) { if ((word = strtok_r(NULL, sep, &brkb)) == NULL) { MXS_ERROR("%s: Expected DISCONNECT SERVER $server_id", router->service->name()); return false; } else { int serverid = atoi(word); blr_slave_disconnect_server(router, slave, serverid); return true; } } } return false; } /** * Skip reading empty binlog files (4 bytes only) * * @param router Current router instance * @param slave Current connected slave */ static void blr_slave_skip_empty_files(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave) { char binlog_file[BINLOG_FNAMELEN + 1]; char router_curr_file[BINLOG_FNAMELEN + 1]; char file_path[PATH_MAX + 1] = ""; unsigned int seqno; bool skipped_files = false; char t_prefix[BINLOG_FILE_EXTRA_INFO] = ""; MARIADB_GTID_INFO* f_tree = router->storage_type == BLR_BINLOG_STORAGE_TREE ? &slave->f_info : NULL; char next_file[BINLOG_FNAMELEN + 1] = ""; // Save the current router binlog filename pthread_mutex_lock(&router->binlog_lock); strcpy(router_curr_file, router->binlog_name); pthread_mutex_unlock(&router->binlog_lock); // Set the starting filename strcpy(binlog_file, slave->binlog_name); // Add tree prefix if (f_tree) { sprintf(t_prefix, "%" PRIu32 "/%" PRIu32 "/", f_tree->gtid_elms.domain_id, f_tree->gtid_elms.server_id); } // Get binlog filename full-path blr_get_file_fullpath(binlog_file, router->binlogdir, file_path, t_prefix[0] ? t_prefix : NULL); /** * Get the next file in sequence or next by GTID maps * if current file has 4 bytes size or it doesn't exist at all. * Stop if the new file is the current binlog file. */ while (!blr_compare_binlogs(router, &f_tree->gtid_elms, router_curr_file, binlog_file) && blr_slave_get_file_size(file_path) <= 4 && blr_file_next_exists(router, slave, next_file)) { // Log skipped file MXS_INFO("Slave %s:%i, skip reading empty file '%s' " "(0 or 4 bytes size).", slave->dcb->remote, dcb_get_port(slave->dcb), binlog_file); // Update binlog_file name strcpy(binlog_file, next_file); // Get binlog file full-path blr_get_file_fullpath(binlog_file, router->binlogdir, file_path, t_prefix[0] ? t_prefix : NULL); skipped_files = true; } // One or more files skipped: set last found filename and pos = 4 if (skipped_files) { strcpy(slave->binlog_name, binlog_file); slave->binlog_pos = 4; } } /** * Get the full path of a binlog filename. * * @param binlog_file The binlog filename * @param root_dir The binlog storage directory * @param full_path The output fullpahth name: * the memory area must be preallocated. * @param t_prefix The file_tree prefix with rep_domain * and server_id. */ static inline void blr_get_file_fullpath(const char* binlog_file, const char* root_dir, char* full_path, const char* t_prefix) { strcpy(full_path, root_dir); strcat(full_path, "/"); if (t_prefix) { strcat(full_path, t_prefix); } strcat(full_path, binlog_file); } /** * Returns the list of binlog files * saved in GTID repo. * * It's called olny if mariadb10_slave_gtid option is set * * @param router The router instance * @param slave The connected client * @param extra_data Whether to dispay path file * info before filename * @retun Sent bytes */ static int blr_show_binary_logs(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, const char* extra_data) { char current_file[BINLOG_FNAMELEN]; uint64_t current_pos = 0; static const char select_query[] = "SELECT binlog_file, " "MAX(end_pos) AS size, " "rep_domain, " "server_id " "FROM gtid_maps " "GROUP BY binlog_file " "ORDER BY id ASC;"; static const char select_query_full[] = "SELECT binlog_file, " "MAX(end_pos) AS size, " "rep_domain, " "server_id " "FROM gtid_maps " "GROUP BY rep_domain, " "server_id, " "binlog_file " "ORDER BY id ASC;"; int seqno; char* errmsg = NULL; BINARY_LOG_DATA_RESULT result = {}; /* Get current binlog finename and position */ pthread_mutex_lock(&router->binlog_lock); strcpy(current_file, router->binlog_name); current_pos = router->current_pos; pthread_mutex_unlock(&router->binlog_lock); /** * First part of result set: * send 2 columns and their defintions. */ /* This call sets seq to 1 in the packet */ blr_slave_send_fieldcount(router, slave, 2); /* Set 'seqno' counter to next value: 2 */ seqno = 2; /* Col 1 def */ blr_slave_send_columndef(router, slave, "Log_name", BLR_TYPE_STRING, 40, seqno++); /* Col 2 def */ blr_slave_send_columndef(router, slave, "File_size", BLR_TYPE_INT, 40, seqno++); /* Cols EOF */ blr_slave_send_eof(router, slave, seqno); /* Increment sequence */ seqno++; /* Initialise the result data struct */ result.seq_no = seqno; result.client = slave->dcb; result.last_file = NULL; result.binlogdir = router->binlogdir; result.use_tree = router->storage_type == BLR_BINLOG_STORAGE_TREE; /** * Second part of result set: * * add rows for select binlog files. * * Note: * - result.last_file is freed and updated by binary_logs_select_cb() * - result.seq_no is increased */ if (sqlite3_exec(router->gtid_maps, !result.use_tree ? select_query : select_query_full, binary_logs_select_cb, &result, &errmsg) != SQLITE_OK) { MXS_ERROR("Failed to exec 'SELECT binlog_file FROM gtid_maps': " "%s", errmsg ? errmsg : "database is not available"); sqlite3_free(errmsg); /* Free last_file */ MXS_FREE(result.last_file); result.last_file = NULL; /* Add EOF for empty result set */ return blr_slave_send_eof(router, slave, result.seq_no); } /* Use seqno of last sent packet */ seqno = result.seq_no; /** * Check whether the last file is the current binlog file, * GTID repo might also contain no data at all. * * Add the new row if needed. */ if (!result.last_file || strcmp(current_file, result.last_file) != 0) { char pos[40]; // Buffer for a 64-bit integer. GWBUF* pkt; /* Free last file */ MXS_FREE(result.last_file); /* Create the string value for pos */ sprintf(pos, "%" PRIu64, current_pos); char* filename; char last_filename[BINLOG_FILE_EXTRA_INFO + strlen(current_file) + 1]; if (result.use_tree) { char t_prefix[BINLOG_FILE_EXTRA_INFO]; sprintf(t_prefix, "%" PRIu32 "/%" PRIu32 "/", router->mariadb10_gtid_domain, router->orig_masterid); // Add prefix before filename sprintf(last_filename, "%s%s", t_prefix, current_file); filename = last_filename; } else { filename = current_file; } /* Create & write the new row */ if ((pkt = blr_create_result_row(filename, pos, seqno)) != NULL) { MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); /* Increment sequence */ seqno++; } } /* Add the result set EOF */ return blr_slave_send_eof(router, slave, seqno); } /** * Creates a Result Set row with two STRING columns * * @param val1 First column value * @param val2 Second column value * @param seq_no Sequence number for this row * @return An allocated GWBUF or NULL */ GWBUF* blr_create_result_row(const char* val1, const char* val2, int seq_no) { int val1_len = strlen(val1); int val2_len = strlen(val2); GWBUF* pkt; uint8_t* ptr; int len = MYSQL_HEADER_LEN + (1 + val1_len + (1 + val2_len)); // Allocate a new GWBUF buffer if ((pkt = gwbuf_alloc(len)) == NULL) { return NULL; } ptr = GWBUF_DATA(pkt); // Add length of data packet encode_value(ptr, len - MYSQL_HEADER_LEN, 24); ptr += 3; // Sequence number in response *ptr++ = seq_no; // Length of result string "val1" *ptr++ = val1_len; memcpy((char*)ptr, val1, val1_len); ptr += val1_len; // Length of result string "val2" *ptr++ = val2_len; memcpy((char*)ptr, val2, val2_len); return pkt; } /** * Binary logs select callback for sqlite3 database * * @param data Data pointer from caller * @param cols Number of columns * @param values The values * @param names The column names * * @return 0 on success, 1 otherwise */ static int binary_logs_select_cb(void* data, int cols, char** values, char** names) { BINARY_LOG_DATA_RESULT* data_set = (BINARY_LOG_DATA_RESULT*)data; DCB* dcb = data_set->client; int ret = 1; // Failure uint32_t fsize; char file_size[40]; mxb_assert(cols >= 4 && dcb); if (values[0] // File Name && values[1] // File Size && values[2] // Domain ID && values[3]) // Server ID { GWBUF* pkt; char file_path[PATH_MAX + 1]; char filename[1 + strlen(values[0]) + BINLOG_FILE_EXTRA_INFO]; char t_prefix[BINLOG_FILE_EXTRA_INFO] = ""; sprintf(t_prefix, "%s/%s/", values[2], // domain ID values[3]); // server ID fsize = atoll(values[1]); /* File size != 0 && server ID != 0 */ mxb_assert(fsize && atoll(values[3])); /** * In GTID repo binlog file last pos is last GTID. * In case of rotate_event or any event the "file_size" * it's not correct. * In case of binlog files with no transactions at all * the saved size is 4. * * Let's get the real size by calling blr_slave_get_file_size() */ // Get filename full-path, use prefix only if binlog_structure is TREE blr_get_file_fullpath(values[0], data_set->binlogdir, file_path, data_set->use_tree ? t_prefix : NULL); // Get the file size fsize = blr_slave_get_file_size(file_path); sprintf(file_size, "%" PRIu32 "", fsize); // Include prefix in the output if (data_set->use_tree) { sprintf(filename, "%s%s", t_prefix, values[0]); // filename } else { sprintf(filename, "%s", values[0]); // filename only } /* Create the MySQL Result Set row */ if ((pkt = blr_create_result_row(filename, // File name file_size, // File size data_set->seq_no)) != NULL) { /* Increase sequence for next row */ data_set->seq_no++; /* Free last file name */ MXS_FREE(data_set->last_file); /* Set last file name */ data_set->last_file = MXS_STRDUP_A(values[0]); /* Write packet to client */ MXS_SESSION_ROUTE_REPLY(dcb->session, pkt); /* Set success */ ret = 0; } return ret; /* Return success or fallure */ } else { return 0; /* Success: no data from db or end of result set */ } } /** * Handle SELECT @@server_id, @@read_only * that MaxScale MySQL monitor sends to monitored servers * * @param router The router instance * @param slave The connected client * @return Number of bytes written */ static int blr_slave_send_id_ro(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave) { int seqno; GWBUF* pkt; /** * First part of result set: * send 2 columns and their defintions. */ /* This call sets seq to 1 in the packet */ blr_slave_send_fieldcount(router, slave, 2); /* Set 'seqno' counter to next value: 2 */ seqno = 2; /* Col 1 def */ blr_slave_send_columndef(router, slave, "@@server_id", BLR_TYPE_INT, 40, seqno++); /* Col 2 def */ blr_slave_send_columndef(router, slave, "@@read_only", BLR_TYPE_INT, 40, seqno++); /* Cols EOF */ blr_slave_send_eof(router, slave, seqno++); /* Create the MySQL Result Set row */ char server_id[40] = ""; /* Set identy for MySQL replication monitor */ sprintf(server_id, "%d", router->set_master_server_id ? router->masterid : router->serverid); if ((pkt = blr_create_result_row(server_id, // File name "0", // o = OFF seqno++)) != NULL) { /* Write packet to client */ MXS_SESSION_ROUTE_REPLY(slave->dcb->session, pkt); } /* Add the result set EOF and return */ return blr_slave_send_eof(router, slave, seqno); } /** * Handle a SELECT with more than one column. * * Only SELECT @@server_id, @@read_only is supported. * That query is sent by MaxScale MySQL monitor. * * @param router The router instance * @param slave The connected client * @param col1 The first column * @param coln Whatever is after first column * @return True is handled, false otherwise */ static bool blr_handle_complex_select(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, const char* col1, const char* coln) { /* Strip leading spaces */ while (isspace(*coln)) { coln++; } if ((strcasecmp(col1, "@@server_id") == 0 || strcasecmp(col1, "@@global.server_id") == 0) && (strcasecmp(coln, "@@read_only") == 0 || strcasecmp(coln, "@@global.read_only") == 0)) { blr_slave_send_id_ro(router, slave); return true; } else { return false; } } /** * Purge binary logs find binlog callback for sqlite3 database * * @param data Data pointer from caller * @param cols Number of columns * @param values The values * @param names The column names * * @return 0 on success, 1 otherwise */ static int binary_logs_find_file_cb(void* data, int cols, char** values, char** names) { mxb_assert(cols == 2); BINARY_LOG_DATA_RESULT* data_set = (BINARY_LOG_DATA_RESULT*)data; if (values[0]) // Server ID { data_set->rowid = atoll(values[0]); } return 0; } /** * Purge binary logs delete files callback for sqlite3 database * * @param data Data pointer from caller * @param cols Number of columns * @param values The values * @param names The column names * * @return 0 on success, 1 otherwise */ static int binary_logs_purge_cb(void* data, int cols, char** values, char** names) { mxb_assert(cols == 2); BINARY_LOG_DATA_RESULT* result_data = (BINARY_LOG_DATA_RESULT*)data; if (values[0] && values[1]) { char* filename; char full_path[PATH_MAX + 1]; /* values[0] is filename, values[1] is prefix + file */ filename = !result_data->use_tree ? values[0] : values[1]; sprintf(full_path, "%s/%s", result_data->binlogdir, filename); MXS_DEBUG("Deleting binlog file %s", full_path); if (unlink(full_path) == -1 && errno != ENOENT) { MXS_ERROR("Failed to remove binlog file '%s': %d, %s", full_path, errno, mxs_strerror(errno)); } result_data->n_files++; } return 0; } /** * Parse the PURGE BINARY LOGS TO 'file' SQL statement. * * @param purge_command The SQL command to parse * @return The file found in the command. * or NULL in case of parse errors. */ static const char* blr_purge_getfile(char* purge_command) { char* word; char* brkb; const char* sep = " \t"; word = strtok_r(purge_command, sep, &brkb); // Check BINARY if (strcasecmp(word, "BINARY") != 0) { MXS_ERROR("Invalid PURGE command: PURGE %s", word); return NULL; } word = strtok_r(NULL, sep, &brkb); // Check LOGS if (!word || strcasecmp(word, "LOGS") != 0) { MXS_ERROR("Invalid PURGE command: PURGE BINARY %s", word ? word : ""); return NULL; } word = strtok_r(NULL, sep, &brkb); // Nothing else, return error if (!word) { MXS_ERROR("Invalid PURGE command: PURGE BINARY LOGS"); return NULL; } else // Check for TO 'file' { if (strcasecmp(word, "TO") != 0) { MXS_ERROR("Invalid PURGE command: PURGE BINARY LOGS %s", word); return NULL; } // Get filename if ((word = strtok_r(NULL, sep, &brkb)) != NULL) { // Remove heading and trailing "'" char* p = word; if (*p == '\'') { word++; } if (p[strlen(p) - 1] == '\'') { p[strlen(p) - 1] = '\0'; } return word; } else { MXS_ERROR("Invalid PURGE command: PURGE BINARY LOGS TO"); return NULL; } } return NULL; } /** * Purge MaxScale binlog files * * The routine it's called olny if mariadb10_slave_gtid option is set * as the up to date list of binlog files is in the GTID maps repo. * * Note: the current binlog file is not deleted frm disk/db. * * @param router The router instance * @param slave The connected client * @param purge_opts The PURGE BINARY LOGS options * @retun Sent bytes */ static bool blr_purge_binary_logs(ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, char* purge_opts) { char* errmsg = NULL; size_t n_delete = 0; // Select first ROWID of user specifed file static const char find_file_tpl[] = "SELECT MIN(id) AS min_id, " "(rep_domain || '/' || " "server_id || '/' || " "binlog_file) AS file " "FROM gtid_maps " "WHERE binlog_file = '%s' " "GROUP BY binlog_file " "ORDER BY id ASC;"; // SELECT files with ROWID < given one and DELETE static const char delete_list_tpl[] = "SELECT binlog_file, " "(rep_domain || '/' || " "server_id || '/' || " "binlog_file) AS file " "FROM gtid_maps " "WHERE id < %" PRIu64 " " "GROUP BY file " "ORDER BY id ASC; " "DELETE FROM gtid_maps " "WHERE id < %" PRIu64 ";"; static char sql_stmt[GTID_SQL_BUFFER_SIZE]; BINARY_LOG_DATA_RESULT result; static const char* selected_file; /** * Parse PURGE BINARY LOGS TO 'file' statement */ if ((selected_file = blr_purge_getfile(purge_opts)) == NULL) { // Abort on parsing failure blr_slave_send_error_packet(slave, "Malformed PURGE BINARY LOGS TO 'file' detected.", 1064, "42000"); return false; } /* Initialise result data fields */ result.rowid = 0; result.n_files = 0; result.binlogdir = router->binlogdir; result.use_tree = router->storage_type == BLR_BINLOG_STORAGE_TREE; /* Use the provided name, no prefix: find the first row */ sprintf(sql_stmt, find_file_tpl, selected_file); /* Get file rowid */ if (sqlite3_exec(router->gtid_maps, sql_stmt, binary_logs_find_file_cb, &result, &errmsg) != SQLITE_OK) { MXS_ERROR("PURGE BINARY LOGS: failed to select ROWID of current file " "from GTID maps DB, %s, select [%s]", errmsg, sql_stmt); sqlite3_free(errmsg); blr_slave_send_error_packet(slave, "Cannot find current file in binlog GTID DB.", 1373, NULL); return false; } if (result.rowid) { /* Prepare SQL statement for ROWID < result.rowid */ sprintf(sql_stmt, delete_list_tpl, result.rowid, result.rowid); /* Purge all files with ROWID < result.rowid */ if (sqlite3_exec(router->gtid_maps, sql_stmt, binary_logs_purge_cb, &result, &errmsg) != SQLITE_OK) { MXS_ERROR("Failed to select list of files to purge" "from GTID maps DB: %s, select [%s]", errmsg, sql_stmt); sqlite3_free(errmsg); blr_slave_send_error_packet(slave, "Cannot build the purge list of files.", 1373, NULL); return false; } } else { blr_slave_send_error_packet(slave, "Target log not found in binlog index", 1373, NULL); return false; } MXS_INFO("Deleted %lu binlog files in %s", result.n_files, result.binlogdir); // Send OK and nithing else blr_slave_send_ok(router, slave); return true; } /** * Log previus master details configuration and new added options. * * @param router The current router instance * @param current_master The current master server details * @param change_master The options in CHANGE MASTER TO command */ static void blr_log_config_changes(ROUTER_INSTANCE* router, const MasterServerConfig& current_master, const ChangeMasterConfig& change_master) { /* Prepare heartbeat and retry msgs */ static const char heartbeat[] = ", MASTER_HEARTBEAT_PERIOD="; static const char retry[] = ", MASTER_CONNECT_RETRY="; std::string h; if (change_master.heartbeat_period != -1) { h = std::to_string(change_master.heartbeat_period); } std::string r; if (change_master.connect_retry != -1) { r = std::to_string(change_master.connect_retry); } char heartbeat_msg[sizeof(heartbeat) + h.length()]; char retry_msg[sizeof(retry) + r.length()]; heartbeat_msg[0] = 0; retry_msg[0] = 0; if (!h.empty()) { sprintf(heartbeat_msg, "%s%lu", heartbeat, router->heartbeat); // Display the current "long" value } if (!r.empty()) { sprintf(retry_msg, "%s%d", retry, router->retry_interval); // Display the current "long" value } /* Prepare GTID msg */ const char* gtid_msg = !change_master.use_mariadb10_gtid.empty() ? ", MASTER_USE_GTID=Slave_pos" : ""; /* Log previous state and new changes */ MXS_NOTICE("%s: 'CHANGE MASTER TO executed'. Previous state " "MASTER_HOST='%s', MASTER_PORT=%i, MASTER_LOG_FILE='%s', " "MASTER_LOG_POS=%lu, MASTER_USER='%s'. " "New state is MASTER_HOST='%s', MASTER_PORT=%i, " "MASTER_LOG_FILE='%s', MASTER_LOG_POS=%lu, " "MASTER_USER='%s'" "%s%s%s", router->service->name(), current_master.host.c_str(), current_master.port, current_master.logfile.c_str(), current_master.pos, current_master.user.c_str(), router->service->dbref->server->address, router->service->dbref->server->port, router->binlog_name, router->current_pos, router->user, gtid_msg, heartbeat_msg, retry_msg); } /** * Check whether connecting slave server can continue * the MySQL Slave protocol registration to binlog server. * * (1) Binlog Server should be configured. * * (2) Connecting Slave must be MariaDB 10 one, * if master->mariadb10_compat is set * * (3) If mariadb10_master_gtid option is set, * a slave without GTID Request will be rejected. * * @param router The router instance * @param slave The registering slave * @param check The check requested by the caller * @return true on succes, false otherwise */ static bool blr_check_connecting_slave(const ROUTER_INSTANCE* router, ROUTER_SLAVE* slave, enum blr_slave_check check) { int rv = true; const char* err_msg = NULL; const char* err_status = "HY000"; int err_code = BINLOG_FATAL_ERROR_READING; const char* msg_detail = ""; switch (check) { case BLR_SLAVE_CONNECTING: // (1) if (router->master_state == BLRM_UNCONFIGURED) { err_msg = "Binlog router is not yet configured" " for replication."; rv = false; } break; case BLR_SLAVE_IS_MARIADB10: // (2) /** * If Master is MariaDB10 don't allow registration from * MariaDB/Mysql 5 Slaves */ if (router->mariadb10_compat && !slave->mariadb10_compat) { err_msg = "MariaDB 10 Slave is required" " for Slave registration."; rv = false; } break; case BLR_SLAVE_HAS_MARIADB10_GTID: // (3) /** * Check for mariadb10_master_gtid option set to On and * connecting slave with GTID request. */ if (router->mariadb10_master_gtid && !slave->mariadb_gtid) { err_msg = "MariaDB 10 Slave GTID is required" " for Slave registration."; msg_detail = " Please use: CHANGE MASTER TO master_use_gtid=slave_pos."; rv = false; } break; default: MXS_WARNING("%s: Slave %s: Unkwon status check %d.", router->service->name(), slave->dcb->remote, check); break; } if (!rv) { /* Force BLRS_ERRORED state */ pthread_mutex_lock(&slave->catch_lock); slave->state = BLRS_ERRORED; pthread_mutex_unlock(&slave->catch_lock); /* Send error that stops slave replication */ blr_send_custom_error(slave->dcb, ++slave->seqno, 0, err_msg, err_status, err_code); MXS_ERROR("%s: Slave %s: %s%s", router->service->name(), slave->dcb->remote, err_msg, msg_detail); } return rv; } /** * Abort the change master process * * @param router The router instance * @param current_master Current master configuration * @param change_master The CHANGE MASTER TO options * @param error Error message to log */ static void blr_abort_change_master(ROUTER_INSTANCE* router, const MasterServerConfig& current_master, const char* error) { MXS_ERROR("%s: %s", router->service->name(), error); /* restore previous master_host and master_port */ blr_master_restore_config(router, current_master); } /** * Abort the COM_BINLOG_DUMP slave request * * @param slave The connecting slave * @param errmsg Error message to send and log */ static void blr_slave_abort_dump_request(ROUTER_SLAVE* slave, const char* errmsg) { MXS_ERROR("Slave %lu requests COM_BINLOG_DUMP: %s. Aborting.", (unsigned long)slave->serverid, errmsg); blr_send_custom_error(slave->dcb, slave->seqno++, 0, errmsg, "HY000", BINLOG_FATAL_ERROR_READING); } /** * Check whether the current binlog file can be changed * * @param router The curret router instance * @param change_master The options in CHANGE_MASTER TO command * @param error Pointer to outout error message * * @return True if binlog can be changed or false. */ static bool blr_binlog_change_check(const ROUTER_INSTANCE* router, const ChangeMasterConfig& change_master, char* error) { char* master_logfile = NULL; /** * MASTER_LOG_FILE is not set in CHANGE MASTER TO. * If binlog server is not configured and * mariadb10_master_gtid is not set, then return an error. */ if (change_master.binlog_file.empty()) { if (router->master_state == BLRM_UNCONFIGURED && !router->mariadb10_master_gtid) { snprintf(error, BINLOG_ERROR_MSG_LEN, "Router is not configured for master connection, " "MASTER_LOG_FILE is required"); return false; } else { return true; } } else /** * If binlog file is set in CHANGE MASTER TO * and MASTER_USE_GTID option is on, then return an error. */ { /** * Check first MASTER_USE_GTID option: * * if not present return an error: */ if (router->mariadb10_master_gtid && change_master.use_mariadb10_gtid.empty()) { snprintf(error, BINLOG_ERROR_MSG_LEN, "%s MASTER_USE_GTID=Slave_pos is required", router->master_state == BLRM_UNCONFIGURED ? "Router is not configured for master connection," : "Cannot use MASTER_LOG_FILE for master connection,"); return false; } else { return true; } } return true; } /** * Set the new binlog filenanme from MASTER_LOG_FILE * * If no name change, the current router file is set. * Note: * Empty filename, MASTER_LOG_FILE = '', can be passed only * if mariadb10_master_gtid option is set. * * @param router The router instance * @param binlog_file The binlog file in MASTER_LOG_FILE * @param error Ouput error message * @param new_logfile The new log file name * * @return True if binlog name has been set or false * on errors */ static bool blr_change_binlog_name(ROUTER_INSTANCE* router, const char* binlog_file, char** new_logfile, char* error) { bool ret = true; /* MASTER_LOG_FILE is not present in CHANGE MASTER TO */ if (binlog_file == NULL) { /* Use current binlog file */ *new_logfile = MXS_STRDUP_A(router->binlog_name); } else { /** * Change the binlog filename as from MASTER_LOG_FILE * New binlog file can be: * - the next in sequence router file * - current router file * - empty if router->mariadb10_master_gtid is set. */ *new_logfile = blr_set_master_logfile(router, binlog_file, error); if (*new_logfile == NULL) { if (!router->mariadb10_master_gtid || strlen(binlog_file) > 1) { /* Binlog name can not be changed */ ret = false; } else { *new_logfile = MXS_STRDUP_A(""); // Blank the error message error[0] = 0; } } } return ret; } static bool blr_change_binlog_name(ROUTER_INSTANCE* router, const std::string& binlog_file, char** new_logfile, char* error) { return blr_change_binlog_name(router, binlog_file.empty() ? nullptr : binlog_file.c_str(), new_logfile, error); } /** * Apply binlog filename and position changes * * @param router The current router instance * @param change_master The change master options * @param error The error message to fill * @param new_logfile The new binlog filename * set in previous stage * * @return True if changes have been set * or false on errors. */ static bool blr_apply_changes(ROUTER_INSTANCE* router, const ChangeMasterConfig& change_master, char* new_logfile, char* error) { bool ret = true; const char* master_log_pos = NULL; long long pos = 0; /* Set new binlog position from MASTER_LOG_POS */ master_log_pos = change_master.binlog_pos.empty() ? nullptr : change_master.binlog_pos.c_str(); if (master_log_pos == NULL) { pos = 0; } else { pos = atoll(master_log_pos); } /** * Binlog name and position are checked if: * - mariadb10_master_gtid is off and * - master connection is already configured. * Checks: * (1) filename is different from current router log file and pos != 4 * (2) position is not current one for current file * * Binlog file and pos can set after all checks. */ /* MariaDB 10 GTID request */ if (router->mariadb10_master_gtid) { if (!change_master.use_mariadb10_gtid.empty()) { /* MASTER_USE_GTID=Slave_pos is set */ MXS_INFO("%s: MASTER_USE_GTID is [%s]", router->service->name(), change_master.use_mariadb10_gtid.c_str()); } /* Always log the current GTID value with CHANGE_MASTER TO */ MXS_INFO("%s: CHANGE MASTER TO, current GTID value is [%s]", router->service->name(), router->last_mariadb_gtid); /* Always set empty filename at pos 4 with CHANGE_MASTER TO */ strcpy(router->binlog_name, ""); router->current_pos = 4; router->binlog_position = 4; router->current_safe_event = 4; } /* The new filename is not the current one */ else if (strcmp(new_logfile, router->binlog_name) != 0 && router->master_state != BLRM_UNCONFIGURED) { if (master_log_pos == NULL) { snprintf(error, BINLOG_ERROR_MSG_LEN, "Please provide an explicit MASTER_LOG_POS " "for new MASTER_LOG_FILE %s: " "Permitted binlog pos is %d. " "Current master_log_file=%s, master_log_pos=%lu", new_logfile, 4, router->binlog_name, router->current_pos); ret = false; } else { if (pos != 4) { snprintf(error, BINLOG_ERROR_MSG_LEN, "Can not set MASTER_LOG_POS to %s for " "MASTER_LOG_FILE %s: " "Permitted binlog pos is %d. " "Current master_log_file=%s, master_log_pos=%lu", master_log_pos, new_logfile, 4, router->binlog_name, router->current_pos); ret = false; } } /* Set new binlog name at pos 4 */ if (ret) { strcpy(router->binlog_name, new_logfile); router->current_pos = 4; router->binlog_position = 4; router->current_safe_event = 4; /** * Close current file binlog file, * next start slave will create the new one */ fsync(router->binlog_fd); close(router->binlog_fd); router->binlog_fd = -1; MXS_INFO("%s: New MASTER_LOG_FILE is [%s]", router->service->name(), router->binlog_name); } } else { /** * Same binlog or master connection not configured * Position cannot be different from * current pos or 4 (if BLRM_UNCONFIGURED). * Note: * pos is not checked if router->mariadb10_master_gtid is set */ if (router->master_state == BLRM_UNCONFIGURED) { if (master_log_pos != NULL && pos != 4) { snprintf(error, BINLOG_ERROR_MSG_LEN, "Can not set MASTER_LOG_POS to %s: " "Permitted binlog pos is 4. Specified master_log_file=%s", master_log_pos, new_logfile); ret = false; } else { /** * BLRM_UNCONFIGURED state: * - set pos to 4 * - set binlog name */ router->current_pos = 4; router->binlog_position = 4; router->current_safe_event = 4; strcpy(router->binlog_name, new_logfile); MXS_INFO("%s: New MASTER_LOG_FILE is [%s]", router->service->name(), router->binlog_name); } } else { if (master_log_pos != NULL && pos != static_cast(router->current_pos)) { snprintf(error, BINLOG_ERROR_MSG_LEN, "Can not set MASTER_LOG_POS to %s: " "Permitted binlog pos is %lu. " "Current master_log_file=%s, master_log_pos=%lu", master_log_pos, router->current_pos, router->binlog_name, router->current_pos); ret = false; } } if (ret) { MXS_INFO("%s: New MASTER_LOG_POS is [%lu]", router->service->name(), router->current_pos); } } return ret; } /** * Saves a MARIADB_GTID_INFO data for later usage * * @param info The MARIADB_GTID_INFO data to copy * @param save_info The MARIADB_GTID_INFO allocated * buffer to save data * @param save_prefix The allocated buffer where * to save file prefix */ static void blr_slave_info_save(const MARIADB_GTID_INFO* info, MARIADB_GTID_INFO* save_info, char* save_prefix) { /* Save current file details */ memcpy(save_info, info, sizeof(MARIADB_GTID_INFO)); /* Fill save file prefix */ sprintf(save_prefix, "%" PRIu32 "/%" PRIu32 "/", save_info->gtid_elms.domain_id, save_info->gtid_elms.server_id); } /** * Log message for slave file End Of File * * @param router The current router instance * @param slave The connected slave * @param c_prefix The file prefix of slave file * @param next_file The next file to read or fake rotate to * @param log_action The action type to log */ static void blr_slave_log_next_file_action(const ROUTER_INSTANCE* router, const ROUTER_SLAVE* slave, const char* c_prefix, const char* next_file, slave_eof_action_t log_action) { char m_prefix[BINLOG_FILE_EXTRA_INFO] = ""; char r_prefix[BINLOG_FILE_EXTRA_INFO] = ""; bool s_tree = router->storage_type == BLR_BINLOG_STORAGE_TREE; bool have_heartbeat = router->send_slave_heartbeat && (slave->heartbeat > 0); pthread_mutex_lock(&router->binlog_lock); if (s_tree) { /* Get master file prefix */ sprintf(m_prefix, "%" PRIu32 "/%" PRIu32 "/", router->mariadb10_gtid_domain, router->orig_masterid); /* Get rotating slave file prefix */ sprintf(r_prefix, "%" PRIu32 "/%" PRIu32 "/", slave->f_info.gtid_elms.domain_id, slave->f_info.gtid_elms.server_id); } pthread_mutex_unlock(&router->binlog_lock); switch (log_action) { case SLAVE_EOF_ROTATE: /* This has to be always logged */ MXS_WARNING("%s: Slave [%s]:%d, server-id %d reached end of file for binlog file [%s%s] " "at %lu which is not the file currently being downloaded or last file found. " "This may be caused by a previous failure of the master. " "Current master binlog is [%s%s] at %lu, replication state is [%s]. " "Now rotating to new file [%s%s]", router->service->name(), slave->dcb->remote, dcb_get_port(slave->dcb), slave->serverid, c_prefix, slave->binlog_name, (unsigned long)slave->binlog_pos, m_prefix, router->binlog_name[0] ? router->binlog_name : "no_set_yet", router->binlog_position, blrm_states[router->master_state], r_prefix, next_file); break; case SLAVE_EOF_ERROR: /* Log error */ MXS_ERROR("%s: Slave [%s]:%d, server-id %d reached " "end of file for '%s%s' and next file to read%s%s%s%s " "is not %s. Force replication abort after %d retries.", router->service->name(), slave->dcb->remote, dcb_get_port(slave->dcb), slave->serverid, c_prefix, slave->binlog_name, next_file[0] ? " '" : "", next_file[0] ? r_prefix : "", next_file, next_file[0] ? "'" : "", next_file[0] ? "accessible" : "existent", MISSING_FILE_READ_RETRIES); break; case SLAVE_EOF_WARNING: /* We don't have the next_file, just warning */ MXS_WARNING("%s: Slave [%s]:%d, server-id %d reached end " "of file for binlog file [%s%s] " "at %lu. This is the last downloaded or " "the last file found. " "Next file%s%s%s%s is not %s. " "This may be caused by a previous failure of " "the master server. Current master binlog is " "[%s%s] at %lu and replication state is [%s]. " "The slave server is now in '%s' state.", router->service->name(), slave->dcb->remote, dcb_get_port(slave->dcb), slave->serverid, c_prefix, slave->binlog_name, (unsigned long)slave->binlog_pos, next_file[0] ? " '" : "", next_file[0] ? r_prefix : "", next_file, next_file[0] ? "'" : "", next_file[0] ? "accessible" : "existent", m_prefix, router->binlog_name[0] ? router->binlog_name : "no_set_yet", router->binlog_position, blrm_states[router->master_state], have_heartbeat ? "wait_state" : "read_again"); break; default: break; } }