diff --git a/server/core/modutil.c b/server/core/modutil.c index 860f93cc9..355bb71aa 100644 --- a/server/core/modutil.c +++ b/server/core/modutil.c @@ -38,6 +38,19 @@ extern int lm_enabled_logfiles_bitmask; extern size_t log_ses_count[]; extern __thread log_info_t tls_log_info; +/** These are used when converting MySQL wildcards to regular expressions */ +static SPINLOCK re_lock = SPINLOCK_INIT; +static bool pattern_init = false; +static pcre2_code *re_percent = NULL; +static pcre2_code *re_single = NULL; +static pcre2_code *re_escape = NULL; +static const PCRE2_SPTR pattern_percent = (PCRE2_SPTR) "%"; +static const PCRE2_SPTR pattern_single = (PCRE2_SPTR) "([^\\\\]|^)_"; +static const PCRE2_SPTR pattern_escape = (PCRE2_SPTR) "[.]"; +static const char* sub_percent = ".*"; +static const char* sub_single = "$1."; +static const char* sub_escape = "\\."; + static void modutil_reply_routing_error( DCB* backend_dcb, int error, @@ -842,3 +855,119 @@ int modutil_count_statements(GWBUF* buffer) return num; } + +/** + * Initialize the PCRE2 patterns used when converting MySQL wildcards to PCRE syntax. + */ +void init_pcre2_patterns() +{ + spinlock_acquire(&re_lock); + if (!pattern_init) + { + int err; + size_t erroff; + PCRE2_UCHAR errbuf[STRERROR_BUFLEN]; + + if ((re_percent = pcre2_compile(pattern_percent, PCRE2_ZERO_TERMINATED, + 0, &err, &erroff, NULL)) && + (re_single = pcre2_compile(pattern_single, PCRE2_ZERO_TERMINATED, + 0, &err, &erroff, NULL)) && + (re_escape = pcre2_compile(pattern_escape, PCRE2_ZERO_TERMINATED, + 0, &err, &erroff, NULL))) + { + pattern_init = true; + } + else + { + pcre2_get_error_message(err, errbuf, sizeof(errbuf)); + skygw_log_write(LE, "Error: Failed to compile PCRE2 pattern: %s", + errbuf); + } + + if (!pattern_init) + { + pcre2_code_free(re_percent); + pcre2_code_free(re_single); + pcre2_code_free(re_escape); + re_percent = NULL; + re_single = NULL; + re_escape = NULL; + } + } + spinlock_release(&re_lock); +} + +/** + * Check if @c string matches @c pattern according to the MySQL wildcard rules. + * The wildcard character @c '%%' is replaced with @c '.*' and @c '_' is replaced + * with @c '.'. All Regular expression special characters are escaped before + * matching is made. + * @param pattern Wildcard pattern + * @param string String to match + * @return MXS_PCRE2_MATCH if the pattern matches, MXS_PCRE2_NOMATCH if it does + * not match and MXS_PCRE2_ERROR if an error occurred + * @see maxscale_pcre2.h + */ +mxs_pcre2_result_t modutil_mysql_wildcard_match(const char* pattern, const char* string) +{ + if (!pattern_init) + { + init_pcre2_patterns(); + } + mxs_pcre2_result_t rval = MXS_PCRE2_ERROR; + bool err = false; + PCRE2_SIZE matchsize = strlen(string) + 1; + PCRE2_SIZE tempsize = matchsize; + char* matchstr = (char*) malloc(matchsize); + char* tempstr = (char*) malloc(tempsize); + + pcre2_match_data *mdata_percent = pcre2_match_data_create_from_pattern(re_percent, NULL); + pcre2_match_data *mdata_single = pcre2_match_data_create_from_pattern(re_single, NULL); + pcre2_match_data *mdata_escape = pcre2_match_data_create_from_pattern(re_escape, NULL); + + if (matchstr && tempstr && mdata_percent && mdata_single && mdata_escape) + { + if (mxs_pcre2_substitute(re_escape, pattern, sub_escape, + &matchstr, &matchsize) == MXS_PCRE2_ERROR || + mxs_pcre2_substitute(re_single, matchstr, sub_single, + &tempstr, &tempsize) == MXS_PCRE2_ERROR || + mxs_pcre2_substitute(re_percent, tempstr, sub_percent, + &matchstr, &matchsize) == MXS_PCRE2_ERROR) + { + err = true; + } + + if (!err) + { + int errcode; + rval = mxs_pcre2_simple_match(matchstr, string, PCRE2_CASELESS, &errcode); + if (rval == MXS_PCRE2_ERROR) + { + if(errcode != 0) + { + PCRE2_UCHAR errbuf[STRERROR_BUFLEN]; + pcre2_get_error_message(errcode, errbuf, sizeof(errbuf)); + skygw_log_write(LE, "Error: Failed to match pattern: %s", + errbuf); + } + err = true; + } + } + } + else + { + err = true; + } + + if (err) + { + skygw_log_write(LE, "Error: Fatal error when matching wildcard patterns."); + } + + pcre2_match_data_free(mdata_percent); + pcre2_match_data_free(mdata_single); + pcre2_match_data_free(mdata_escape); + free(matchstr); + free(tempstr); + return rval; +} diff --git a/server/include/modutil.h b/server/include/modutil.h index 4964b7b31..090776483 100644 --- a/server/include/modutil.h +++ b/server/include/modutil.h @@ -34,6 +34,7 @@ #include #include #include +#include #define PTR_IS_RESULTSET(b) (b[0] == 0x01 && b[1] == 0x0 && b[2] == 0x0 && b[3] == 0x01) #define PTR_IS_EOF(b) (b[0] == 0x05 && b[1] == 0x0 && b[2] == 0x0 && b[4] == 0xfe) @@ -68,4 +69,5 @@ GWBUF *modutil_create_mysql_err_msg( const char *msg); int modutil_count_signal_packets(GWBUF*,int,int,int*); +mxs_pcre2_result_t modutil_mysql_wildcard_match(const char* pattern, const char* string); #endif diff --git a/server/modules/monitor/mysql_mon.c b/server/modules/monitor/mysql_mon.c index 23bdb23ad..4c0b6d4a2 100644 --- a/server/modules/monitor/mysql_mon.c +++ b/server/modules/monitor/mysql_mon.c @@ -51,8 +51,9 @@ * @endverbatim */ - #include +#include +#include /** Defined in log_manager.cc */ extern int lm_enabled_logfiles_bitmask; @@ -81,7 +82,10 @@ static void set_master_heartbeat(MYSQL_MONITOR *, MONITOR_SERVERS *); static void set_slave_heartbeat(MONITOR *, MONITOR_SERVERS *); static int add_slave_to_master(long *, int, long); bool isMySQLEvent(monitor_event_t event); +void check_maxscale_schema_replication(MONITOR *monitor); static bool report_version_err = true; +static const char* hb_table_name = "maxscale_schema.replication_heartbeat"; + static MONITOR_OBJECT MyObject = { startMonitor, stopMonitor, @@ -219,6 +223,7 @@ startMonitor(void *arg, void* opt) { memset(handle->events,true,sizeof(handle->events)); } + handle->tid = (THREAD)thread_start(monitorMain, monitor); return handle; } @@ -734,6 +739,7 @@ int num_servers=0; MONITOR_SERVERS *root_master = NULL; size_t nrounds = 0; int log_no_master = 1; +bool heartbeat_checked = false; spinlock_acquire(&mon->lock); handle = (MYSQL_MONITOR *)mon->handle; @@ -762,6 +768,13 @@ detect_stale_master = handle->detectStaleMaster; } /** Wait base interval */ thread_millisleep(MON_BASE_INTERVAL_MS); + + if (handle->replicationHeartbeat && !heartbeat_checked) + { + check_maxscale_schema_replication(mon); + heartbeat_checked = true; + } + /** * Calculate how far away the monitor interval is from its full * cycle and if monitor interval time further than the base @@ -1465,3 +1478,223 @@ bool isMySQLEvent(monitor_event_t event) } return false; } + +/** + * Check if replicate_ignore_table is defined and if maxscale_schema.replication_hearbeat + * table is in the list. + * @param database Server to check + * @return False if the table is not replicated or an error occurred when querying + * the server + */ +bool check_replicate_ignore_table(MONITOR_SERVERS* database) +{ + MYSQL_RES *result; + bool rval = true; + + if (mysql_query(database->con, + "show variables like 'replicate_ignore_table'") == 0 && + (result = mysql_store_result(database->con)) && + mysql_num_fields(result) > 1) + { + MYSQL_ROW row; + + while ((row = mysql_fetch_row(result))) + { + if (strlen(row[1]) > 0 && + strcasestr(row[1], hb_table_name)) + { + skygw_log_write(LE, "Warning: 'replicate_ignore_table' is " + "defined on server '%s' and '%s' was found in it. ", + database->server->unique_name, hb_table_name); + rval = false; + } + } + + mysql_free_result(result); + } + else + { + skygw_log_write(LE, "Error: Failed to query server %s for " + "'replicate_ignore_table': %s", + database->server->unique_name, + mysql_error(database->con)); + rval = false; + } + return rval; +} + +/** + * Check if replicate_do_table is defined and if maxscale_schema.replication_hearbeat + * table is not in the list. + * @param database Server to check + * @return False if the table is not replicated or an error occurred when querying + * the server + */ +bool check_replicate_do_table(MONITOR_SERVERS* database) +{ + MYSQL_RES *result; + bool rval = true; + + if (mysql_query(database->con, + "show variables like 'replicate_do_table'") == 0 && + (result = mysql_store_result(database->con)) && + mysql_num_fields(result) > 1) + { + MYSQL_ROW row; + + while ((row = mysql_fetch_row(result))) + { + if (strlen(row[1]) > 0 && + strcasestr(row[1], hb_table_name) == NULL) + { + skygw_log_write(LE, "Warning: 'replicate_do_table' is " + "defined on server '%s' and '%s' was not found in it. ", + database->server->unique_name, hb_table_name); + rval = false; + } + } + mysql_free_result(result); + } + else + { + skygw_log_write(LE, "Error: Failed to query server %s for " + "'replicate_do_table': %s", + database->server->unique_name, + mysql_error(database->con)); + rval = false; + } + return rval; +} + +/** + * Check if replicate_wild_do_table is defined and if it doesn't match + * maxscale_schema.replication_heartbeat. + * @param database Database server + * @return False if the table is not replicated or an error occurred when trying to + * query the server. + */ +bool check_replicate_wild_do_table(MONITOR_SERVERS* database) +{ + MYSQL_RES *result; + bool rval = true; + + if (mysql_query(database->con, + "show variables like 'replicate_wild_do_table'") == 0 && + (result = mysql_store_result(database->con)) && + mysql_num_fields(result) > 1) + { + MYSQL_ROW row; + + while ((row = mysql_fetch_row(result))) + { + if (strlen(row[1]) > 0) + { + mxs_pcre2_result_t rc = modutil_mysql_wildcard_match(row[1], hb_table_name); + if (rc == MXS_PCRE2_NOMATCH) + { + skygw_log_write(LE, "Warning: 'replicate_wild_do_table' is " + "defined on server '%s' and '%s' does not match it. ", + database->server->unique_name, + hb_table_name); + rval = false; + } + } + } + mysql_free_result(result); + } + else + { + skygw_log_write(LE, "Error: Failed to query server %s for " + "'replicate_wild_do_table': %s", + database->server->unique_name, + mysql_error(database->con)); + rval = false; + } + return rval; +} + + +/** + * Check if replicate_wild_ignore_table is defined and if it matches + * maxscale_schema.replication_heartbeat. + * @param database Database server + * @return False if the table is not replicated or an error occurred when trying to + * query the server. + */ +bool check_replicate_wild_ignore_table(MONITOR_SERVERS* database) +{ + MYSQL_RES *result; + bool rval = true; + + if (mysql_query(database->con, + "show variables like 'replicate_wild_ignore_table'") == 0 && + (result = mysql_store_result(database->con)) && + mysql_num_fields(result) > 1) + { + MYSQL_ROW row; + + while ((row = mysql_fetch_row(result))) + { + if (strlen(row[1]) > 0) + { + mxs_pcre2_result_t rc = modutil_mysql_wildcard_match(row[1], hb_table_name); + if (rc == MXS_PCRE2_MATCH) + { + skygw_log_write(LE, "Warning: 'replicate_wild_ignore_table' is " + "defined on server '%s' and '%s' matches it. ", + database->server->unique_name, + hb_table_name); + rval = false; + } + } + } + mysql_free_result(result); + } + else + { + skygw_log_write(LE, "Error: Failed to query server %s for " + "'replicate_wild_do_table': %s", + database->server->unique_name, + mysql_error(database->con)); + rval = false; + } + return rval; +} + +/** + * Check if the maxscale_schema.replication_heartbeat table is replicated on all + * servers and log a warning if problems were found. + * @param monitor Monitor structure + */ +void check_maxscale_schema_replication(MONITOR *monitor) +{ + MONITOR_SERVERS* database = monitor->databases; + bool err = false; + + while (database) + { + connect_result_t rval = mon_connect_to_db(monitor, database); + if (rval == MONITOR_CONN_OK) + { + if (!check_replicate_ignore_table(database) || + !check_replicate_do_table(database) || + !check_replicate_wild_do_table(database) || + !check_replicate_wild_ignore_table(database)) + { + err = true; + } + } + else + { + mon_log_connect_error(database, rval); + } + database = database->next; + } + + if (err) + { + skygw_log_write(LE, "Warning: Problems were encountered when " + "checking if '%s' is replicated. Make sure that the table is " + "replicated to all slaves.", hb_table_name); + } +}