From a732a4c9a22037329bca888f4b4d4d24d1386da3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Thu, 4 Jan 2018 11:33:06 +0200 Subject: [PATCH 01/47] Fix packaging when both RPM and DEB tools are installed If both RPM and DEB tools are installed, only DEB packages should be generated. --- cmake/package.cmake | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/cmake/package.cmake b/cmake/package.cmake index 20f88c52c..e228645ca 100644 --- a/cmake/package.cmake +++ b/cmake/package.cmake @@ -58,19 +58,25 @@ find_program(DEBBUILD dpkg-buildpackage) if(TARBALL) include(cmake/package_tgz.cmake) -elseif (NOT ( ${RPMBUILD} STREQUAL "RPMBUILD-NOTFOUND" ) OR NOT ( ${DEBBUILD} STREQUAL "DEBBUILD-NOTFOUND" )) - if(NOT ( ${RPMBUILD} STREQUAL "RPMBUILD-NOTFOUND" ) ) +elseif(${RPMBUILD} MATCHES "NOTFOUND" AND ${DEBBUILD} MATCHES "NOTFOUND") + message(FATAL_ERROR "Could not automatically resolve the package generator and no generators " + "defined on the command line. Please install distribution specific packaging software or " + "define -DTARBALL=Y to build tar.gz packages.") +else() + + if(${DEBBUILD} MATCHES "NOTFOUND") + # No DEB packaging tools found, must be an RPM system include(cmake/package_rpm.cmake) - endif() - if(NOT ( ${DEBBUILD} STREQUAL "DEBBUILD-NOTFOUND" ) ) + else() + # We have DEB packaging tools, must be a DEB system + if (NOT ${RPMBUILD} MATCHES "NOTFOUND") + # Debian based systems can have both RPM and DEB packaging tools + message(WARNING "Found both DEB and RPM packaging tools, generating DEB packages. If this is not correct, " + "remove the packaging tools for the package type you DO NOT want to create.") + endif() include(cmake/package_deb.cmake) endif() message(STATUS "You can install startup scripts and system configuration files for MaxScale by running the 'postinst' shell script located at ${CMAKE_INSTALL_PREFIX}.") message(STATUS "To remove these installed files, run the 'postrm' shell script located in the same folder.") - -else() - message(FATAL_ERROR "Could not automatically resolve the package generator and no generators " - "defined on the command line. Please install distribution specific packaging software or " - "define -DTARBALL=Y to build tar.gz packages.") endif() From e310bbbe53d7b1db8c4b007d62a2b169fac51752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Thu, 4 Jan 2018 14:53:03 +0200 Subject: [PATCH 02/47] Initialize query classifier in housekeeper thread The query classifier was not initialized for the housekeeper thread. This means that tasks could not use the query classifier and as the avro conversion is done inside a task, it can't use it. --- server/core/housekeeper.c | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/server/core/housekeeper.c b/server/core/housekeeper.c index d7406d72f..2e68199b3 100644 --- a/server/core/housekeeper.c +++ b/server/core/housekeeper.c @@ -18,6 +18,7 @@ #include #include #include +#include /** * @file housekeeper.c Provide a mechanism to run periodic tasks @@ -57,21 +58,30 @@ static THREAD hk_thr_handle; static void hkthread(void *); +struct hkinit_result +{ + sem_t sem; + bool ok; +}; + bool hkinit() { - bool inited = false; + struct hkinit_result res; + sem_init(&res.sem, 0, 0); + res.ok = false; - if (thread_start(&hk_thr_handle, hkthread, NULL) != NULL) + if (thread_start(&hk_thr_handle, hkthread, &res) != NULL) { - inited = true; + sem_wait(&res.sem); } else { MXS_ALERT("Failed to start housekeeper thread."); } - return inited; + sem_destroy(&res.sem); + return res.ok; } /** @@ -262,6 +272,16 @@ hkthread(void *data) void *taskdata; int i; + struct hkinit_result* res = (struct hkinit_result*)data; + res->ok = qc_thread_init(QC_INIT_BOTH); + + if (!res->ok) + { + MXS_ERROR("Could not initialize housekeeper thread."); + } + + sem_post(&res->sem); + while (!do_shutdown) { for (i = 0; i < 10; i++) @@ -301,6 +321,7 @@ hkthread(void *data) spinlock_release(&tasklock); } + qc_thread_end(QC_INIT_BOTH); MXS_NOTICE("Housekeeper shutting down."); } From 95ad3aa7df33577914ae96e893df1bcc59899824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Thu, 4 Jan 2018 14:55:16 +0200 Subject: [PATCH 03/47] MXS-1543: Log a warning if STATEMENT or MIXED is used If avrorouter suspects that statement based or mixed replication formats are being used, it logs a warning. --- server/modules/routing/avrorouter/avro_file.c | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/server/modules/routing/avrorouter/avro_file.c b/server/modules/routing/avrorouter/avro_file.c index 73df4ef98..f7bcdaa09 100644 --- a/server/modules/routing/avrorouter/avro_file.c +++ b/server/modules/routing/avrorouter/avro_file.c @@ -31,6 +31,7 @@ */ #include "avrorouter.h" +#include #include #include @@ -1046,6 +1047,25 @@ void handle_query_event(AVRO_INSTANCE *router, REP_HEADER *hdr, int *pending_tra len = tmpsz; unify_whitespace(sql, len); + static bool warn_not_row_format = true; + + if (warn_not_row_format) + { + GWBUF* buffer = gwbuf_alloc(len + 5); + gw_mysql_set_byte3(GWBUF_DATA(buffer), len + 1); + GWBUF_DATA(buffer)[4] = 0x03; + memcpy(GWBUF_DATA(buffer) + 5, sql, len); + qc_query_op_t op = qc_get_operation(buffer); + gwbuf_free(buffer); + + if (op == QUERY_OP_UPDATE || op == QUERY_OP_INSERT || op == QUERY_OP_DELETE) + { + MXS_WARNING("Possible STATEMENT or MIXED format binary log. Check that " + "'binlog_format' is set to ROW on the master."); + warn_not_row_format = false; + } + } + if (is_create_table_statement(router, sql, len)) { TABLE_CREATE *created = NULL; From c64fd4f39f4d52fcb05518c24e6550b03bbe9fb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Fri, 5 Jan 2018 08:52:36 +0200 Subject: [PATCH 04/47] MXS-1543: Add test case Added test case that checks that statement based replication is detected. --- maxscale-system-test/CMakeLists.txt | 4 ++++ maxscale-system-test/mxs1543.cpp | 32 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 maxscale-system-test/mxs1543.cpp diff --git a/maxscale-system-test/CMakeLists.txt b/maxscale-system-test/CMakeLists.txt index 31e6c5a21..9177d7daf 100644 --- a/maxscale-system-test/CMakeLists.txt +++ b/maxscale-system-test/CMakeLists.txt @@ -522,6 +522,10 @@ add_test_executable(mxs1516.cpp mxs1516 replication LABELS REPL_BACKEND) # https://jira.mariadb.org/browse/MXS-1542 add_test_executable(mxs1542.cpp mxs1542 avro LABELS REPL_BACKEND) +# MXS-1543: Avrorouter doesn't detect MIXED or STATEMENT format replication +# https://jira.mariadb.org/browse/MXS-1543 +add_test_executable(mxs1543.cpp mxs1543 avro LABELS REPL_BACKEND) + # MXS-1585: Crash in MaxScale 2.1.12 # https://jira.mariadb.org/browse/MXS-1585 add_test_executable(mxs1585.cpp mxs1585 mxs1585 LABELS REPL_BACKEND) diff --git a/maxscale-system-test/mxs1543.cpp b/maxscale-system-test/mxs1543.cpp new file mode 100644 index 000000000..6d185f92d --- /dev/null +++ b/maxscale-system-test/mxs1543.cpp @@ -0,0 +1,32 @@ +/** + * MXS-1543: https://jira.mariadb.org/browse/MXS-1543 + * + * Avrorouter doesn't detect MIXED or STATEMENT format replication + */ + +#include "testconnections.h" + +int main(int argc, char** argv) +{ + TestConnections::skip_maxscale_start(true); + TestConnections::check_nodes(false); + TestConnections test(argc, argv); + + test.replicate_from_master(); + + test.repl->connect(); + execute_query(test.repl->nodes[0], "CREATE OR REPLACE TABLE t1 (data VARCHAR(30))"); + execute_query(test.repl->nodes[0], "INSERT INTO t1 VALUES ('ROW')"); + execute_query(test.repl->nodes[0], "SET binlog_format=STATEMENT"); + execute_query(test.repl->nodes[0], "FLUSH LOGS"); + execute_query(test.repl->nodes[0], "INSERT INTO t1 VALUES ('STATEMENT')"); + execute_query(test.repl->nodes[0], "SET binlog_format=ROW"); + execute_query(test.repl->nodes[0], "FLUSH LOGS"); + execute_query(test.repl->nodes[0], "INSERT INTO t1 VALUES ('ROW2')"); + + // Wait for the avrorouter to process the data + sleep(10); + test.check_log_err("Possible STATEMENT or MIXED", true); + + return test.global_result; +} From ce1c45828ae366c612935e3e748b470f712b4b3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Fri, 5 Jan 2018 10:37:43 +0200 Subject: [PATCH 05/47] MXS-1575: Fix CREATE TABLE processing The CREATE TABLE processing failed to identify the explicit database names that were generated by mysqldump. --- server/modules/routing/avrorouter/avro_schema.c | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/server/modules/routing/avrorouter/avro_schema.c b/server/modules/routing/avrorouter/avro_schema.c index 9bdc47edb..fc61301d3 100644 --- a/server/modules/routing/avrorouter/avro_schema.c +++ b/server/modules/routing/avrorouter/avro_schema.c @@ -421,28 +421,33 @@ static bool get_database_name(const char* sql, char* dest) if (ptr) { ptr--; - while (*ptr == '`' || isspace(*ptr)) + while (ptr >= sql && (*ptr == '`' || isspace(*ptr))) { ptr--; } - while (*ptr != '`' && *ptr != '.' && !isspace(*ptr)) + while (ptr >= sql && *ptr != '`' && *ptr != '.' && !isspace(*ptr)) { ptr--; } - if (*ptr == '.') + while (ptr >= sql && (*ptr == '`' || isspace(*ptr))) + { + ptr--; + } + + if (ptr >= sql && *ptr == '.') { // The query defines an explicit database - while (*ptr == '`' || *ptr == '.' || isspace(*ptr)) + while (ptr >= sql && (*ptr == '`' || *ptr == '.' || isspace(*ptr))) { ptr--; } const char* end = ptr + 1; - while (*ptr != '`' && *ptr != '.' && !isspace(*ptr)) + while (ptr >= sql && *ptr != '`' && *ptr != '.' && !isspace(*ptr)) { ptr--; } @@ -1139,7 +1144,7 @@ void read_alter_identifier(const char *sql, const char *end, char *dest, int siz int len = 0; const char *tok = get_tok(sql, &len, end); // ALTER if (tok && (tok = get_tok(tok + len, &len, end)) // TABLE - && (tok = get_tok(tok + len, &len, end))) // Table identifier + && (tok = get_tok(tok + len, &len, end))) // Table identifier { snprintf(dest, size, "%.*s", len, tok); remove_backticks(dest); From e5b530313792ecf37e1dfea0c33df12173445543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Fri, 5 Jan 2018 12:57:09 +0200 Subject: [PATCH 06/47] Initialize the query classifier in tests The test initialization function now loads the query classifier. --- server/core/test/test_utils.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/core/test/test_utils.h b/server/core/test/test_utils.h index 464679bae..5c00c724c 100644 --- a/server/core/test/test_utils.h +++ b/server/core/test/test_utils.h @@ -20,6 +20,7 @@ #include #include #include +#include #include "../maxscale/poll.h" #include "../maxscale/statistics.h" @@ -36,6 +37,8 @@ void init_test_env(char *path) ts_stats_init(); mxs_log_init(NULL, logdir, MXS_LOG_TARGET_DEFAULT); dcb_global_init(); + qc_setup(NULL, NULL); + qc_process_init(QC_INIT_BOTH); poll_init(); hkinit(); } From 3f78dbc9230a438b07b7aa4f7080a83d5bbcf498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Fri, 5 Jan 2018 13:34:46 +0200 Subject: [PATCH 07/47] Improve avrorouter assertion output When an assertion fails due to an overflow of the event buffer, all processed values for that event are dumped. This commit also enables the assertions even for non-debug builds which should speed up the elimination process for bugs in the avrorouter. The overhead of doing this is minimal as the output is already gathered for the INFO level logging. --- server/modules/routing/avrorouter/avro_rbr.c | 60 ++++++++++++++------ 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/server/modules/routing/avrorouter/avro_rbr.c b/server/modules/routing/avrorouter/avro_rbr.c index 3fcf1bf8f..be3907094 100644 --- a/server/modules/routing/avrorouter/avro_rbr.c +++ b/server/modules/routing/avrorouter/avro_rbr.c @@ -17,6 +17,7 @@ #include #include #include +#include #define WRITE_EVENT 0 #define UPDATE_EVENT 1 @@ -468,6 +469,19 @@ int get_metadata_len(uint8_t type) } } +// Make sure that both `i` and `trace` are defined before using this macro +#define check_overflow(t) do \ + { \ + if (!(t)) \ + { \ + for (long x = 0; x < i;x++) \ + { \ + MXS_ALERT("%s", trace[x]); \ + } \ + raise(SIGABRT); \ + } \ + }while(false) + /** * @brief Extract the values from a single row in a row event * @@ -484,7 +498,7 @@ uint8_t* process_row_event_data(TABLE_MAP *map, TABLE_CREATE *create, avro_value { int npresent = 0; avro_value_t field; - long ncolumns = map->columns; + long ncolumns = MXS_MIN(map->columns, create->columns); uint8_t *metadata = map->column_metadata; size_t metadata_offset = 0; @@ -497,7 +511,10 @@ uint8_t* process_row_event_data(TABLE_MAP *map, TABLE_CREATE *create, avro_value ptr += (ncolumns + 7) / 8; ss_dassert(ptr < end); - for (long i = 0; i < map->columns && i < create->columns && npresent < ncolumns; i++) + char trace[ncolumns][768]; + memset(trace, 0, sizeof(trace)); + + for (long i = 0; i < ncolumns && npresent < ncolumns; i++) { ss_debug(int rc = )avro_value_get_by_name(record, create->column_names[i], &field, NULL); ss_dassert(rc == 0); @@ -507,7 +524,7 @@ uint8_t* process_row_event_data(TABLE_MAP *map, TABLE_CREATE *create, avro_value npresent++; if (bit_is_set(null_bitmap, ncolumns, i)) { - MXS_INFO("[%ld] NULL", i); + sprintf(trace[i], "[%ld] NULL", i); if (column_is_blob(map->column_types[i])) { uint8_t nullvalue = 0; @@ -529,9 +546,9 @@ uint8_t* process_row_event_data(TABLE_MAP *map, TABLE_CREATE *create, avro_value char strval[bytes * 2 + 1]; gw_bin2hex(strval, val, bytes); avro_value_set_string(&field, strval); - MXS_INFO("[%ld] ENUM: %lu bytes", i, bytes); + sprintf(trace[i], "[%ld] ENUM: %lu bytes", i, bytes); ptr += bytes; - ss_dassert(ptr < end); + check_overflow(ptr < end); } else { @@ -561,13 +578,13 @@ uint8_t* process_row_event_data(TABLE_MAP *map, TABLE_CREATE *create, avro_value bytes = *ptr++; } - MXS_INFO("[%ld] CHAR: field: %d bytes, data: %d bytes", i, field_length, bytes); + sprintf(trace[i], "[%ld] CHAR: field: %d bytes, data: %d bytes", i, field_length, bytes); char str[bytes + 1]; memcpy(str, ptr, bytes); str[bytes] = '\0'; avro_value_set_string(&field, str); ptr += bytes; - ss_dassert(ptr < end); + check_overflow(ptr < end); } } else if (column_is_bit(map->column_types[i])) @@ -584,17 +601,17 @@ uint8_t* process_row_event_data(TABLE_MAP *map, TABLE_CREATE *create, avro_value MXS_WARNING("BIT is not currently supported, values are stored as 0."); } avro_value_set_int(&field, value); - MXS_INFO("[%ld] BIT", i); + sprintf(trace[i], "[%ld] BIT", i); ptr += bytes; - ss_dassert(ptr < end); + check_overflow(ptr < end); } else if (column_is_decimal(map->column_types[i])) { double f_value = 0.0; ptr += unpack_decimal_field(ptr, metadata + metadata_offset, &f_value); avro_value_set_double(&field, f_value); - MXS_INFO("[%ld] DOUBLE", i); - ss_dassert(ptr < end); + sprintf(trace[i], "[%ld] DECIMAL", i); + check_overflow(ptr < end); } else if (column_is_variable_string(map->column_types[i])) { @@ -611,13 +628,13 @@ uint8_t* process_row_event_data(TABLE_MAP *map, TABLE_CREATE *create, avro_value ptr++; } - MXS_INFO("[%ld] VARCHAR: field: %d bytes, data: %lu bytes", i, bytes, sz); + sprintf(trace[i], "[%ld] VARCHAR: field: %d bytes, data: %lu bytes", i, bytes, sz); char buf[sz + 1]; memcpy(buf, ptr, sz); buf[sz] = '\0'; ptr += sz; avro_value_set_string(&field, buf); - ss_dassert(ptr < end); + check_overflow(ptr < end); } else if (column_is_blob(map->column_types[i])) { @@ -625,7 +642,7 @@ uint8_t* process_row_event_data(TABLE_MAP *map, TABLE_CREATE *create, avro_value uint64_t len = 0; memcpy(&len, ptr, bytes); ptr += bytes; - MXS_INFO("[%ld] BLOB: field: %d bytes, data: %lu bytes", i, bytes, len); + sprintf(trace[i], "[%ld] BLOB: field: %d bytes, data: %lu bytes", i, bytes, len); if (len) { avro_value_set_bytes(&field, ptr, len); @@ -636,7 +653,7 @@ uint8_t* process_row_event_data(TABLE_MAP *map, TABLE_CREATE *create, avro_value uint8_t nullvalue = 0; avro_value_set_bytes(&field, &nullvalue, 1); } - ss_dassert(ptr < end); + check_overflow(ptr < end); } else if (column_is_temporal(map->column_types[i])) { @@ -647,8 +664,8 @@ uint8_t* process_row_event_data(TABLE_MAP *map, TABLE_CREATE *create, avro_value create->column_lengths[i], &tm); format_temporal_value(buf, sizeof(buf), map->column_types[i], &tm); avro_value_set_string(&field, buf); - MXS_INFO("[%ld] %s: %s", i, column_type_to_string(map->column_types[i]), buf); - ss_dassert(ptr < end); + sprintf(trace[i], "[%ld] %s: %s", i, column_type_to_string(map->column_types[i]), buf); + check_overflow(ptr < end); } /** All numeric types (INT, LONG, FLOAT etc.) */ else @@ -658,11 +675,18 @@ uint8_t* process_row_event_data(TABLE_MAP *map, TABLE_CREATE *create, avro_value ptr += unpack_numeric_field(ptr, map->column_types[i], &metadata[metadata_offset], lval); set_numeric_field_value(&field, map->column_types[i], &metadata[metadata_offset], lval); - ss_dassert(ptr < end); + sprintf(trace[i], "[%ld] %s", i, column_type_to_string(map->column_types[i])); + check_overflow(ptr < end); } ss_dassert(metadata_offset <= map->column_metadata_size); metadata_offset += get_metadata_len(map->column_types[i]); } + else + { + sprintf(trace[i], "[%ld] %s: Not present", i, column_type_to_string(map->column_types[i])); + } + + MXS_INFO("%s", trace[i]); } return ptr; From 579dca07502f4d09fea3cc71500187b66261366f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Fri, 5 Jan 2018 15:19:09 +0200 Subject: [PATCH 08/47] Log to stdout in unit tests The log manager will log to stdout to work around directory and file permissions. --- server/core/test/test_utils.h | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/server/core/test/test_utils.h b/server/core/test/test_utils.h index 5c00c724c..1de1f9059 100644 --- a/server/core/test/test_utils.h +++ b/server/core/test/test_utils.h @@ -22,20 +22,21 @@ #include #include +#include + #include "../maxscale/poll.h" #include "../maxscale/statistics.h" void init_test_env(char *path) { - int argc = 3; - - const char* logdir = path ? path : TEST_LOG_DIR; - config_get_global_options()->n_threads = 1; ts_stats_init(); - mxs_log_init(NULL, logdir, MXS_LOG_TARGET_DEFAULT); + if (!mxs_log_init(NULL, NULL, MXS_LOG_TARGET_STDOUT)) + { + exit(1); + } dcb_global_init(); qc_setup(NULL, NULL); qc_process_init(QC_INIT_BOTH); From 0416d66bcbe70b4f1ae5258f06067a95dc44725f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Fri, 5 Jan 2018 20:27:15 +0200 Subject: [PATCH 09/47] Set query classifier with an absolute path in tests Setting the query classifier with an absolute path makes it easier to manage test setup without having to manually resolve the relative path to the query classifier from the test source directory. --- server/core/test/test_utils.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/core/test/test_utils.h b/server/core/test/test_utils.h index 1de1f9059..01fca0667 100644 --- a/server/core/test/test_utils.h +++ b/server/core/test/test_utils.h @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include @@ -38,6 +40,7 @@ void init_test_env(char *path) exit(1); } dcb_global_init(); + set_libdir(MXS_STRDUP(TEST_DIR "/query_classifier/qc_sqlite/")); qc_setup(NULL, NULL); qc_process_init(QC_INIT_BOTH); poll_init(); From a8876fbcb8fa2ba3d29030eb9c4d936e2ab9308f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Mon, 8 Jan 2018 12:50:45 +0200 Subject: [PATCH 10/47] Add CONTRIBUTING document and pull request template These templates allow for easier pull requests. --- CONTRIBUTING.md | 27 +++++++++++++++++++++++++++ PULL_REQUEST_TEMPLATE.md | 3 +++ 2 files changed, 30 insertions(+) create mode 100644 CONTRIBUTING.md create mode 100644 PULL_REQUEST_TEMPLATE.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..69af32211 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,27 @@ +# Contributing to Maxscale + +## Prerequisites + +Basically, in order for us to be able to accept a contribution, it must be +licensed under [BSD-new](http://en.wikipedia.org/wiki/BSD_licenses). Upon +request, we can also provide a _contributor agreement_ for you to sign. + +When you submit a pull request, add the following comment to your pull request. + +> I am contributing the new code of the whole pull request, including one or + several files that are either new files or modified ones, under the BSD-new + license. + +Without this comment, the pull request will not be accepted. + +## Practicalities + +* Please ensure that your pull-request has been made against the correct + branch. For bug fixes or minor improvements, use the default branch and for + new code, use the `develop` branch (e.g. for a patch to 2.1.x, use the `2.1` + branch). + +* Please ensure that your code follows our [Coding Style](https://github.com/mariadb-corporation/MaxScale/wiki/Coding-Style-and-Guidelines). + All new code should be formatted with the + [Astyle configuration](https://github.com/mariadb-corporation/MaxScale/wiki/Coding-Style-and-Guidelines#tldr) + provided with the MaxScale source code. diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..ce348d0f0 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,3 @@ +I am contributing the new code of the whole pull request, including one or +several files that are either new files or modified ones, under the BSD-new +license. From 398dbbc66dc0d537aeaa4498a5059d0dea21adfc Mon Sep 17 00:00:00 2001 From: Johan Wikman Date: Mon, 8 Jan 2018 14:28:13 +0200 Subject: [PATCH 11/47] Update 2.1 version number --- VERSION21.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION21.cmake b/VERSION21.cmake index bba5e1b04..4b2078c0d 100644 --- a/VERSION21.cmake +++ b/VERSION21.cmake @@ -5,7 +5,7 @@ set(MAXSCALE_VERSION_MAJOR "2" CACHE STRING "Major version") set(MAXSCALE_VERSION_MINOR "1" CACHE STRING "Minor version") -set(MAXSCALE_VERSION_PATCH "13" CACHE STRING "Patch version") +set(MAXSCALE_VERSION_PATCH "14" CACHE STRING "Patch version") # This should only be incremented if a package is rebuilt set(MAXSCALE_BUILD_NUMBER 1 CACHE STRING "Release number") From ad634fe31e0a3f8d5bbd62dc2e1dd29b364ba7ab Mon Sep 17 00:00:00 2001 From: Johan Wikman Date: Fri, 12 Jan 2018 15:56:52 +0200 Subject: [PATCH 12/47] MXS-1596 Stress test for failover - Start 4 threads where each thread sits in a loop and performs 20% updates and 80% selects. Each thread has a table of its own. - The main thread executes the following in a loop. - Take down the current master and wait a while (failover assumed to happen). - Put up the old master node and wait a while. Keep on doing that for 1.5 minutes. At the end check that: - There is one 'Master'. - The other nodes are either - 'Slave' or - 'Running' in which case it is checked it is because the node could not be rejoined. --- maxscale-system-test/.gitignore | 1 + maxscale-system-test/CMakeLists.txt | 2 + ...cale.cnf.template.mysqlmon_failover_stress | 94 +++ .../mysqlmon_failover_stress.cpp | 576 ++++++++++++++++++ 4 files changed, 673 insertions(+) create mode 100644 maxscale-system-test/cnf/maxscale.cnf.template.mysqlmon_failover_stress create mode 100755 maxscale-system-test/mysqlmon_failover_stress.cpp diff --git a/maxscale-system-test/.gitignore b/maxscale-system-test/.gitignore index d85dc5dd8..b54406bcd 100644 --- a/maxscale-system-test/.gitignore +++ b/maxscale-system-test/.gitignore @@ -122,6 +122,7 @@ mysqlmon_failover_manual2_2 mysqlmon_failover_rejoin_old_slave mysqlmon_failover_rolling_master mysqlmon_failover_rolling_restart_slaves +mysqlmon_failover_stress mysqlmon_switchover_bad_master mysqlmon_switchover mxs1045 diff --git a/maxscale-system-test/CMakeLists.txt b/maxscale-system-test/CMakeLists.txt index 0d6dcd31f..2f992d9b4 100644 --- a/maxscale-system-test/CMakeLists.txt +++ b/maxscale-system-test/CMakeLists.txt @@ -290,6 +290,8 @@ add_test_executable(mysqlmon_failover_rejoin_old_slave.cpp mysqlmon_failover_rej # MySQL Monitor rolling restart slaves add_test_executable(mysqlmon_failover_rolling_restart_slaves.cpp mysqlmon_failover_rolling_restart_slaves mysqlmon_failover_rolling_restart_slaves LABELS mysqlmon REPL_BACKEND) +add_test_executable(mysqlmon_failover_stress.cpp mysqlmon_failover_stress mysqlmon_failover_stress LABELS mysqlmon REPL_BACKEND) + # Test monitor state change events when manually clearing server bits add_test_executable(false_monitor_state_change.cpp false_monitor_state_change replication LABELS mysqlmon REPL_BACKEND) diff --git a/maxscale-system-test/cnf/maxscale.cnf.template.mysqlmon_failover_stress b/maxscale-system-test/cnf/maxscale.cnf.template.mysqlmon_failover_stress new file mode 100644 index 000000000..1de5c5fb8 --- /dev/null +++ b/maxscale-system-test/cnf/maxscale.cnf.template.mysqlmon_failover_stress @@ -0,0 +1,94 @@ +[maxscale] +threads=###threads### + +[MySQL-Monitor] +type=monitor +module=mysqlmon +servers= server1, server2, server3, server4 +user=maxskysql +passwd= skysql +monitor_interval=1000 +allow_cluster_recovery=true +detect_standalone_master=true +auto_failover=true +auto_rejoin=true +replication_user=repl +replication_password=repl +backend_connect_timeout=5 +backend_read_timeout=5 +backend_write_timeout=5 + +[RW-Split-Router] +type=service +router= readwritesplit +servers=server1, server2, server3, server4 +user=maxskysql +passwd=skysql + +[Read-Connection-Router-Slave] +type=service +router=readconnroute +router_options= slave +servers=server1, server2, server3, server4 +user=maxskysql +passwd=skysql + +[Read-Connection-Router-Master] +type=service +router=readconnroute +router_options=master +servers=server1, server2, server3, server4 +user=maxskysql +passwd=skysql + +[RW-Split-Listener] +type=listener +service=RW-Split-Router +protocol=MySQLClient +port=4006 + +[Read-Connection-Listener-Slave] +type=listener +service=Read-Connection-Router-Slave +protocol=MySQLClient +port=4009 + +[Read-Connection-Listener-Master] +type=listener +service=Read-Connection-Router-Master +protocol=MySQLClient +port=4008 + +[CLI] +type=service +router=cli + +[CLI Listener] +type=listener +service=CLI +protocol=maxscaled +socket=default + +[server1] +type=server +address=###node_server_IP_1### +port=###node_server_port_1### +protocol=MySQLBackend + +[server2] +type=server +address=###node_server_IP_2### +port=###node_server_port_2### +protocol=MySQLBackend + +[server3] +type=server +address=###node_server_IP_3### +port=###node_server_port_3### +protocol=MySQLBackend + +[server4] +type=server +address=###node_server_IP_4### +port=###node_server_port_4### +protocol=MySQLBackend diff --git a/maxscale-system-test/mysqlmon_failover_stress.cpp b/maxscale-system-test/mysqlmon_failover_stress.cpp new file mode 100755 index 000000000..e6452583b --- /dev/null +++ b/maxscale-system-test/mysqlmon_failover_stress.cpp @@ -0,0 +1,576 @@ +/* + * 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: 2020-01-01 + * + * 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. + */ + +#include +#include +#include +#include +#include +#include +#include "testconnections.h" +#include "fail_switch_rejoin_common.cpp" + +using namespace std; + +// How often the monitor checks the server state. +// NOTE: Ensure this is identical with the value in the configuration file. +const time_t MONITOR_INTERVAL = 1; + +// After how many seconds should the failover/rejoin operation surely have +// been performed. Not very critical. +const time_t FAILOVER_DURATION = 5; + +// How long should we keep in running. +const time_t TEST_DURATION = 90; + +#define CMESSAGE(msg) \ + do {\ + stringstream ss;\ + ss << "client(" << m_id << ") : " << msg << "\n";\ + cout << ss.str() << flush;\ + } while (false) + +#if !defined(NDEBUG) + +#define ss_dassert(x) do { if (!(x)) { fprintf(stderr, "Assertion failed: %s", #x); abort(); } } while(false) +#define ss_debug(x) x + +#else + +#define ss_dassert(s) +#define ss_debug(x) + +#endif + + +namespace +{ + +class Client +{ +public: + enum + { + DEFAULT_N_CLIENTS = 4, + DEFAULT_N_ROWS = 100 + }; + + static void init(TestConnections& test, size_t nClients, size_t nRows) + { + s_nClients = nClients; + s_nRows = nRows; + + if (create_tables(test)) + { + if (insert_data(test)) + { + cout << "\nSyncing slaves." << endl; + test.repl->sync_slaves(); + } + } + } + + static void start(bool verbose, + const char* zHost, int port, const char* zUser, const char* zPassword) + { + for (size_t i = 0; i < s_nClients; ++i) + { + s_threads.push_back(std::thread(&Client::thread_main, + i, verbose, zHost, port, zUser, zPassword)); + } + } + + static void stop() + { + s_shutdown = true; + + for (size_t i = 0; i < s_nClients; ++i) + { + s_threads[i].join(); + } + } + +private: + Client(int id, bool verbose) + : m_id(id) + , m_verbose(verbose) + , m_value(1) + { + ss_debug(int rv); + + unsigned int seed = (time(NULL) << m_id); + ss_debug(rv =) initstate_r(seed, m_initstate, sizeof(m_initstate), &m_random_data); + ss_dassert(rv == 0); + + ss_debug(rv=) srandom_r(seed, &m_random_data); + ss_dassert(rv == 0); + } + + enum action_t + { + ACTION_SELECT, + ACTION_UPDATE + }; + + action_t action() const + { + double d = random_decimal_fraction(); + + // 20% updates + // 80% selects + if (d <= 0.2) + { + return ACTION_UPDATE; + } + else + { + return ACTION_SELECT; + } + } + + bool run(MYSQL* pConn) + { + bool rv = false; + + switch (action()) + { + case ACTION_SELECT: + rv = run_select(pConn); + break; + + case ACTION_UPDATE: + rv = run_update(pConn); + break; + + default: + ss_dassert(!true); + } + + return rv; + } + + bool run_select(MYSQL* pConn) + { + bool rv = true; + + string stmt("SELECT * FROM test.t"); + stmt += std::to_string(m_id); + stmt += " WHERE id="; + stmt += std::to_string(get_random_id()); + + if (mysql_query(pConn, stmt.c_str()) == 0) + { + flush_response(pConn); + } + else + { + if (m_verbose) + { + CMESSAGE("\"" << stmt << "\" failed: " << mysql_error(pConn)); + } + rv = false; + } + + return rv; + } + + bool run_update(MYSQL* pConn) + { + bool rv = true; + + string stmt("UPDATE test.t"); + stmt += std::to_string(m_id); + stmt += " SET id="; + stmt += std::to_string(m_value); + stmt += " WHERE id="; + stmt += std::to_string(get_random_id()); + m_value = (m_value + 1) % s_nRows; + + if (mysql_query(pConn, stmt.c_str()) == 0) + { + flush_response(pConn); + } + else + { + if (m_verbose) + { + CMESSAGE("\"" << stmt << "\" failed: " << mysql_error(pConn)); + } + rv = false; + } + + return rv; + } + + static void flush_response(MYSQL* pConn) + { + do + { + MYSQL_RES* pRes = mysql_store_result(pConn); + mysql_free_result(pRes); + } + while (mysql_next_result(pConn) == 0); + } + + int get_random_id() const + { + int id = s_nRows * random_decimal_fraction(); + + ss_dassert(id >= 0); + ss_dassert(id <= s_nRows); + + return id; + } + + double random_decimal_fraction() const + { + int32_t r; + ss_debug(int rv=) random_r(&m_random_data, &r); + ss_dassert(rv == 0); + + return double(r) / RAND_MAX; + } + + void run(const char* zHost, int port, const char* zUser, const char* zPassword) + { + do + { + MYSQL* pMysql = mysql_init(NULL); + + if (pMysql) + { + unsigned int timeout = 5; + mysql_options(pMysql, MYSQL_OPT_CONNECT_TIMEOUT, &timeout); + mysql_options(pMysql, MYSQL_OPT_READ_TIMEOUT, &timeout); + mysql_options(pMysql, MYSQL_OPT_WRITE_TIMEOUT, &timeout); + + if (m_verbose) + { + CMESSAGE("Connecting"); + } + + if (mysql_real_connect(pMysql, zHost, zUser, zPassword, "test", port, NULL, 0)) + { + if (m_verbose) + { + CMESSAGE("Connected."); + } + + while (!s_shutdown && run(pMysql)) + { + ; + } + } + else + { + if (m_verbose) + { + CMESSAGE("mysql_real_connect() failed: " << mysql_error(pMysql)); + } + } + + if (m_verbose) + { + CMESSAGE("Closing"); + } + mysql_close(pMysql); + } + else + { + CMESSAGE("mysql_init() failed."); + } + + // To prevent some backend from becoming overwhelmed. + sleep(1); + } + while (!s_shutdown); + } + + static void thread_main(int i, bool verbose, + const char* zHost, int port, const char* zUser, const char* zPassword) + { + if (mysql_thread_init() == 0) + { + Client client(i, verbose); + + client.run(zHost, port, zUser, zPassword); + + mysql_thread_end(); + } + else + { + int m_id = i; + CMESSAGE("mysql_thread_init() failed."); + } + } + + static bool create_tables(TestConnections& test) + { + cout << "\nCreating tables." << endl; + + MYSQL* pConn = test.maxscales->conn_rwsplit[0]; + + string drop_head("DROP TABLE IF EXISTS test.t"); + string create_head("CREATE TABLE test.t"); + string create_tail(" (id INT)"); + + for (size_t i = 0; i < s_nClients; ++i) + { + string drop = drop_head + std::to_string(i); + test.try_query(pConn, drop.c_str()); + + string create = create_head + std::to_string(i) + create_tail; + test.try_query(pConn, create.c_str()); + } + + return test.ok(); + } + + static bool insert_data(TestConnections& test) + { + cout << "\nInserting data." << endl; + + MYSQL* pConn = test.maxscales->conn_rwsplit[0]; + + for (size_t i = 0; i < s_nClients; ++i) + { + string insert("insert into test.t"); + insert += std::to_string(i); + insert += " values "; + + for (size_t j = 0; j < s_nRows; ++j) + { + insert += "("; + insert += std::to_string(j); + insert += ")"; + + if (j < s_nRows - 1) + { + insert += ", "; + } + } + + test.try_query(pConn, insert.c_str()); + } + + return test.ok(); + } + +private: + enum + { + INITSTATE_SIZE = 32 + }; + + size_t m_id; + bool m_verbose; + size_t m_value; + char m_initstate[INITSTATE_SIZE]; + mutable struct random_data m_random_data; + + static size_t s_nClients; + static size_t s_nRows; + static bool s_shutdown; + + static std::vector s_threads; +}; + +size_t Client::s_nClients; +size_t Client::s_nRows; +bool Client::s_shutdown; +std::vector Client::s_threads; + +} + +namespace +{ + +void list_servers(TestConnections& test) +{ + test.maxscales->execute_maxadmin_command_print(0, (char*)"list servers"); +} + +void sleep(int s) +{ + cout << "Sleeping " << s << " times 1 second" << flush; + do + { + ::sleep(1); + cout << "." << flush; + --s; + } + while (s > 0); + + cout << endl; +} + +bool check_server_status(TestConnections& test, int id) +{ + bool is_master = false; + + Mariadb_nodes* pRepl = test.repl; + + string server = string("server") + std::to_string(id); + + StringSet statuses = test.get_server_status(server.c_str()); + std::ostream_iterator oi(cout, " "); + + cout << server << ": "; + std::copy(statuses.begin(), statuses.end(), oi); + + cout << " => "; + + if (statuses.count("Master")) + { + is_master = true; + cout << "OK"; + } + else if (statuses.count("Slave")) + { + cout << "OK"; + } + else if (statuses.count("Running")) + { + MYSQL* pConn = pRepl->nodes[id - 1]; + + char result[1024]; + if (find_field(pConn, "SHOW SLAVE STATUS", "Last_IO_Error", result) == 0) + { + const char needle[] = + ", which is not in the master's binlog. " + "Since the master's binlog contains GTIDs with higher sequence numbers, " + "it probably means that the slave has diverged due to executing extra " + "erroneous transactions"; + + if (strstr(result, needle)) + { + // A rejoin was attempted, but it failed because the node (old master) + // had events that were not present in the new master. That is, a rejoin + // is not possible in principle without corrective action. + cout << "OK (could not be joined due to GTID issue)"; + } + else + { + cout << result; + test.assert(false, "Merely 'Running' node did not error in expected way."); + } + } + else + { + test.assert(false, "Could not execute \"SHOW SLAVE STATUS\""); + } + } + else + { + test.assert(false, "Unexpected server state for %s.", server.c_str()); + } + + cout << endl; + + return is_master; +} + +void check_server_statuses(TestConnections& test) +{ + int masters = 0; + + masters += check_server_status(test, 1); + masters += check_server_status(test, 2); + masters += check_server_status(test, 3); + masters += check_server_status(test, 4); + + test.assert(masters == 1, "Unpexpected number of masters: %d", masters); +} + +void run(TestConnections& test) +{ + int n_threads = Client::DEFAULT_N_CLIENTS; + + cout << "\nConnecting to MaxScale." << endl; + test.maxscales->connect_maxscale(); + + Client::init(test, Client::DEFAULT_N_CLIENTS, Client::DEFAULT_N_ROWS); + + if (test.ok()) + { + const char* zHost = test.maxscales->IP[0]; + int port = test.maxscales->rwsplit_port[0]; + const char* zUser = test.maxscales->user_name; + const char* zPassword = test.maxscales->password; + + cout << "Connecting to " << zHost << ":" << port << " as " << zUser << ":" << zPassword << endl; + cout << "Starting clients." << endl; + Client::start(test.verbose, zHost, port, zUser, zPassword); + + time_t start = time(NULL); + + list_servers(test); + + while (time(NULL) - start < TEST_DURATION) + { + sleep(FAILOVER_DURATION); + + int master_id = get_master_server_id(test); + + if (master_id > 0 && master_id <= 4) + { + cout << "\nStopping node: " << master_id << endl; + test.repl->stop_node(master_id - 1); + + sleep(2 * MONITOR_INTERVAL); + list_servers(test); + + sleep(FAILOVER_DURATION); + list_servers(test); + + sleep(FAILOVER_DURATION); + cout << "\nStarting node: " << master_id << endl; + test.repl->start_node(master_id - 1); + + sleep(2 * MONITOR_INTERVAL); + list_servers(test); + + sleep(FAILOVER_DURATION); + list_servers(test); + } + else + { + test.assert(false, "Unexpected master id: %d"); + } + } + + sleep(FAILOVER_DURATION); + + cout << "\nStopping clients.\n" << flush; + Client::stop(); + + test.repl->close_connections(); + test.repl->connect(); + + check_server_statuses(test); + } +} + +} + +int main(int argc, char* argv[]) +{ + std::ios::sync_with_stdio(true); + + Mariadb_nodes::require_gtid(true); + TestConnections test(argc, argv); + + run(test); + + return test.global_result; +} From 14f1bbed517733c35d8c1a84d6d31c8c1537c42a Mon Sep 17 00:00:00 2001 From: Esa Korhonen Date: Fri, 12 Jan 2018 14:44:38 +0200 Subject: [PATCH 13/47] NamedServerFilter: Deprecate message for legacy parameter use --- server/modules/filter/namedserverfilter/namedserverfilter.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/server/modules/filter/namedserverfilter/namedserverfilter.cc b/server/modules/filter/namedserverfilter/namedserverfilter.cc index 9328b821b..58ad32731 100644 --- a/server/modules/filter/namedserverfilter/namedserverfilter.cc +++ b/server/modules/filter/namedserverfilter/namedserverfilter.cc @@ -280,6 +280,7 @@ RegexHintFilter::create(const char* name, char** options, MXS_CONFIG_PARAMETER* } else if (legacy_mode && !mapping.size()) { + MXS_WARNING("Use of legacy parameters 'match' and 'server' is deprecated."); /* Using legacy mode and no indexed parameters found. Add the legacy parameters * to the mapping. */ if (!regex_compile_and_add(pcre_ops, true, match_val_legacy, server_val_legacy, From f6f34ad7e50ead1c2144283cf025ded0aa1773d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Thu, 11 Jan 2018 17:19:52 +0200 Subject: [PATCH 14/47] Fix debug build failure on CentOS 6 The build failed due to a comparison between signed and unsigned integers. --- server/modules/authenticator/PAM/PAMAuth/pam_instance.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/modules/authenticator/PAM/PAMAuth/pam_instance.cc b/server/modules/authenticator/PAM/PAMAuth/pam_instance.cc index cacadfa0a..855d47a73 100644 --- a/server/modules/authenticator/PAM/PAMAuth/pam_instance.cc +++ b/server/modules/authenticator/PAM/PAMAuth/pam_instance.cc @@ -180,7 +180,7 @@ int PamInstance::load_users(SERVICE* service) "ON (u.user = t.user AND u.host = t.host) WHERE u.plugin = 'pam' " "ORDER BY user"; #if defined(SS_DEBUG) - const int PAM_USERS_QUERY_NUM_FIELDS = 5; + const unsigned int PAM_USERS_QUERY_NUM_FIELDS = 5; #endif char *user, *pw; From f167bc5f02e75288b5aadf8e48aca63aa7b20030 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Thu, 11 Jan 2018 21:35:59 +0200 Subject: [PATCH 15/47] Update example configuration Updated links and module names in the configuration. Also changed object names so that they don't produce warnings. Removed redundant parameters and tuned default monitor_interval to a slightly more reasonable 2000 milliseconds. Enabled automatic thread number configuration. --- server/maxscale.cnf.template | 51 ++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/server/maxscale.cnf.template b/server/maxscale.cnf.template index 30688089b..09af2fc2e 100644 --- a/server/maxscale.cnf.template +++ b/server/maxscale.cnf.template @@ -1,39 +1,39 @@ -# MaxScale documentation on GitHub: -# https://github.com/mariadb-corporation/MaxScale/blob/2.1/Documentation/Documentation-Contents.md +# MaxScale documentation: +# https://mariadb.com/kb/en/mariadb-enterprise/mariadb-maxscale-22/ # Global parameters # # Complete list of configuration options: -# https://github.com/mariadb-corporation/MaxScale/blob/2.1/Documentation/Getting-Started/Configuration-Guide.md +# https://mariadb.com/kb/en/mariadb-enterprise/mariadb-maxscale-22-mariadb-maxscale-configuration-usage-scenarios/ [maxscale] -threads=1 +threads=auto # Server definitions # # Set the address of the server to the network -# address of a MySQL server. +# address of a MariaDB server. # [server1] type=server address=127.0.0.1 port=3306 -protocol=MySQLBackend +protocol=MariaDBBackend # Monitor for the servers # # This will keep MaxScale aware of the state of the servers. -# MySQL Monitor documentation: -# https://github.com/mariadb-corporation/MaxScale/blob/2.1/Documentation/Monitors/MySQL-Monitor.md +# MariaDB Monitor documentation: +# https://mariadb.com/kb/en/mariadb-enterprise/mariadb-maxscale-22-mariadb-monitor/ -[MySQL Monitor] +[MariaDB-Monitor] type=monitor -module=mysqlmon +module=mariadbmon servers=server1 user=myuser passwd=mypwd -monitor_interval=10000 +monitor_interval=2000 # Service definitions # @@ -42,9 +42,9 @@ monitor_interval=10000 # # ReadConnRoute documentation: -# https://github.com/mariadb-corporation/MaxScale/blob/2.1/Documentation/Routers/ReadConnRoute.md +# https://mariadb.com/kb/en/mariadb-enterprise/mariadb-maxscale-22-readconnroute/ -[Read-Only Service] +[Read-Only-Service] type=service router=readconnroute servers=server1 @@ -53,21 +53,20 @@ passwd=mypwd router_options=slave # ReadWriteSplit documentation: -# https://github.com/mariadb-corporation/MaxScale/blob/2.1/Documentation/Routers/ReadWriteSplit.md +# https://mariadb.com/kb/en/mariadb-enterprise/mariadb-maxscale-22-readwritesplit/ -[Read-Write Service] +[Read-Write-Service] type=service router=readwritesplit servers=server1 user=myuser passwd=mypwd -max_slave_connections=100% # This service enables the use of the MaxAdmin interface # MaxScale administration guide: -# https://github.com/mariadb-corporation/MaxScale/blob/2.1/Documentation/Reference/MaxAdmin.md +# https://mariadb.com/kb/en/mariadb-enterprise/mariadb-maxscale-22-maxadmin-admin-interface/ -[MaxAdmin Service] +[MaxAdmin-Service] type=service router=cli @@ -77,20 +76,20 @@ router=cli # services will listen on. # -[Read-Only Listener] +[Read-Only-Listener] type=listener -service=Read-Only Service -protocol=MySQLClient +service=Read-Only-Service +protocol=MariaDBClient port=4008 -[Read-Write Listener] +[Read-Write-Listener] type=listener -service=Read-Write Service -protocol=MySQLClient +service=Read-Write-Service +protocol=MariaDBClient port=4006 -[MaxAdmin Listener] +[MaxAdmin-Listener] type=listener -service=MaxAdmin Service +service=MaxAdmin-Service protocol=maxscaled socket=default From a44e352f4f253c3822d179a48379d339f56eead3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Sun, 14 Jan 2018 21:35:30 +0200 Subject: [PATCH 16/47] Fix default protocol module names for objects If either a server or a listener was created at runtime, it would use the old protocol module name and log a warning. --- server/core/config_runtime.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/core/config_runtime.cc b/server/core/config_runtime.cc index 71135f33e..da9dbbe90 100644 --- a/server/core/config_runtime.cc +++ b/server/core/config_runtime.cc @@ -152,7 +152,7 @@ bool runtime_create_server(const char *name, const char *address, const char *po } if (protocol == NULL) { - protocol = "MySQLBackend"; + protocol = "mariadbbackend"; } if (authenticator == NULL && (authenticator = get_default_authenticator(protocol)) == NULL) { @@ -808,7 +808,7 @@ bool runtime_create_listener(SERVICE *service, const char *name, const char *add } if (proto == NULL || strcasecmp(proto, CN_DEFAULT) == 0) { - proto = "MySQLClient"; + proto = "mariadbclient"; } if (auth && strcasecmp(auth, CN_DEFAULT) == 0) From f2771d5ad89f864c826661f6cb63088dd9a28bca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Thu, 11 Jan 2018 07:47:25 +0200 Subject: [PATCH 17/47] Remove obsolete message The warning that a schema already exists is obsolete as mapped tables are now always opened instead of being reused. This causes the schema checks to be done for each mapped table. --- server/modules/routing/avrorouter/avro_schema.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/server/modules/routing/avrorouter/avro_schema.c b/server/modules/routing/avrorouter/avro_schema.c index fc61301d3..79ef43bd2 100644 --- a/server/modules/routing/avrorouter/avro_schema.c +++ b/server/modules/routing/avrorouter/avro_schema.c @@ -311,10 +311,6 @@ void save_avro_schema(const char *path, const char* schema, TABLE_MAP *map) } } } - else - { - MXS_NOTICE("Schema version %d already exists: %s", map->version, filepath); - } } /** From 5e4b7ae7c760b32db70aa85d7fc49c0ac7afbfe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Thu, 11 Jan 2018 08:15:01 +0200 Subject: [PATCH 18/47] Fix memory leak in avrorouter The TABLE_MAP freeing function leaked memory. --- server/modules/routing/avrorouter/avro_schema.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/modules/routing/avrorouter/avro_schema.c b/server/modules/routing/avrorouter/avro_schema.c index 79ef43bd2..ef1e41c27 100644 --- a/server/modules/routing/avrorouter/avro_schema.c +++ b/server/modules/routing/avrorouter/avro_schema.c @@ -1446,6 +1446,8 @@ void table_map_free(TABLE_MAP *map) if (map) { MXS_FREE(map->column_types); + MXS_FREE(map->column_metadata); + MXS_FREE(map->null_bitmap); MXS_FREE(map->database); MXS_FREE(map->table); MXS_FREE(map); From dbad6b737d693b5cb1e795c6fabfa219d209fe78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Thu, 11 Jan 2018 08:15:58 +0200 Subject: [PATCH 19/47] Detect redundant table map events When two identical tables are mapped into the same slot, the newer one is ignored. --- server/modules/routing/avrorouter/avro_rbr.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/modules/routing/avrorouter/avro_rbr.c b/server/modules/routing/avrorouter/avro_rbr.c index be3907094..8af455076 100644 --- a/server/modules/routing/avrorouter/avro_rbr.c +++ b/server/modules/routing/avrorouter/avro_rbr.c @@ -91,6 +91,15 @@ bool handle_table_map_event(AVRO_INSTANCE *router, REP_HEADER *hdr, uint8_t *ptr TABLE_MAP *old = hashtable_fetch(router->table_maps, table_ident); TABLE_MAP *map = table_map_alloc(ptr, ev_len, create); MXS_ABORT_IF_NULL(map); // Fatal error at this point + + if (old && old->id == map->id && old->version == map->version && + strcmp(old->table, map->table) == 0 && + strcmp(old->database, map->database) == 0) + { + table_map_free(map); + return true; + } + char* json_schema = json_new_schema_from_table(map); if (json_schema) From 9ca6d586d1521c42b19a919b650a1d9d933dbece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Thu, 11 Jan 2018 11:43:27 +0200 Subject: [PATCH 20/47] MXS-1575: Fix Avro schema versioning The versions were not updated if the table was dropped and created again. --- server/modules/routing/avrorouter/avro_schema.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/server/modules/routing/avrorouter/avro_schema.c b/server/modules/routing/avrorouter/avro_schema.c index ef1e41c27..24fe0adf8 100644 --- a/server/modules/routing/avrorouter/avro_schema.c +++ b/server/modules/routing/avrorouter/avro_schema.c @@ -703,6 +703,21 @@ TABLE_CREATE* table_create_from_schema(const char* file, const char* db, return newtable; } +int resolve_table_version(const char* db, const char* table) +{ + int version = 0; + char buf[PATH_MAX + 1]; + + do + { + version++; + snprintf(buf, sizeof(buf), "%s.%s.%06d.avsc", db, table, version); + } + while (access(buf, F_OK) == 0); + + return version; +} + /** * @brief Handle a query event which contains a CREATE TABLE statement * @param sql Query SQL @@ -758,7 +773,7 @@ TABLE_CREATE* table_create_alloc(const char* sql, int len, const char* db) { if ((rval = MXS_MALLOC(sizeof(TABLE_CREATE)))) { - rval->version = 1; + rval->version = resolve_table_version(db, table); rval->was_used = false; rval->column_names = names; rval->column_lengths = lengths; From ab44a941ab0c04f4d1153652c1363700e7327bbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Thu, 11 Jan 2018 11:44:33 +0200 Subject: [PATCH 21/47] MXS-1575: Fix large DECIMAL value handling DECIMAL types that were larger than 8 bytes were not handled correctly. The current implementation only prints the lowest 8 bytes of the integer part of the decimal. --- server/core/mysql_binlog.c | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/server/core/mysql_binlog.c b/server/core/mysql_binlog.c index ae9b6b844..33f6dd395 100644 --- a/server/core/mysql_binlog.c +++ b/server/core/mysql_binlog.c @@ -259,7 +259,7 @@ size_t datetime_sizes[] = */ static void unpack_datetime(uint8_t *ptr, int length, struct tm *dest) { - int64_t val = 0; + uint64_t val = 0; uint32_t second, minute, hour, day, month, year; if (length == -1) @@ -717,6 +717,7 @@ size_t unpack_decimal_field(uint8_t *ptr, uint8_t *metadata, double *val_float) int fpart2 = decimals - fpart1 * dec_dig; int ibytes = ipart1 * 4 + dig_bytes[ipart2]; int fbytes = fpart1 * 4 + dig_bytes[fpart2]; + int field_size = ibytes + fbytes; /** Remove the sign bit and store it locally */ bool negative = (ptr[0] & 0x80) == 0; @@ -735,7 +736,17 @@ size_t unpack_decimal_field(uint8_t *ptr, uint8_t *metadata, double *val_float) } } - int64_t val_i = unpack_bytes(ptr, ibytes); + int64_t val_i = 0; + + if (ibytes > 8) + { + int extra = ibytes - 8; + ptr += extra; + ibytes -= extra; + ss_dassert(ibytes == 8); + } + + val_i = unpack_bytes(ptr, ibytes); int64_t val_f = fbytes ? unpack_bytes(ptr + ibytes, fbytes) : 0; if (negative) @@ -746,5 +757,5 @@ size_t unpack_decimal_field(uint8_t *ptr, uint8_t *metadata, double *val_float) *val_float = (double)val_i + ((double)val_f / (pow(10.0, decimals))); - return ibytes + fbytes; + return field_size; } From 847dd2c1204d4e0a779d802755f8ad8587eb87fb Mon Sep 17 00:00:00 2001 From: Timofey Turenko Date: Mon, 15 Jan 2018 16:47:15 +0200 Subject: [PATCH 22/47] fix typo in read_env() and set_env.sh --- maxscale-system-test/mariadb_nodes.cpp | 2 +- maxscale-system-test/mdbci/set_env.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/maxscale-system-test/mariadb_nodes.cpp b/maxscale-system-test/mariadb_nodes.cpp index 8e43d151b..1a51292a8 100644 --- a/maxscale-system-test/mariadb_nodes.cpp +++ b/maxscale-system-test/mariadb_nodes.cpp @@ -227,7 +227,7 @@ int Mariadb_nodes::read_env() } else { - sprintf(start_db_command[i], "%s", "service mysql stop"); + sprintf(stop_db_command[i], "%s", "service mysql stop"); } //reading cleanup_db_command diff --git a/maxscale-system-test/mdbci/set_env.sh b/maxscale-system-test/mdbci/set_env.sh index da1608a19..86459e8d1 100644 --- a/maxscale-system-test/mdbci/set_env.sh +++ b/maxscale-system-test/mdbci/set_env.sh @@ -70,7 +70,7 @@ do else ${mdbci_dir}/mdbci ssh --command 'echo \"/usr/sbin/mysqld \$* 2> stderr.log > stdout.log &\" > mysql_start.sh; echo \"sleep 20\" >> mysql_start.sh; echo \"disown\" >> mysql_start.sh; chmod a+x mysql_start.sh' $config_name/node_$num --silent eval 'export $start_cmd_var="/home/$au/mysql_start.sh "' - eval 'export $start_cmd_var="killall mysqld "' + eval 'export $stop_cmd_var="killall mysqld "' fi else eval 'export $start_cmd_var="$mysql_exe start "' From 8dfc1a141d795cfa52d8f63f76cc1eebe1de2a9d Mon Sep 17 00:00:00 2001 From: Timofey Turenko Date: Mon, 15 Jan 2018 16:47:15 +0200 Subject: [PATCH 23/47] fix typo in read_env() and set_env.sh --- maxscale-system-test/mariadb_nodes.cpp | 2 +- maxscale-system-test/mdbci/set_env.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/maxscale-system-test/mariadb_nodes.cpp b/maxscale-system-test/mariadb_nodes.cpp index 4a063ac16..8d417376f 100644 --- a/maxscale-system-test/mariadb_nodes.cpp +++ b/maxscale-system-test/mariadb_nodes.cpp @@ -188,7 +188,7 @@ int Mariadb_nodes::read_env() } else { - sprintf(start_db_command[i], "%s", "service mysql stop"); + sprintf(stop_db_command[i], "%s", "service mysql stop"); } //reading cleanup_db_command diff --git a/maxscale-system-test/mdbci/set_env.sh b/maxscale-system-test/mdbci/set_env.sh index da1608a19..86459e8d1 100644 --- a/maxscale-system-test/mdbci/set_env.sh +++ b/maxscale-system-test/mdbci/set_env.sh @@ -70,7 +70,7 @@ do else ${mdbci_dir}/mdbci ssh --command 'echo \"/usr/sbin/mysqld \$* 2> stderr.log > stdout.log &\" > mysql_start.sh; echo \"sleep 20\" >> mysql_start.sh; echo \"disown\" >> mysql_start.sh; chmod a+x mysql_start.sh' $config_name/node_$num --silent eval 'export $start_cmd_var="/home/$au/mysql_start.sh "' - eval 'export $start_cmd_var="killall mysqld "' + eval 'export $stop_cmd_var="killall mysqld "' fi else eval 'export $start_cmd_var="$mysql_exe start "' From 2b653887a34bcb47845182b8c52bdfd11dc5ecf7 Mon Sep 17 00:00:00 2001 From: Dapeng Huang <7xerocha@gmail.com> Date: Sat, 13 Jan 2018 12:41:05 +0800 Subject: [PATCH 24/47] fix crash at execute empty flush command --- server/modules/routing/maxinfo/maxinfo_exec.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server/modules/routing/maxinfo/maxinfo_exec.c b/server/modules/routing/maxinfo/maxinfo_exec.c index f4a37f967..bebe940fd 100644 --- a/server/modules/routing/maxinfo/maxinfo_exec.c +++ b/server/modules/routing/maxinfo/maxinfo_exec.c @@ -354,6 +354,13 @@ exec_flush(DCB *dcb, MAXINFO_TREE *tree) int i; char errmsg[120]; + sprintf(errmsg, "Unsupported flush command '%s'", tree->value); + if(!tree) + { + maxinfo_send_error(dcb, 0, errmsg); + MXS_ERROR("%s", errmsg); + return; + } for (i = 0; flush_commands[i].name; i++) { if (strcasecmp(flush_commands[i].name, tree->value) == 0) @@ -366,7 +373,6 @@ exec_flush(DCB *dcb, MAXINFO_TREE *tree) { tree->value[80] = 0; } - sprintf(errmsg, "Unsupported flush command '%s'", tree->value); maxinfo_send_error(dcb, 0, errmsg); MXS_ERROR("%s", errmsg); } From 454f195cb03d05e5692f18170fc26db0ed36b27d Mon Sep 17 00:00:00 2001 From: Dapeng Huang <7xerocha@gmail.com> Date: Sat, 13 Jan 2018 12:54:12 +0800 Subject: [PATCH 25/47] fix:cannot connect to maxinfo with python client --- .gitignore | 9 +++++++++ server/modules/routing/maxinfo/maxinfo.c | 10 +++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 70154a506..f47acb69a 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,12 @@ CMakeFiles/* */*/*/*/CMakeFiles/* Makefile /.DS_Store + +# Netbeans Project files +nbproject/ +/build/ + +# RBCommons +.reviewboardrc +# vscode +.vscode diff --git a/server/modules/routing/maxinfo/maxinfo.c b/server/modules/routing/maxinfo/maxinfo.c index 63c84e3bf..81646620a 100644 --- a/server/modules/routing/maxinfo/maxinfo.c +++ b/server/modules/routing/maxinfo/maxinfo.c @@ -61,7 +61,7 @@ static int maxinfo_statistics(INFO_INSTANCE *, INFO_SESSION *, GWBUF *); static int maxinfo_ping(INFO_INSTANCE *, INFO_SESSION *, GWBUF *); static int maxinfo_execute_query(INFO_INSTANCE *, INFO_SESSION *, char *); static int handle_url(INFO_INSTANCE *instance, INFO_SESSION *router_session, GWBUF *queue); - +static int maxinfo_send_ok(DCB *dcb); /* The router entry points */ static MXS_ROUTER *createInstance(SERVICE *service, char **options); @@ -356,7 +356,7 @@ execute(MXS_ROUTER *rinstance, MXS_ROUTER_SESSION *router_session, GWBUF *queue) switch (MYSQL_COMMAND(queue)) { case COM_PING: - rc = maxinfo_ping(instance, session, queue); + rc = maxinfo_send_ok(session->dcb); break; case COM_STATISTICS: rc = maxinfo_statistics(instance, session, queue); @@ -618,7 +618,7 @@ maxinfo_execute_query(INFO_INSTANCE *instance, INFO_SESSION *session, char *sql) respond_starttime(session->dcb); return 1; } - if (strcasecmp(sql, "set names 'utf8'") == 0) + if (strncasecmp(sql, "set names", 9) == 0) { return maxinfo_send_ok(session->dcb); } @@ -626,6 +626,10 @@ maxinfo_execute_query(INFO_INSTANCE *instance, INFO_SESSION *session, char *sql) { return maxinfo_send_ok(session->dcb); } + if (strncasecmp(sql, "set @@session", 13) == 0) + { + return maxinfo_send_ok(session->dcb); + } if (strncasecmp(sql, "set autocommit", 14) == 0) { return maxinfo_send_ok(session->dcb); From 76e53f2081d816af7aed13de77e44a2fc39d6a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Tue, 16 Jan 2018 08:42:40 +0200 Subject: [PATCH 26/47] Make contribution instructions more concise The instructions now separate the two parts more clearly so that bug fixes are always done to the oldest possible version. --- CONTRIBUTING.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 69af32211..61f666a74 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,9 +17,8 @@ Without this comment, the pull request will not be accepted. ## Practicalities * Please ensure that your pull-request has been made against the correct - branch. For bug fixes or minor improvements, use the default branch and for - new code, use the `develop` branch (e.g. for a patch to 2.1.x, use the `2.1` - branch). + branch. For bug fixes or minor improvements, use the default branch (at the + time of writing `2.1`). For new features, use the `develop` branch. * Please ensure that your code follows our [Coding Style](https://github.com/mariadb-corporation/MaxScale/wiki/Coding-Style-and-Guidelines). All new code should be formatted with the From bf02571f4500e1edaa536b5330c530d03a49de38 Mon Sep 17 00:00:00 2001 From: Johan Wikman Date: Tue, 16 Jan 2018 09:36:36 +0200 Subject: [PATCH 27/47] MXS-1596 Test switchover under stress - Start 4 threads where each thread sits in a loop and performs 20% updates and 80% selects. Each thread has a table of its own. - The main thread executes the following in a loop. - Perform a switchover from the current master to the next (which is simply the next node % all nodes). - Keep on doing that for 1.5 minutes. The expectation is that the switchover will succeed, that is, after the operation there will be a new master. --- maxscale-system-test/.gitignore | 3 +- maxscale-system-test/CMakeLists.txt | 4 + ...le.cnf.template.mysqlmon_switchover_stress | 94 +++ .../mysqlmon_switchover_stress.cpp | 598 ++++++++++++++++++ 4 files changed, 698 insertions(+), 1 deletion(-) create mode 100644 maxscale-system-test/cnf/maxscale.cnf.template.mysqlmon_switchover_stress create mode 100644 maxscale-system-test/mysqlmon_switchover_stress.cpp diff --git a/maxscale-system-test/.gitignore b/maxscale-system-test/.gitignore index b54406bcd..ce111daa4 100644 --- a/maxscale-system-test/.gitignore +++ b/maxscale-system-test/.gitignore @@ -123,8 +123,9 @@ mysqlmon_failover_rejoin_old_slave mysqlmon_failover_rolling_master mysqlmon_failover_rolling_restart_slaves mysqlmon_failover_stress -mysqlmon_switchover_bad_master mysqlmon_switchover +mysqlmon_switchover_bad_master +mysqlmon_switchover_stress mxs1045 mxs1071_maxrows mxs1110_16mb diff --git a/maxscale-system-test/CMakeLists.txt b/maxscale-system-test/CMakeLists.txt index bba603f4d..775f75e15 100644 --- a/maxscale-system-test/CMakeLists.txt +++ b/maxscale-system-test/CMakeLists.txt @@ -290,8 +290,12 @@ add_test_executable(mysqlmon_failover_rejoin_old_slave.cpp mysqlmon_failover_rej # MySQL Monitor rolling restart slaves add_test_executable(mysqlmon_failover_rolling_restart_slaves.cpp mysqlmon_failover_rolling_restart_slaves mysqlmon_failover_rolling_restart_slaves LABELS mysqlmon REPL_BACKEND) +# MySQL Monitor failover stress add_test_executable(mysqlmon_failover_stress.cpp mysqlmon_failover_stress mysqlmon_failover_stress LABELS mysqlmon REPL_BACKEND) +# MySQL Monitor switchover stress +add_test_executable(mysqlmon_switchover_stress.cpp mysqlmon_switchover_stress mysqlmon_switchover_stress LABELS mysqlmon REPL_BACKEND) + # Test monitor state change events when manually clearing server bits add_test_executable(false_monitor_state_change.cpp false_monitor_state_change replication LABELS mysqlmon REPL_BACKEND) diff --git a/maxscale-system-test/cnf/maxscale.cnf.template.mysqlmon_switchover_stress b/maxscale-system-test/cnf/maxscale.cnf.template.mysqlmon_switchover_stress new file mode 100644 index 000000000..4a8399367 --- /dev/null +++ b/maxscale-system-test/cnf/maxscale.cnf.template.mysqlmon_switchover_stress @@ -0,0 +1,94 @@ +[maxscale] +threads=###threads### + +[MySQL-Monitor] +type=monitor +module=mysqlmon +servers=server1, server2, server3, server4 +user=maxskysql +passwd=skysql +monitor_interval=1000 +allow_cluster_recovery=true +detect_standalone_master=true +auto_failover=false +auto_rejoin=false +replication_user=repl +replication_password=repl +backend_connect_timeout=5 +backend_read_timeout=5 +backend_write_timeout=5 + +[RW-Split-Router] +type=service +router= readwritesplit +servers=server1, server2, server3, server4 +user=maxskysql +passwd=skysql + +[Read-Connection-Router-Slave] +type=service +router=readconnroute +router_options= slave +servers=server1, server2, server3, server4 +user=maxskysql +passwd=skysql + +[Read-Connection-Router-Master] +type=service +router=readconnroute +router_options=master +servers=server1, server2, server3, server4 +user=maxskysql +passwd=skysql + +[RW-Split-Listener] +type=listener +service=RW-Split-Router +protocol=MySQLClient +port=4006 + +[Read-Connection-Listener-Slave] +type=listener +service=Read-Connection-Router-Slave +protocol=MySQLClient +port=4009 + +[Read-Connection-Listener-Master] +type=listener +service=Read-Connection-Router-Master +protocol=MySQLClient +port=4008 + +[CLI] +type=service +router=cli + +[CLI Listener] +type=listener +service=CLI +protocol=maxscaled +socket=default + +[server1] +type=server +address=###node_server_IP_1### +port=###node_server_port_1### +protocol=MySQLBackend + +[server2] +type=server +address=###node_server_IP_2### +port=###node_server_port_2### +protocol=MySQLBackend + +[server3] +type=server +address=###node_server_IP_3### +port=###node_server_port_3### +protocol=MySQLBackend + +[server4] +type=server +address=###node_server_IP_4### +port=###node_server_port_4### +protocol=MySQLBackend diff --git a/maxscale-system-test/mysqlmon_switchover_stress.cpp b/maxscale-system-test/mysqlmon_switchover_stress.cpp new file mode 100644 index 000000000..1242cbf4e --- /dev/null +++ b/maxscale-system-test/mysqlmon_switchover_stress.cpp @@ -0,0 +1,598 @@ +/* + * 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: 2020-01-01 + * + * 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. + */ + +#include +#include +#include +#include +#include +#include +#include "testconnections.h" +#include "fail_switch_rejoin_common.cpp" + +using namespace std; + +// How often the monitor checks the server state. +// NOTE: Ensure this is identical with the value in the configuration file. +const time_t MONITOR_INTERVAL = 1; + +// After how many seconds should the switchover operation surely have +// been performed. Not very critical. +const time_t SWITCHOVER_DURATION = 5; + +// How long should we keep in running. +const time_t TEST_DURATION = 90; + +#define CMESSAGE(msg) \ + do {\ + stringstream ss;\ + ss << "client(" << m_id << ") : " << msg << "\n";\ + cout << ss.str() << flush;\ + } while (false) + +#if !defined(NDEBUG) + +#define ss_dassert(x) do { if (!(x)) { fprintf(stderr, "Assertion failed: %s", #x); abort(); } } while(false) +#define ss_debug(x) x + +#else + +#define ss_dassert(s) +#define ss_debug(x) + +#endif + + +namespace +{ + +class Client +{ +public: + enum + { + DEFAULT_N_CLIENTS = 4, + DEFAULT_N_ROWS = 100 + }; + + static void init(TestConnections& test, size_t nClients, size_t nRows) + { + s_nClients = nClients; + s_nRows = nRows; + + if (create_tables(test)) + { + if (insert_data(test)) + { + cout << "\nSyncing slaves." << endl; + test.repl->sync_slaves(); + } + } + } + + static void start(bool verbose, + const char* zHost, int port, const char* zUser, const char* zPassword) + { + for (size_t i = 0; i < s_nClients; ++i) + { + s_threads.push_back(std::thread(&Client::thread_main, + i, verbose, zHost, port, zUser, zPassword)); + } + } + + static void stop() + { + s_shutdown = true; + + for (size_t i = 0; i < s_nClients; ++i) + { + s_threads[i].join(); + } + } + +private: + Client(int id, bool verbose) + : m_id(id) + , m_verbose(verbose) + , m_value(1) + { + ss_debug(int rv); + + unsigned int seed = (time(NULL) << m_id); + ss_debug(rv =) initstate_r(seed, m_initstate, sizeof(m_initstate), &m_random_data); + ss_dassert(rv == 0); + + ss_debug(rv=) srandom_r(seed, &m_random_data); + ss_dassert(rv == 0); + } + + enum action_t + { + ACTION_SELECT, + ACTION_UPDATE + }; + + action_t action() const + { + double d = random_decimal_fraction(); + + // 20% updates + // 80% selects + if (d <= 0.2) + { + return ACTION_UPDATE; + } + else + { + return ACTION_SELECT; + } + } + + bool run(MYSQL* pConn) + { + bool rv = false; + + switch (action()) + { + case ACTION_SELECT: + rv = run_select(pConn); + break; + + case ACTION_UPDATE: + rv = run_update(pConn); + break; + + default: + ss_dassert(!true); + } + + return rv; + } + + bool run_select(MYSQL* pConn) + { + bool rv = true; + + string stmt("SELECT * FROM test.t"); + stmt += std::to_string(m_id); + stmt += " WHERE id="; + stmt += std::to_string(get_random_id()); + + if (mysql_query(pConn, stmt.c_str()) == 0) + { + flush_response(pConn); + } + else + { + if (m_verbose) + { + CMESSAGE("\"" << stmt << "\" failed: " << mysql_error(pConn)); + } + rv = false; + } + + return rv; + } + + bool run_update(MYSQL* pConn) + { + bool rv = true; + + string stmt("UPDATE test.t"); + stmt += std::to_string(m_id); + stmt += " SET id="; + stmt += std::to_string(m_value); + stmt += " WHERE id="; + stmt += std::to_string(get_random_id()); + m_value = (m_value + 1) % s_nRows; + + if (mysql_query(pConn, stmt.c_str()) == 0) + { + flush_response(pConn); + } + else + { + if (m_verbose) + { + CMESSAGE("\"" << stmt << "\" failed: " << mysql_error(pConn)); + } + rv = false; + } + + return rv; + } + + static void flush_response(MYSQL* pConn) + { + do + { + MYSQL_RES* pRes = mysql_store_result(pConn); + mysql_free_result(pRes); + } + while (mysql_next_result(pConn) == 0); + } + + int get_random_id() const + { + int id = s_nRows * random_decimal_fraction(); + + ss_dassert(id >= 0); + ss_dassert(id <= s_nRows); + + return id; + } + + double random_decimal_fraction() const + { + int32_t r; + ss_debug(int rv=) random_r(&m_random_data, &r); + ss_dassert(rv == 0); + + return double(r) / RAND_MAX; + } + + void run(const char* zHost, int port, const char* zUser, const char* zPassword) + { + do + { + MYSQL* pMysql = mysql_init(NULL); + + if (pMysql) + { + unsigned int timeout = 5; + mysql_options(pMysql, MYSQL_OPT_CONNECT_TIMEOUT, &timeout); + mysql_options(pMysql, MYSQL_OPT_READ_TIMEOUT, &timeout); + mysql_options(pMysql, MYSQL_OPT_WRITE_TIMEOUT, &timeout); + + if (m_verbose) + { + CMESSAGE("Connecting"); + } + + if (mysql_real_connect(pMysql, zHost, zUser, zPassword, "test", port, NULL, 0)) + { + if (m_verbose) + { + CMESSAGE("Connected."); + } + + while (!s_shutdown && run(pMysql)) + { + ; + } + } + else + { + if (m_verbose) + { + CMESSAGE("mysql_real_connect() failed: " << mysql_error(pMysql)); + } + } + + if (m_verbose) + { + CMESSAGE("Closing"); + } + mysql_close(pMysql); + } + else + { + CMESSAGE("mysql_init() failed."); + } + + // To prevent some backend from becoming overwhelmed. + sleep(1); + } + while (!s_shutdown); + } + + static void thread_main(int i, bool verbose, + const char* zHost, int port, const char* zUser, const char* zPassword) + { + if (mysql_thread_init() == 0) + { + Client client(i, verbose); + + client.run(zHost, port, zUser, zPassword); + + mysql_thread_end(); + } + else + { + int m_id = i; + CMESSAGE("mysql_thread_init() failed."); + } + } + + static bool create_tables(TestConnections& test) + { + cout << "\nCreating tables." << endl; + + MYSQL* pConn = test.maxscales->conn_rwsplit[0]; + + string drop_head("DROP TABLE IF EXISTS test.t"); + string create_head("CREATE TABLE test.t"); + string create_tail(" (id INT)"); + + for (size_t i = 0; i < s_nClients; ++i) + { + string drop = drop_head + std::to_string(i); + test.try_query(pConn, drop.c_str()); + + string create = create_head + std::to_string(i) + create_tail; + test.try_query(pConn, create.c_str()); + } + + return test.ok(); + } + + static bool insert_data(TestConnections& test) + { + cout << "\nInserting data." << endl; + + MYSQL* pConn = test.maxscales->conn_rwsplit[0]; + + for (size_t i = 0; i < s_nClients; ++i) + { + string insert("insert into test.t"); + insert += std::to_string(i); + insert += " values "; + + for (size_t j = 0; j < s_nRows; ++j) + { + insert += "("; + insert += std::to_string(j); + insert += ")"; + + if (j < s_nRows - 1) + { + insert += ", "; + } + } + + test.try_query(pConn, insert.c_str()); + } + + return test.ok(); + } + +private: + enum + { + INITSTATE_SIZE = 32 + }; + + size_t m_id; + bool m_verbose; + size_t m_value; + char m_initstate[INITSTATE_SIZE]; + mutable struct random_data m_random_data; + + static size_t s_nClients; + static size_t s_nRows; + static bool s_shutdown; + + static std::vector s_threads; +}; + +size_t Client::s_nClients; +size_t Client::s_nRows; +bool Client::s_shutdown; +std::vector Client::s_threads; + +} + +namespace +{ + +void list_servers(TestConnections& test) +{ + test.maxscales->execute_maxadmin_command_print(0, (char*)"list servers"); +} + +void sleep(int s) +{ + cout << "Sleeping " << s << " times 1 second" << flush; + do + { + ::sleep(1); + cout << "." << flush; + --s; + } + while (s > 0); + + cout << endl; +} + +bool check_server_status(TestConnections& test, int id) +{ + bool is_master = false; + + Mariadb_nodes* pRepl = test.repl; + + string server = string("server") + std::to_string(id); + + StringSet statuses = test.get_server_status(server.c_str()); + std::ostream_iterator oi(cout, " "); + + cout << server << ": "; + std::copy(statuses.begin(), statuses.end(), oi); + + cout << " => "; + + if (statuses.count("Master")) + { + is_master = true; + cout << "OK" << endl; + } + else if (statuses.count("Slave")) + { + cout << "OK" << endl; + } + else if (statuses.count("Running")) + { + MYSQL* pConn = pRepl->nodes[id - 1]; + + char result[1024]; + if (find_field(pConn, "SHOW SLAVE STATUS", "Last_IO_Error", result) == 0) + { + cout << result << endl; + test.assert(false, "Server is neither slave, nor master."); + } + else + { + cout << "?" << endl; + test.assert(false, "Could not execute \"SHOW SLAVE STATUS\""); + } + } + else + { + cout << "?" << endl; + test.assert(false, "Unexpected server state for %s.", server.c_str()); + } + + return is_master; +} + +void check_server_statuses(TestConnections& test) +{ + int masters = 0; + + masters += check_server_status(test, 1); + masters += check_server_status(test, 2); + masters += check_server_status(test, 3); + masters += check_server_status(test, 4); + + test.assert(masters == 1, "Unpexpected number of masters: %d", masters); +} + +int get_next_master_id(TestConnections& test, int current_id) +{ + int next_id; + + do + { + next_id = (current_id + 1) % 5; + string server("server"); + server += std::to_string(next_id); + StringSet states = test.get_server_status(server.c_str()); + if (states.count("Slave") != 0) + { + break; + } + } + while (next_id != current_id); + + return next_id != current_id ? next_id : -1; +} + +void run(TestConnections& test) +{ + int n_threads = Client::DEFAULT_N_CLIENTS; + + cout << "\nConnecting to MaxScale." << endl; + test.maxscales->connect_maxscale(); + + Client::init(test, Client::DEFAULT_N_CLIENTS, Client::DEFAULT_N_ROWS); + + if (test.ok()) + { + const char* zHost = test.maxscales->IP[0]; + int port = test.maxscales->rwsplit_port[0]; + const char* zUser = test.maxscales->user_name; + const char* zPassword = test.maxscales->password; + + cout << "Connecting to " << zHost << ":" << port << " as " << zUser << ":" << zPassword << endl; + cout << "Starting clients." << endl; + Client::start(test.verbose, zHost, port, zUser, zPassword); + + time_t start = time(NULL); + + list_servers(test); + + int current_master_id = 1; + + while ((test.global_result == 0) && (time(NULL) - start < TEST_DURATION)) + { + sleep(SWITCHOVER_DURATION); + + int next_master_id = get_next_master_id(test, current_master_id); + + if (next_master_id != -1) + { + cout << "\nTrying to do manual switchover from server" << current_master_id + << " to server" << next_master_id << endl; + + string command("call command mysqlmon switchover MySQL-Monitor "); + command += "server"; + command += std::to_string(next_master_id); + command += " "; + command += "server"; + command += std::to_string(current_master_id); + + cout << "\nCommand: " << command << endl; + + test.maxscales->execute_maxadmin_command_print(0, (char*)command.c_str()); + + sleep(1); + list_servers(test); + + sleep(SWITCHOVER_DURATION); + + current_master_id = next_master_id; + + int master_id = get_master_server_id(test); + + if (master_id < 0) + { + test.assert(false, "No master available after switchover."); + } + else if (master_id != current_master_id) + { + test.assert(false, + "Master should have been server%d, but it was server%d.", + current_master_id, master_id); + } + } + else + { + test.assert(false, + "Could not find any slave to switch to."); + } + } + + cout << "\nStopping clients.\n" << flush; + Client::stop(); + + sleep(SWITCHOVER_DURATION); + + test.repl->close_connections(); + test.repl->connect(); + + check_server_statuses(test); + } +} + +} + +int main(int argc, char* argv[]) +{ + std::ios::sync_with_stdio(true); + + Mariadb_nodes::require_gtid(true); + TestConnections test(argc, argv); + + run(test); + + return test.global_result; +} From ff2ad05d0a69e8f98b4725c87a496b62ff45cd0a Mon Sep 17 00:00:00 2001 From: Esa Korhonen Date: Thu, 11 Jan 2018 14:56:59 +0200 Subject: [PATCH 28/47] Add manual rejoin command to MariaDB Monitor The rejoin command takes as parameters the monitor name and name of server to rejoin. This change required refactoring the rejoin code. --- .../modules/monitor/mariadbmon/mysql_mon.cc | 358 ++++++++++++++---- 1 file changed, 276 insertions(+), 82 deletions(-) diff --git a/server/modules/monitor/mariadbmon/mysql_mon.cc b/server/modules/monitor/mariadbmon/mysql_mon.cc index 1c7cfbb96..021dbf876 100644 --- a/server/modules/monitor/mariadbmon/mysql_mon.cc +++ b/server/modules/monitor/mariadbmon/mysql_mon.cc @@ -98,6 +98,7 @@ static void set_slave_heartbeat(MXS_MONITOR *, MXS_MONITORED_SERVER *); static int add_slave_to_master(long *, int, long); static bool isMySQLEvent(mxs_monitor_event_t event); void check_maxscale_schema_replication(MXS_MONITOR *monitor); +static MySqlServerInfo* get_server_info(const MYSQL_MONITOR* handle, const MXS_MONITORED_SERVER* db); static bool mon_process_failover(MYSQL_MONITOR*, uint32_t, bool*); static bool do_failover(MYSQL_MONITOR* mon, json_t** output); static bool do_switchover(MYSQL_MONITOR* mon, MXS_MONITORED_SERVER* current_master, @@ -107,9 +108,16 @@ static bool update_replication_settings(MXS_MONITORED_SERVER *database, MySqlSer static bool query_one_row(MXS_MONITORED_SERVER *database, const char* query, unsigned int expected_cols, StringVector* output); static void read_server_variables(MXS_MONITORED_SERVER* database, MySqlServerInfo* serv_info); -static int check_and_join_cluster(MYSQL_MONITOR* mon); +static bool server_is_rejoin_suspect(MYSQL_MONITOR* mon, MXS_MONITORED_SERVER* server, + MySqlServerInfo* master_info); +static bool get_joinable_servers(MYSQL_MONITOR* mon, ServerVector* output); +static uint32_t do_rejoin(MYSQL_MONITOR* mon, const ServerVector& servers); static bool join_cluster(MXS_MONITORED_SERVER* server, const char* change_cmd); static void disable_setting(MYSQL_MONITOR* mon, const char* setting); +static bool cluster_can_be_joined(MYSQL_MONITOR* mon); +static bool can_replicate_from(MYSQL_MONITOR* mon, + MXS_MONITORED_SERVER* slave, MySqlServerInfo* slave_info, + MXS_MONITORED_SERVER* master, MySqlServerInfo* master_info); static bool report_version_err = true; static const char* hb_table_name = "maxscale_schema.replication_heartbeat"; @@ -528,6 +536,118 @@ bool mysql_handle_failover(const MODULECMD_ARG* args, json_t** output) return rv; } +/** + * Perform user-activated rejoin + * + * @param mon Cluster monitor + * @param rejoin_server Server to join + * @param output Json error output + * @return True on success + */ +bool mysql_rejoin(MXS_MONITOR* mon, SERVER* rejoin_server, json_t** output) +{ + MYSQL_MONITOR *handle = static_cast(mon->handle); + bool stopped = stop_monitor(mon); + if (stopped) + { + MXS_NOTICE("Stopped monitor %s for the duration of rejoin.", mon->name); + } + else + { + MXS_NOTICE("Monitor %s already stopped, rejoin can proceed.", mon->name); + } + + bool rval = false; + if (cluster_can_be_joined(handle)) + { + MXS_MONITORED_SERVER* mon_server = NULL; + // Search for the MONITORED_SERVER. Could this be a general monitor function? + for (MXS_MONITORED_SERVER* iterator = mon->monitored_servers; + iterator != NULL && mon_server == NULL; + iterator = iterator->next) + { + if (iterator->server == rejoin_server) + { + mon_server = iterator; + } + } + + if (mon_server) + { + MXS_MONITORED_SERVER* master = handle->master; + MySqlServerInfo* master_info = get_server_info(handle, master); + MySqlServerInfo* server_info = get_server_info(handle, mon_server); + + if (server_is_rejoin_suspect(handle, mon_server, master_info) && + update_gtids(handle, master, master_info) && + can_replicate_from(handle, mon_server, server_info, master, master_info)) + { + ServerVector joinable_server; + joinable_server.push_back(mon_server); + if (do_rejoin(handle, joinable_server) == 1) + { + rval = true; + MXS_NOTICE("Rejoin performed."); + } + else + { + PRINT_MXS_JSON_ERROR(output, "Rejoin attempted but failed."); + } + } + else + { + PRINT_MXS_JSON_ERROR(output, "Server is not eligible for rejoin or eligibility could not be " + "ascertained."); + } + } + else + { + PRINT_MXS_JSON_ERROR(output, "The given server '%s' is not monitored by this monitor.", + rejoin_server->unique_name); + } + } + else + { + const char BAD_CLUSTER[] = "The server cluster of monitor '%s' is not in a state valid for joining. " + "Either it has no master or its gtid domain is unknown."; + PRINT_MXS_JSON_ERROR(output, BAD_CLUSTER, mon->name); + } + + if (stopped) + { + startMonitor(mon, mon->parameters); + } + return rval; +} + +/** + * Command handler for 'rejoin' + * + * @param args Arguments given by user + * @param output Json error output + * @return True on success + */ +bool mysql_handle_rejoin(const MODULECMD_ARG* args, json_t** output) +{ + ss_dassert(args->argc == 2); + ss_dassert(MODULECMD_GET_TYPE(&args->argv[0].type) == MODULECMD_ARG_MONITOR); + ss_dassert(MODULECMD_GET_TYPE(&args->argv[1].type) == MODULECMD_ARG_SERVER); + + MXS_MONITOR* mon = args->argv[0].value.monitor; + SERVER* server = args->argv[1].value.server; + + bool rv = false; + if (!config_get_global_options()->passive) + { + rv = mysql_rejoin(mon, server, output); + } + else + { + PRINT_MXS_JSON_ERROR(output, "Rejoin attempted but not performed, as MaxScale is in passive mode."); + } + return rv; +} + /** * The module entry point routine. It is this routine that * must populate the structure that is referred to as the @@ -569,6 +689,19 @@ extern "C" mysql_handle_failover, MXS_ARRAY_NELEMS(failover_argv), failover_argv, "Perform master failover"); + static modulecmd_arg_type_t rejoin_argv[] = + { + { + MODULECMD_ARG_MONITOR | MODULECMD_ARG_NAME_MATCHES_DOMAIN, + ARG_MONITOR_DESC + }, + { MODULECMD_ARG_SERVER, "Joining server" } + }; + + modulecmd_register_command(MXS_MODULE_NAME, "rejoin", MODULECMD_TYPE_ACTIVE, + mysql_handle_rejoin, MXS_ARRAY_NELEMS(rejoin_argv), + rejoin_argv, "Rejoin server to a cluster"); + static MXS_MONITOR_OBJECT MyObject = { startMonitor, @@ -2319,23 +2452,30 @@ monitorMain(void *arg) // Do not auto-join servers on this monitor loop if a failover (or any other cluster modification) // has been performed, as server states have not been updated yet. It will happen next iteration. - if (handle->auto_rejoin && !failover_performed && - handle->master != NULL && SERVER_IS_MASTER(handle->master->server) && - handle->master_gtid_domain >= 0) + if (handle->auto_rejoin && !failover_performed && cluster_can_be_joined(handle)) { // Check if any servers should be autojoined to the cluster - int joins = check_and_join_cluster(handle); - if (joins < 0) + ServerVector joinable_servers; + if (get_joinable_servers(handle, &joinable_servers)) { - MXS_ERROR("A cluster join operation failed, disabling automatic rejoining. " - "To re-enable, manually set '%s' to 'true' for monitor '%s' via MaxAdmin or " - "the REST API.", CN_AUTO_REJOIN, mon->name); - handle->auto_rejoin = false; - disable_setting(handle, CN_AUTO_REJOIN); + uint32_t joins = do_rejoin(handle, joinable_servers); + if (joins > 0) + { + MXS_NOTICE("%d server(s) redirected or rejoined the cluster.", joins); + } + if (joins < joinable_servers.size()) + { + MXS_ERROR("A cluster join operation failed, disabling automatic rejoining. " + "To re-enable, manually set '%s' to 'true' for monitor '%s' via MaxAdmin or " + "the REST API.", CN_AUTO_REJOIN, mon->name); + handle->auto_rejoin = false; + disable_setting(handle, CN_AUTO_REJOIN); + } } - else if (joins > 0) + else { - MXS_NOTICE("%d server(s) redirected or rejoined the cluster.", joins); + MXS_ERROR("Query error to master '%s' prevented a possible rejoin operation.", + handle->master->server->unique_name); } } @@ -4128,102 +4268,145 @@ static bool can_replicate_from(MYSQL_MONITOR* mon, } /** - * Check cluster for servers not replicating from the current master and redirect/join them. If an error - * occurs, stop and return negative value. + * Checks if a server is a possible rejoin candidate. A true result from this function is not yet sufficient + * criteria and another call to can_replicate_from() should be made. * * @param mon Cluster monitor - * @return The number of servers successfully redirected. Negative on I/O-error. + * @param server Server to check. + * @param master_info Master server info + * @return True, if server is a rejoin suspect. */ -static int check_and_join_cluster(MYSQL_MONITOR* mon) +static bool server_is_rejoin_suspect(MYSQL_MONITOR* mon, MXS_MONITORED_SERVER* server, + MySqlServerInfo* master_info) { + bool is_suspect = false; + if (!SERVER_IS_MASTER(server->server) && SERVER_IS_RUNNING(server->server)) + { + MySqlServerInfo* server_info = get_server_info(mon, server); + SlaveStatusInfo* slave_status = &server_info->slave_status; + // Has no slave connection, yet is not a master. + if (server_info->n_slaves_configured == 0) + { + is_suspect = true; + } + // Or has existing slave connection ... + else if (server_info->n_slaves_configured == 1) + { + MXS_MONITORED_SERVER* master = mon->master; + // which is connected to master but it's the wrong one + if (slave_status->slave_io_running && + slave_status->master_server_id != master_info->server_id) + { + is_suspect = true; + } + // or is disconnected but master host or port is wrong. + else if (!slave_status->slave_io_running && slave_status->slave_sql_running && + (slave_status->master_host != master->server->name || + slave_status->master_port != master->server->port)) + { + is_suspect = true; + } + } + } + return is_suspect; +} + +/** + * Scan the servers in the cluster and add (re)joinable servers to an array. + * + * @param mon Cluster monitor + * @param output Array to save results to. Each element is a valid (re)joinable server according + * to latest data. + * @return False, if there were possible rejoinable servers but communications error to master server + * prevented final checks. + */ +static bool get_joinable_servers(MYSQL_MONITOR* mon, ServerVector* output) +{ + ss_dassert(output); MXS_MONITORED_SERVER* master = mon->master; MySqlServerInfo *master_info = get_server_info(mon, master); // Whether a join operation should be attempted or not depends on several criteria. Start with the ones // easiest to test. Go though all slaves and construct a preliminary list. ServerVector suspects; - std::vector suspect_infos; for (MXS_MONITORED_SERVER* server = mon->monitor->monitored_servers; server != NULL; server = server->next) { - if (!SERVER_IS_MASTER(server->server) && SERVER_IS_RUNNING(server->server)) + if (server_is_rejoin_suspect(mon, server, master_info)) { - MySqlServerInfo* server_info = get_server_info(mon, server); - SlaveStatusInfo* slave_status = &server_info->slave_status; - bool is_suspect = false; - // Has no slave connection, yet is not a master. - if (server_info->n_slaves_configured == 0) - { - is_suspect = true; - } - // Or has existing slave connection ... - else if (server_info->n_slaves_configured == 1) - { - // which is connected to master but it's the wrong one - if (slave_status->slave_io_running && - slave_status->master_server_id != master_info->server_id) - { - is_suspect = true; - } - // or is disconnected but master host or port is wrong - else if (!slave_status->slave_io_running && slave_status->slave_sql_running && - (slave_status->master_host != master->server->name || - slave_status->master_port != master->server->port)) - { - is_suspect = true; - } - } - if (is_suspect) - { - suspects.push_back(server); - suspect_infos.push_back(server_info); - } + suspects.push_back(server); } } - int rval = 0; + // Update Gtid of master for better info. + bool comm_ok = true; if (!suspects.empty()) { - // Update Gtid of master for better info. - if (!update_gtids(mon, master, master_info)) + if (update_gtids(mon, master, master_info)) { - rval = -1; - } - string change_cmd = generate_change_master_cmd(mon, master); - for (size_t i = 0; i < suspects.size() && rval >= 0; i++) - { - MXS_MONITORED_SERVER* suspect = suspects[i]; - MySqlServerInfo* suspect_info = suspect_infos[i]; - if (can_replicate_from(mon, suspect, suspect_info, master, master_info)) + for (size_t i = 0; i < suspects.size(); i++) { - bool op_success = true; - const char* name = suspect->server->unique_name; - const char* master_name = master->server->unique_name; - if (suspect_info->n_slaves_configured == 0) + MXS_MONITORED_SERVER* suspect = suspects[i]; + MySqlServerInfo* suspect_info = get_server_info(mon, suspect); + if (can_replicate_from(mon, suspect, suspect_info, master, master_info)) { - MXS_NOTICE("Directing standalone server '%s' to replicate from '%s'.", name, master_name); - op_success = join_cluster(suspect, change_cmd.c_str()); - } - else - { - MXS_NOTICE("Server '%s' is replicating from a server other than '%s', " - "redirecting it to '%s'.", name, master_name, master_name); - op_success = redirect_one_slave(suspect, change_cmd.c_str()); - } - - if (op_success) - { - rval++; - } - else - { - rval = -1; + output->push_back(suspect); } } } + else + { + comm_ok = false; + } } - return rval; + return comm_ok; +} + +/** + * (Re)join given servers to the cluster. The servers in the array are assumed to be joinable. + * Usually the list is created by get_joinable_servers(). + * + * @param mon Cluster monitor + * @param joinable_servers Which servers to rejoin + * @return The number of servers successfully rejoined + */ +static uint32_t do_rejoin(MYSQL_MONITOR* mon, const ServerVector& joinable_servers) +{ + MXS_MONITORED_SERVER* master = mon->master; + uint32_t servers_joined = 0; + if (!joinable_servers.empty()) + { + string change_cmd = generate_change_master_cmd(mon, master); + for (ServerVector::const_iterator iter = joinable_servers.begin(); + iter != joinable_servers.end(); + iter++) + { + MXS_MONITORED_SERVER* joinable = *iter; + const char* name = joinable->server->unique_name; + const char* master_name = master->server->unique_name; + MySqlServerInfo* redir_info = get_server_info(mon, joinable); + + bool op_success; + if (redir_info->n_slaves_configured == 0) + { + MXS_NOTICE("Directing standalone server '%s' to replicate from '%s'.", name, master_name); + op_success = join_cluster(joinable, change_cmd.c_str()); + } + else + { + MXS_NOTICE("Server '%s' is replicating from a server other than '%s', " + "redirecting it to '%s'.", name, master_name, master_name); + op_success = redirect_one_slave(joinable, change_cmd.c_str()); + } + + if (op_success) + { + servers_joined++; + } + } + } + return servers_joined; } /** @@ -4265,3 +4448,14 @@ static void disable_setting(MYSQL_MONITOR* mon, const char* setting) p.value = const_cast("false"); monitorAddParameters(mon->monitor, &p); } + +/** + * Is the cluster a valid rejoin target + * + * @param mon Cluster monitor + * @return True, if cluster can be joined + */ +static bool cluster_can_be_joined(MYSQL_MONITOR* mon) +{ + return (mon->master != NULL && SERVER_IS_MASTER(mon->master->server) && mon->master_gtid_domain >= 0); +} From c2c898ee934eebfb78b6377ec5d6b848c4a5af0b Mon Sep 17 00:00:00 2001 From: Esa Korhonen Date: Thu, 11 Jan 2018 15:05:35 +0200 Subject: [PATCH 29/47] Fix formatting in MariaDB Monitor --- .../modules/monitor/mariadbmon/mysql_mon.cc | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/server/modules/monitor/mariadbmon/mysql_mon.cc b/server/modules/monitor/mariadbmon/mysql_mon.cc index 021dbf876..abc3796df 100644 --- a/server/modules/monitor/mariadbmon/mysql_mon.cc +++ b/server/modules/monitor/mariadbmon/mysql_mon.cc @@ -1390,11 +1390,11 @@ static bool do_show_slave_status(MYSQL_MONITOR* mon, const char* last_io_error = mxs_mysql_get_value(result, row, "Last_IO_Error"); const char* last_sql_error = mxs_mysql_get_value(result, row, "Last_SQL_Error"); ss_dassert(beats && period && using_gtid && master_host && master_port && - last_io_error && last_sql_error); + last_io_error && last_sql_error); serv_info->slave_status.master_host = master_host; serv_info->slave_status.master_port = atoi(master_port); serv_info->slave_status.last_error = *last_io_error ? last_io_error : - (*last_sql_error ? last_sql_error : ""); + (*last_sql_error ? last_sql_error : ""); int heartbeats = atoi(beats); if (serv_info->slave_heartbeats < heartbeats) @@ -1409,7 +1409,8 @@ static bool do_show_slave_status(MYSQL_MONITOR* mon, const char* gtid_io_pos = mxs_mysql_get_value(result, row, "Gtid_IO_Pos"); ss_dassert(gtid_io_pos); serv_info->slave_status.gtid_io_pos = gtid_io_pos[0] != '\0' ? - Gtid(gtid_io_pos, mon->master_gtid_domain) : Gtid(); + Gtid(gtid_io_pos, mon->master_gtid_domain) : + Gtid(); } else { @@ -2353,7 +2354,8 @@ monitorMain(void *arg) { monitor_clear_pending_status(root_master, SERVER_SLAVE | SERVER_SLAVE_OF_EXTERNAL_MASTER); - server_clear_status_nolock(root_master->server, SERVER_SLAVE | SERVER_SLAVE_OF_EXTERNAL_MASTER); + server_clear_status_nolock(root_master->server, + SERVER_SLAVE | SERVER_SLAVE_OF_EXTERNAL_MASTER); } } @@ -3403,9 +3405,10 @@ MXS_MONITORED_SERVER* select_new_master(MYSQL_MONITOR* mon, if (cand_io > master_io || // If io sequences are identical, the slave with more events processed wins. (cand_io == master_io && (cand_processed > master_processed || - // Finally, if binlog positions are identical, prefer a slave with - // log_slave_updates. - (cand_processed == master_processed && cand_updates && !master_updates)))) + // Finally, if binlog positions are identical, + // prefer a slave with log_slave_updates. + (cand_processed == master_processed && + cand_updates && !master_updates)))) { select_this = true; } @@ -4134,7 +4137,8 @@ static bool do_switchover(MYSQL_MONITOR* mon, MXS_MONITORED_SERVER* current_mast { redirected_slaves.push_back(demotion_target); } - int redirects = redirect_slaves(mon, promotion_target, redirectable_slaves, &redirected_slaves); + int redirects = redirect_slaves(mon, promotion_target, + redirectable_slaves, &redirected_slaves); bool success = redirectable_slaves.empty() ? start_ok : start_ok || redirects > 0; if (success == false) @@ -4183,12 +4187,12 @@ static bool do_switchover(MYSQL_MONITOR* mon, MXS_MONITORED_SERVER* current_mast if (mxs_mysql_query(demotion_target->con, QUERY_UNDO) == 0) { PRINT_MXS_JSON_ERROR(err_out, "read_only disabled on server %s.", - demotion_target->server->unique_name); + demotion_target->server->unique_name); } else { PRINT_MXS_JSON_ERROR(err_out, "Could not disable read_only on server %s: '%s'.", - demotion_target->server->unique_name, mysql_error(demotion_target->con)); + demotion_target->server->unique_name, mysql_error(demotion_target->con)); } } } From 5273cbada617e1e0214cb8c28549bda4cb8c43f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Tue, 16 Jan 2018 12:10:58 +0200 Subject: [PATCH 30/47] MXS-1600: Add case-insensitive matching to MySQLAuth The authenticator now supports similar identifier matching as the MariaDB server. The lower_case_table_names parameter explains its intended use (case-insensitive identifier matching): https://mariadb.com/kb/en/library/server-system-variables/#lower_case_table_names --- .../Authenticators/MySQL-Authenticator.md | 13 +++++++++++++ server/modules/authenticator/MySQLAuth/dbusers.c | 11 +++++++---- .../modules/authenticator/MySQLAuth/mysql_auth.c | 5 +++++ .../modules/authenticator/MySQLAuth/mysql_auth.h | 15 +++++++++++---- 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/Documentation/Authenticators/MySQL-Authenticator.md b/Documentation/Authenticators/MySQL-Authenticator.md index f2397d018..ffbdb0c7d 100644 --- a/Documentation/Authenticators/MySQL-Authenticator.md +++ b/Documentation/Authenticators/MySQL-Authenticator.md @@ -67,3 +67,16 @@ injected into the list of users. ``` authenticator_options=inject_service_user=false ``` + +### `lower_case_table_names` + +Enable case-insensitive identifier matching for authentication. This parameter +is disabled by default. + +The parameter functions exactly as the MariaDB Server system variable +[lower_case_table_names](https://mariadb.com/kb/en/library/server-system-variables/#lower_case_table_names). +This makes the matching done by the authenticator on database names to be +case-insensitive by converting all names into their lowercase form. + +**Note:** The identifier names are converted using an ASCII-only function. This + means that non-ASCII characters will retain their case-sensitivity. diff --git a/server/modules/authenticator/MySQLAuth/dbusers.c b/server/modules/authenticator/MySQLAuth/dbusers.c index eb72bba95..c1981272b 100644 --- a/server/modules/authenticator/MySQLAuth/dbusers.c +++ b/server/modules/authenticator/MySQLAuth/dbusers.c @@ -186,7 +186,10 @@ int validate_mysql_user(MYSQL_AUTH* instance, DCB *dcb, MYSQL_session *session, uint8_t *scramble, size_t scramble_len) { sqlite3 *handle = instance->handle; - size_t len = sizeof(mysqlauth_validate_user_query) + strlen(session->user) * 2 + + const char* validate_query = instance->lower_case_table_names ? + mysqlauth_validate_user_query_lower : + mysqlauth_validate_user_query; + size_t len = strlen(validate_query) + 1 + strlen(session->user) * 2 + strlen(session->db) * 2 + MYSQL_HOST_MAXLEN + session->auth_token_len * 4 + 1; char sql[len + 1]; int rval = MXS_AUTH_FAILED; @@ -198,7 +201,7 @@ int validate_mysql_user(MYSQL_AUTH* instance, DCB *dcb, MYSQL_session *session, } else { - sprintf(sql, mysqlauth_validate_user_query, session->user, dcb->remote, + sprintf(sql, validate_query, session->user, dcb->remote, dcb->remote, session->db, session->db); } @@ -214,7 +217,7 @@ int validate_mysql_user(MYSQL_AUTH* instance, DCB *dcb, MYSQL_session *session, if (!res.ok && strchr(dcb->remote, ':') && strchr(dcb->remote, '.')) { const char *ipv4 = strrchr(dcb->remote, ':') + 1; - sprintf(sql, mysqlauth_validate_user_query, session->user, ipv4, ipv4, + sprintf(sql, validate_query, session->user, ipv4, ipv4, session->db, session->db); if (sqlite3_exec(handle, sql, auth_cb, &res, &err) != SQLITE_OK) @@ -233,7 +236,7 @@ int validate_mysql_user(MYSQL_AUTH* instance, DCB *dcb, MYSQL_session *session, char client_hostname[MYSQL_HOST_MAXLEN] = ""; get_hostname(dcb, client_hostname, sizeof(client_hostname) - 1); - sprintf(sql, mysqlauth_validate_user_query, session->user, client_hostname, + sprintf(sql, validate_query, session->user, client_hostname, client_hostname, session->db, session->db); if (sqlite3_exec(handle, sql, auth_cb, &res, &err) != SQLITE_OK) diff --git a/server/modules/authenticator/MySQLAuth/mysql_auth.c b/server/modules/authenticator/MySQLAuth/mysql_auth.c index d8e4f6e17..2b9177e21 100644 --- a/server/modules/authenticator/MySQLAuth/mysql_auth.c +++ b/server/modules/authenticator/MySQLAuth/mysql_auth.c @@ -183,6 +183,7 @@ static void* mysql_auth_init(char **options) instance->cache_dir = NULL; instance->inject_service_user = true; instance->skip_auth = false; + instance->lower_case_table_names = false; instance->handle = NULL; for (int i = 0; options[i]; i++) @@ -209,6 +210,10 @@ static void* mysql_auth_init(char **options) { instance->skip_auth = config_truth_value(value); } + else if (strcmp(options[i], "lower_case_table_names") == 0) + { + instance->lower_case_table_names = config_truth_value(value); + } else { MXS_ERROR("Unknown authenticator option: %s", options[i]); diff --git a/server/modules/authenticator/MySQLAuth/mysql_auth.h b/server/modules/authenticator/MySQLAuth/mysql_auth.h index 07f66a1e5..ae289e202 100644 --- a/server/modules/authenticator/MySQLAuth/mysql_auth.h +++ b/server/modules/authenticator/MySQLAuth/mysql_auth.h @@ -66,6 +66,12 @@ static const char mysqlauth_validate_user_query[] = " WHERE user = '%s' AND ( '%s' = host OR '%s' LIKE host) AND (anydb = '1' OR '%s' = '' OR '%s' LIKE db)" " LIMIT 1"; +/** Query that checks if there's a grant for the user being authenticated */ +static const char mysqlauth_validate_user_query_lower[] = + "SELECT password FROM " MYSQLAUTH_USERS_TABLE_NAME + " WHERE user = '%s' AND ( '%s' = host OR '%s' LIKE host) AND (anydb = '1' OR '%s' = '' OR LOWER('%s') LIKE LOWER(db))" + " LIMIT 1"; + /** Query that only checks if there's a matching user */ static const char mysqlauth_skip_auth_query[] = "SELECT password FROM " MYSQLAUTH_USERS_TABLE_NAME @@ -106,10 +112,11 @@ static int db_flags = SQLITE_OPEN_READWRITE | typedef struct mysql_auth { - sqlite3 *handle; /**< SQLite3 database handle */ - char *cache_dir; /**< Custom cache directory location */ - bool inject_service_user; /**< Inject the service user into the list of users */ - bool skip_auth; /**< Authentication will always be successful */ + sqlite3 *handle; /**< SQLite3 database handle */ + char *cache_dir; /**< Custom cache directory location */ + bool inject_service_user; /**< Inject the service user into the list of users */ + bool skip_auth; /**< Authentication will always be successful */ + bool lower_case_table_names; /**< Disable database case-sensitivity */ } MYSQL_AUTH; /** Common structure for both backend and client authenticators */ From 1d211ecee249f75da9f1b7a442863d1e07680d88 Mon Sep 17 00:00:00 2001 From: Esa Korhonen Date: Thu, 4 Jan 2018 15:58:07 +0200 Subject: [PATCH 31/47] MXS-1511: QLA-Filter replace newlines in SQL-queries with custom strings The config parameter 'newline_replacement' (defaults to 1 space " ") now defines what to write to the log file when the sql-query has a newline sequence (\n, \r or \r\n). If 'newline_replacement' is the empty string "", no replacing is done and newlines are printed to file. Also, adds the config parameter 'separator', which defines the string printed between elements. Default value is ",". --- server/modules/filter/qlafilter/qlafilter.cc | 317 +++++++++---------- 1 file changed, 150 insertions(+), 167 deletions(-) diff --git a/server/modules/filter/qlafilter/qlafilter.cc b/server/modules/filter/qlafilter/qlafilter.cc index 3125b6728..c576824c4 100644 --- a/server/modules/filter/qlafilter/qlafilter.cc +++ b/server/modules/filter/qlafilter/qlafilter.cc @@ -107,9 +107,9 @@ typedef struct char *unified_filename; /* Filename of the unified log file */ bool flush_writes; /* Flush log file after every write? */ bool append; /* Open files in append-mode? */ - - /* Avoid repeatedly printing some errors/warnings. */ - bool write_warning_given; + char *query_newline; /* Character(s) used to replace a newline within a query */ + char *separator; /* Character(s) used to separate elements */ + bool write_warning_given; /* Avoid repeatedly printing some errors/warnings. */ } QLA_INSTANCE; /** @@ -156,8 +156,8 @@ typedef struct LOG_EVENT_DATA event_data; /* Information about the latest event, required if logging execution time. */ } QLA_SESSION; -static FILE* open_log_file(uint32_t, QLA_INSTANCE *, const char *); -static int write_log_entry(uint32_t, FILE*, QLA_INSTANCE*, QLA_SESSION*, +static FILE* open_log_file(QLA_INSTANCE *, uint32_t, const char *); +static int write_log_entry(FILE*, QLA_INSTANCE*, QLA_SESSION*, uint32_t, const char*, const char*, size_t, int); static bool cb_log(const MODULECMD_ARG *argv, json_t** output); @@ -197,6 +197,8 @@ static const char PARAM_LOG_TYPE[] = "log_type"; static const char PARAM_LOG_DATA[] = "log_data"; static const char PARAM_FLUSH[] = "flush"; static const char PARAM_APPEND[] = "append"; +static const char PARAM_NEWLINE[] = "newline_replacement"; +static const char PARAM_SEPARATOR[] = "separator"; MXS_BEGIN_DECLS @@ -300,6 +302,18 @@ MXS_MODULE* MXS_CREATE_MODULE() MXS_MODULE_OPT_NONE, log_data_values }, + { + PARAM_NEWLINE, + MXS_MODULE_PARAM_QUOTEDSTRING, + " ", + MXS_MODULE_OPT_NONE + }, + { + PARAM_SEPARATOR, + MXS_MODULE_PARAM_QUOTEDSTRING, + ",", + MXS_MODULE_OPT_NONE + }, { PARAM_FLUSH, MXS_MODULE_PARAM_BOOL, @@ -350,6 +364,8 @@ createInstance(const char *name, char **options, MXS_CONFIG_PARAMETER *params) my_instance->flush_writes = config_get_bool(params, PARAM_FLUSH); my_instance->log_file_data_flags = config_get_enum(params, PARAM_LOG_DATA, log_data_values); my_instance->log_mode_flags = config_get_enum(params, PARAM_LOG_TYPE, log_type_values); + my_instance->query_newline = config_copy_string(params, PARAM_NEWLINE); + my_instance->separator = config_copy_string(params, PARAM_SEPARATOR); my_instance->match = config_copy_string(params, PARAM_MATCH); my_instance->exclude = config_copy_string(params, PARAM_EXCLUDE); @@ -378,8 +394,7 @@ createInstance(const char *name, char **options, MXS_CONFIG_PARAMETER *params) { snprintf(filename, namelen, "%s.unified", my_instance->filebase); // Open the file. It is only closed at program exit - my_instance->unified_fp = open_log_file(my_instance->log_file_data_flags, - my_instance, filename); + my_instance->unified_fp = open_log_file(my_instance, my_instance->log_file_data_flags, filename); if (my_instance->unified_fp == NULL) { @@ -413,6 +428,8 @@ createInstance(const char *name, char **options, MXS_CONFIG_PARAMETER *params) MXS_FREE(my_instance->filebase); MXS_FREE(my_instance->source); MXS_FREE(my_instance->user_name); + MXS_FREE(my_instance->query_newline); + MXS_FREE(my_instance->separator); MXS_FREE(my_instance); my_instance = NULL; } @@ -485,7 +502,7 @@ newSession(MXS_FILTER *instance, MXS_SESSION *session) { uint32_t data_flags = (my_instance->log_file_data_flags & ~LOG_DATA_SESSION); // No point printing "Session" - my_session->fp = open_log_file(data_flags, my_instance, my_session->filename); + my_session->fp = open_log_file(my_instance, data_flags, my_session->filename); if (my_session->fp == NULL) { @@ -574,22 +591,21 @@ setUpstream(MXS_FILTER *instance, MXS_FILTER_SESSION *session, MXS_UPSTREAM *ups * * @param my_instance Filter instance * @param my_session Filter session + * @param date_string Date string * @param query Query string, not 0-terminated * @param querylen Query string length - * @param date_string Date string * @param elapsed_ms Query execution time, in milliseconds */ void write_log_entries(QLA_INSTANCE* my_instance, QLA_SESSION* my_session, - const char* query, int querylen, const char* date_string, int elapsed_ms) + const char* date_string, const char* query, int querylen, int elapsed_ms) { bool write_error = false; if (my_instance->log_mode_flags & CONFIG_FILE_SESSION) { // In this case there is no need to write the session // number into the files. - uint32_t data_flags = (my_instance->log_file_data_flags & - ~LOG_DATA_SESSION); - if (write_log_entry(data_flags, my_session->fp, my_instance, my_session, + uint32_t data_flags = (my_instance->log_file_data_flags & ~LOG_DATA_SESSION); + if (write_log_entry(my_session->fp, my_instance, my_session, data_flags, date_string, query, querylen, elapsed_ms) < 0) { write_error = true; @@ -598,7 +614,7 @@ void write_log_entries(QLA_INSTANCE* my_instance, QLA_SESSION* my_session, if (my_instance->log_mode_flags & CONFIG_FILE_UNIFIED) { uint32_t data_flags = my_instance->log_file_data_flags; - if (write_log_entry(data_flags, my_instance->unified_fp, my_instance, my_session, + if (write_log_entry(my_instance->unified_fp, my_instance, my_session, data_flags, date_string, query, querylen, elapsed_ms) < 0) { write_error = true; @@ -667,7 +683,7 @@ routeQuery(MXS_FILTER *instance, MXS_FILTER_SESSION *session, GWBUF *queue) else { // If execution times are not logged, write the log entry now. - write_log_entries(my_instance, my_session, query, query_len, event.query_date, -1); + write_log_entries(my_instance, my_session, event.query_date, query, query_len, -1); } } /* Pass the query downstream */ @@ -703,7 +719,7 @@ clientReply(MXS_FILTER *instance, MXS_FILTER_SESSION *session, GWBUF *queue) // Calculate elapsed time in milliseconds. double elapsed_ms = 1E3 * (now.tv_sec - event.begin_time.tv_sec) + (now.tv_nsec - event.begin_time.tv_nsec) / (double)1E6; - write_log_entries(my_instance, my_session, query, query_len, event.query_date, + write_log_entries(my_instance, my_session, event.query_date, query, query_len, std::floor(elapsed_ms + 0.5)); clear(event); } @@ -811,12 +827,13 @@ static uint64_t getCapabilities(MXS_FILTER* instance) /** * Open the log file and print a header if appropriate. - * @param data_flags Data save settings flags + * * @param instance The filter instance + * @param data_flags Data save settings flags * @param filename Target file path * @return A valid file on success, null otherwise. */ -static FILE* open_log_file(uint32_t data_flags, QLA_INSTANCE *instance, const char *filename) +static FILE* open_log_file(QLA_INSTANCE *instance, uint32_t data_flags, const char *filename) { bool file_existed = false; FILE *fp = NULL; @@ -843,68 +860,56 @@ static FILE* open_log_file(uint32_t data_flags, QLA_INSTANCE *instance, const ch } } } - if (fp && !file_existed) + + if (fp && !file_existed && data_flags != 0) { - // Print a header. Luckily, we know the header has limited length - const char SERVICE[] = "Service,"; - const char SESSION[] = "Session,"; - const char DATE[] = "Date,"; - const char USERHOST[] = "User@Host,"; - const char QUERY[] = "Query,"; - const char REPLY_TIME[] = "Reply_time,"; - const int headerlen = sizeof(SERVICE) + sizeof(SERVICE) + sizeof(DATE) + - sizeof(USERHOST) + sizeof(QUERY) + sizeof(REPLY_TIME); + // Print a header. + const char SERVICE[] = "Service"; + const char SESSION[] = "Session"; + const char DATE[] = "Date"; + const char USERHOST[] = "User@Host"; + const char QUERY[] = "Query"; + const char REPLY_TIME[] = "Reply_time"; - char print_str[headerlen]; - memset(print_str, '\0', headerlen); + std::stringstream header; + const char* curr_sep = ""; // Use empty string as the first separator + const char* real_sep = instance->separator; - char *current_pos = print_str; - if (instance->log_file_data_flags & LOG_DATA_SERVICE) + if (data_flags & LOG_DATA_SERVICE) { - strcat(current_pos, SERVICE); - current_pos += sizeof(SERVICE) - 1; + header << SERVICE; + curr_sep = real_sep; } - if (instance->log_file_data_flags & LOG_DATA_SESSION) + if (data_flags & LOG_DATA_SESSION) { - strcat(current_pos, SESSION); - current_pos += sizeof(SERVICE) - 1; + header << curr_sep << SESSION; + curr_sep = real_sep; } - if (instance->log_file_data_flags & LOG_DATA_DATE) + if (data_flags & LOG_DATA_DATE) { - strcat(current_pos, DATE); - current_pos += sizeof(DATE) - 1; + header << curr_sep << DATE; + curr_sep = real_sep; } - if (instance->log_file_data_flags & LOG_DATA_USER) + if (data_flags & LOG_DATA_USER) { - strcat(current_pos, USERHOST); - current_pos += sizeof(USERHOST) - 1; + header << curr_sep << USERHOST; + curr_sep = real_sep; } - if (instance->log_file_data_flags & LOG_DATA_REPLY_TIME) + if (data_flags & LOG_DATA_REPLY_TIME) { - strcat(current_pos, REPLY_TIME); - current_pos += sizeof(REPLY_TIME) - 1; + header << curr_sep << REPLY_TIME; + curr_sep = real_sep; } - if (instance->log_file_data_flags & LOG_DATA_QUERY) + if (data_flags & LOG_DATA_QUERY) { - strcat(current_pos, QUERY); - current_pos += sizeof(QUERY) - 1; - } - if (current_pos > print_str) - { - // Overwrite the last ','. - *(current_pos - 1) = '\n'; - } - else - { - // Nothing to print - return fp; + header << curr_sep << QUERY; } + header << '\n'; // Finally, write the log header. - int written = fprintf(fp, "%s", print_str); + int written = fprintf(fp, "%s", header.str().c_str()); - if ((written <= 0) || - ((instance->flush_writes) && (fflush(fp) < 0))) + if ((written <= 0) || ((instance->flush_writes) && (fflush(fp) < 0))) { // Weird error, file opened but a write failed. Best to stop. fclose(fp); @@ -915,151 +920,129 @@ static FILE* open_log_file(uint32_t data_flags, QLA_INSTANCE *instance, const ch return fp; } +static void print_string_replace_newlines(const char *sql_string, + size_t sql_str_len, const char* rep_newline, + std::stringstream* output) +{ + ss_dassert(output); + size_t line_begin = 0; + size_t search_pos = 0; + while (search_pos < sql_str_len) + { + int line_end_chars = 0; + // A newline is either \r\n, \n or \r + if (sql_string[search_pos] == '\r') + { + if (search_pos + 1 < sql_str_len && sql_string[search_pos + 1] == '\n') + { + // Got \r\n + line_end_chars = 2; + } + else + { + // Just \r + line_end_chars = 1; + } + } + else if (sql_string[search_pos] == '\n') + { + // Just \n + line_end_chars = 1; + } + + if (line_end_chars > 0) + { + // Found line ending characters, write out the line excluding line end. + output->write(&sql_string[line_begin], search_pos - line_begin); + *output << rep_newline; + // Next line begins after line end chars + line_begin = search_pos + line_end_chars; + // For \r\n, advance search_pos + search_pos += line_end_chars - 1; + } + + search_pos++; + } + + // Print anything left + if (line_begin < sql_str_len) + { + output->write(&sql_string[line_begin], sql_str_len - line_begin); + } +} + /** * Write an entry to the log file. * - * @param data_flags Controls what to write * @param logfile Target file * @param instance Filter instance * @param session Filter session + * @param data_flags Controls what to write * @param time_string Date entry * @param sql_string SQL-query, *not* NULL terminated * @param sql_str_len Length of SQL-string * @param elapsed_ms Query execution time, in milliseconds * @return The number of characters written, or a negative value on failure */ -static int write_log_entry(uint32_t data_flags, FILE *logfile, QLA_INSTANCE *instance, - QLA_SESSION *session, const char *time_string, const char *sql_string, - size_t sql_str_len, int elapsed_ms) +static int write_log_entry(FILE *logfile, QLA_INSTANCE *instance, QLA_SESSION *session, uint32_t data_flags, + const char *time_string, const char *sql_string, size_t sql_str_len, + int elapsed_ms) { ss_dassert(logfile != NULL); - size_t print_len = 0; - /** - * First calculate an upper limit for the total length. The strlen()-calls - * could be removed if the values would be saved into the instance or session - * or if we had some reasonable max lengths. (Apparently there are max lengths - * but they are much higher than what is typically needed.) - */ + if (data_flags == 0) + { + // Nothing to print + return 0; + } + + /* Printing to the file in parts would likely cause garbled printing if several threads write + * simultaneously, so we have to first print to a string. */ + std::stringstream output; + const char* curr_sep = ""; // Use empty string as the first separator + const char* real_sep = instance->separator; - // The numbers have some extra for delimiters. - const size_t integer_chars = 20; // Enough space for any integer type if (data_flags & LOG_DATA_SERVICE) { - print_len += strlen(session->service) + 1; + output << session->service; + curr_sep = real_sep; } if (data_flags & LOG_DATA_SESSION) { - print_len += integer_chars; + output << curr_sep << session->ses_id; + curr_sep = real_sep; } if (data_flags & LOG_DATA_DATE) { - print_len += QLA_DATE_BUFFER_SIZE + 1; + output << curr_sep << time_string; + curr_sep = real_sep; } if (data_flags & LOG_DATA_USER) { - print_len += strlen(session->user) + strlen(session->remote) + 2; + output << curr_sep << session->user << "@" << session->remote; + curr_sep = real_sep; } if (data_flags & LOG_DATA_REPLY_TIME) { - print_len += integer_chars; + output << curr_sep << elapsed_ms; + curr_sep = real_sep; } if (data_flags & LOG_DATA_QUERY) { - print_len += sql_str_len + 1; // Can't use strlen, not null-terminated - } - - if (print_len == 0) - { - return 0; // Nothing to print - } - - /* Allocate space for a buffer. Printing to the file in parts would likely - cause garbled printing if several threads write simultaneously, so we - have to first print to a string. */ - char *print_str = NULL; - if ((print_str = (char*)MXS_CALLOC(print_len, sizeof(char))) == NULL) - { - return -1; - } - - bool error = false; - char *current_pos = print_str; - int rval = 0; - if (!error && (data_flags & LOG_DATA_SERVICE)) - { - if ((rval = sprintf(current_pos, "%s,", session->service)) < 0) + output << curr_sep; + if (*instance->query_newline) { - error = true; + print_string_replace_newlines(sql_string, sql_str_len, instance->query_newline, &output); } else { - current_pos += rval; + // The newline replacement is an empty string so print the query as is + output.write(sql_string, sql_str_len); // non-null-terminated string } } - if (!error && (data_flags & LOG_DATA_SESSION)) - { - if ((rval = sprintf(current_pos, "%lu,", session->ses_id)) < 0) - { - error = true; - } - else - { - current_pos += rval; - } - } - if (!error && (data_flags & LOG_DATA_DATE)) - { - if ((rval = sprintf(current_pos, "%s,", time_string)) < 0) - { - error = true; - } - else - { - current_pos += rval; - } - } - if (!error && (data_flags & LOG_DATA_USER)) - { - if ((rval = sprintf(current_pos, "%s@%s,", session->user, session->remote)) < 0) - { - error = true; - } - else - { - current_pos += rval; - } - } - if (!error && (data_flags & LOG_DATA_REPLY_TIME)) - { - if ((rval = sprintf(current_pos, "%d,", elapsed_ms)) < 0) - { - error = true; - } - else - { - current_pos += rval; - } - } - if (!error && (data_flags & LOG_DATA_QUERY)) - { - strncat(current_pos, sql_string, sql_str_len); // non-null-terminated string - current_pos += sql_str_len + 1; // +1 to move to the next char after - } - if (error || current_pos <= print_str) - { - MXS_FREE(print_str); - MXS_ERROR("qlafilter ('%s'): Failed to format log event.", instance->name); - return -1; - } - else - { - // Overwrite the last ','. The rest is already filled with 0. - *(current_pos - 1) = '\n'; - } + output << "\n"; // Finally, write the log event. - int written = fprintf(logfile, "%s", print_str); - MXS_FREE(print_str); + int written = fprintf(logfile, "%s", output.str().c_str()); if ((!instance->flush_writes) || (written <= 0)) { From 71f74ea0d264e1f3d92c2b88f3e8a72b67d13660 Mon Sep 17 00:00:00 2001 From: Esa Korhonen Date: Tue, 16 Jan 2018 12:20:18 +0200 Subject: [PATCH 32/47] Add warning about SUPER-users to Switchover Limitations documentation --- Documentation/Monitors/MariaDB-Monitor.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Documentation/Monitors/MariaDB-Monitor.md b/Documentation/Monitors/MariaDB-Monitor.md index e0cc8134d..cd601ecf7 100644 --- a/Documentation/Monitors/MariaDB-Monitor.md +++ b/Documentation/Monitors/MariaDB-Monitor.md @@ -277,6 +277,18 @@ The backends must all use GTID-based replication, and the domain id should not change during a switchover or failover. Master and slaves must have well-behaving GTIDs with no extra events on slave servers. +Switchover requires that the cluster is "frozen" for the duration of the +operation. This means that no data modifying statements such as INSERT or UPDATE +are executed and the GTID position of the master server is stable. When +switchover begins, the monitor sets the global *read_only* flag on the old +master backend to stop any updates. *read_only* does not affect users with the +SUPER-privilege so any such user can issue writes during a switchover. These +writes have a high chance to break replication, because the write may not be +replicated to all slaves before they switch to the new master. To prevent this, +any users who commonly do updates should not have the SUPER-privilege. For even +more security, the only SUPER-user session during a switchover should be the +MaxScale monitor user. + ### Configuration parameters #### `auto_failover` From dbbeeac145b8e8535b10c18107415e03b7bd0553 Mon Sep 17 00:00:00 2001 From: Johan Wikman Date: Tue, 16 Jan 2018 13:56:29 +0200 Subject: [PATCH 33/47] MXS-1596 Use non-privileged user for client The pre-existing users in the MaxScale test environment have the super privilege so they are not affected by the database being set in read-only mode. Consequently, a custom user without the super privilege must be used by the client threads. --- .../mysqlmon_switchover_stress.cpp | 97 ++++++++++++++----- 1 file changed, 74 insertions(+), 23 deletions(-) diff --git a/maxscale-system-test/mysqlmon_switchover_stress.cpp b/maxscale-system-test/mysqlmon_switchover_stress.cpp index 1242cbf4e..209a0a8e8 100644 --- a/maxscale-system-test/mysqlmon_switchover_stress.cpp +++ b/maxscale-system-test/mysqlmon_switchover_stress.cpp @@ -33,6 +33,9 @@ const time_t SWITCHOVER_DURATION = 5; // How long should we keep in running. const time_t TEST_DURATION = 90; +const char* CLIENT_USER = "mysqlmon_switchover_stress"; +const char* CLIENT_PASSWORD = "mysqlmon_switchover_stress"; + #define CMESSAGE(msg) \ do {\ stringstream ss;\ @@ -478,11 +481,17 @@ void check_server_statuses(TestConnections& test) int get_next_master_id(TestConnections& test, int current_id) { - int next_id; + int next_id = current_id; do { - next_id = (current_id + 1) % 5; + next_id = (next_id + 1) % 5; + if (next_id == 0) + { + next_id = 1; + } + ss_dassert(next_id >= 1); + ss_dassert(next_id <= 4); string server("server"); server += std::to_string(next_id); StringSet states = test.get_server_status(server.c_str()); @@ -496,6 +505,58 @@ int get_next_master_id(TestConnections& test, int current_id) return next_id != current_id ? next_id : -1; } +void create_client_user(TestConnections& test) +{ + string stmt; + + // Drop user + stmt = "DROP USER IF EXISTS "; + stmt += "'"; + stmt += CLIENT_USER; + stmt += "'@'%%'"; + test.try_query(test.maxscales->conn_rwsplit[0], stmt.c_str()); + + // Create user + stmt = "CREATE USER "; + stmt += "'"; + stmt += CLIENT_USER; + stmt += "'@'%%'"; + stmt += " IDENTIFIED BY "; + stmt += "'"; + stmt += CLIENT_PASSWORD; + stmt += "'"; + test.try_query(test.maxscales->conn_rwsplit[0], stmt.c_str()); + + // Grant access + stmt = "GRANT SELECT, INSERT, UPDATE ON *.* TO "; + stmt += "'"; + stmt += CLIENT_USER; + stmt += "'@'%%'"; + test.try_query(test.maxscales->conn_rwsplit[0], stmt.c_str()); + + test.try_query(test.maxscales->conn_rwsplit[0], "FLUSH PRIVILEGES"); +} + +void switchover(TestConnections& test, int next_master_id, int current_master_id) +{ + cout << "\nTrying to do manual switchover from server" << current_master_id + << " to server" << next_master_id << endl; + + string command("call command mysqlmon switchover MySQL-Monitor "); + command += "server"; + command += std::to_string(next_master_id); + command += " "; + command += "server"; + command += std::to_string(current_master_id); + + cout << "\nCommand: " << command << endl; + + test.maxscales->execute_maxadmin_command_print(0, (char*)command.c_str()); + + sleep(1); + list_servers(test); +} + void run(TestConnections& test) { int n_threads = Client::DEFAULT_N_CLIENTS; @@ -503,14 +564,16 @@ void run(TestConnections& test) cout << "\nConnecting to MaxScale." << endl; test.maxscales->connect_maxscale(); + create_client_user(test); + Client::init(test, Client::DEFAULT_N_CLIENTS, Client::DEFAULT_N_ROWS); if (test.ok()) { const char* zHost = test.maxscales->IP[0]; int port = test.maxscales->rwsplit_port[0]; - const char* zUser = test.maxscales->user_name; - const char* zPassword = test.maxscales->password; + const char* zUser = CLIENT_USER; + const char* zPassword = CLIENT_PASSWORD; cout << "Connecting to " << zHost << ":" << port << " as " << zUser << ":" << zPassword << endl; cout << "Starting clients." << endl; @@ -530,27 +593,11 @@ void run(TestConnections& test) if (next_master_id != -1) { - cout << "\nTrying to do manual switchover from server" << current_master_id - << " to server" << next_master_id << endl; - - string command("call command mysqlmon switchover MySQL-Monitor "); - command += "server"; - command += std::to_string(next_master_id); - command += " "; - command += "server"; - command += std::to_string(current_master_id); - - cout << "\nCommand: " << command << endl; - - test.maxscales->execute_maxadmin_command_print(0, (char*)command.c_str()); - - sleep(1); - list_servers(test); + switchover(test, next_master_id, current_master_id); + current_master_id = next_master_id; sleep(SWITCHOVER_DURATION); - current_master_id = next_master_id; - int master_id = get_master_server_id(test); if (master_id < 0) @@ -574,7 +621,11 @@ void run(TestConnections& test) cout << "\nStopping clients.\n" << flush; Client::stop(); - sleep(SWITCHOVER_DURATION); + // Ensure master is at server1. Shortens startup time for next test. + if (current_master_id != 1) + { + switchover(test, 1, current_master_id); + } test.repl->close_connections(); test.repl->connect(); From 23f2c3b9806460b8f6fd22ae1aa10a6c6567515c Mon Sep 17 00:00:00 2001 From: Esa Korhonen Date: Fri, 12 Jan 2018 13:11:21 +0200 Subject: [PATCH 34/47] Better failover timing and redirection success is tested Works similar to switchover. --- .../modules/monitor/mariadbmon/mysql_mon.cc | 117 +++++++++++++----- 1 file changed, 88 insertions(+), 29 deletions(-) diff --git a/server/modules/monitor/mariadbmon/mysql_mon.cc b/server/modules/monitor/mariadbmon/mysql_mon.cc index abc3796df..df32155f7 100644 --- a/server/modules/monitor/mariadbmon/mysql_mon.cc +++ b/server/modules/monitor/mariadbmon/mysql_mon.cc @@ -118,6 +118,9 @@ static bool cluster_can_be_joined(MYSQL_MONITOR* mon); static bool can_replicate_from(MYSQL_MONITOR* mon, MXS_MONITORED_SERVER* slave, MySqlServerInfo* slave_info, MXS_MONITORED_SERVER* master, MySqlServerInfo* master_info); +static bool wait_cluster_stabilization(MYSQL_MONITOR* mon, MXS_MONITORED_SERVER* new_master, + const ServerVector& slaves, int seconds_remaining); +static string get_connection_errors(const ServerVector& servers); static bool report_version_err = true; static const char* hb_table_name = "maxscale_schema.replication_heartbeat"; @@ -3446,10 +3449,12 @@ MXS_MONITORED_SERVER* select_new_master(MYSQL_MONITOR* mon, * * @param mon The monitor * @param new_master The new master + * @param seconds_remaining How much time left * @param err_out Json error output * @return True if relay log was processed within time limit, or false if time ran out or an error occurred. */ -bool failover_wait_relay_log(MYSQL_MONITOR* mon, MXS_MONITORED_SERVER* new_master, json_t** err_out) +bool failover_wait_relay_log(MYSQL_MONITOR* mon, MXS_MONITORED_SERVER* new_master, int seconds_remaining, + json_t** err_out) { MySqlServerInfo* master_info = get_server_info(mon, new_master); time_t begin = time(NULL); @@ -3458,7 +3463,7 @@ bool failover_wait_relay_log(MYSQL_MONITOR* mon, MXS_MONITORED_SERVER* new_maste while (master_info->relay_log_events() > 0 && query_ok && io_pos_stable && - difftime(time(NULL), begin) < mon->failover_timeout) + difftime(time(NULL), begin) < seconds_remaining) { MXS_INFO("Relay log of server '%s' not yet empty, waiting to clear %" PRId64 " events.", new_master->server->unique_name, master_info->relay_log_events()); @@ -3608,6 +3613,36 @@ int redirect_slaves(MYSQL_MONITOR* mon, MXS_MONITORED_SERVER* new_master, const return successes; } +/** + * Print a redirect error to logs. If err_out exists, generate a combined error message by querying all + * the server parameters for connection errors and append these errors to err_out. + * + * @param demotion_target If not NULL, this is the first server to query. + * @param redirectable_slaves Other servers to query for errors. + * @param err_out If not null, the error output object. + */ +void print_redirect_errors(MXS_MONITORED_SERVER* first_server, const ServerVector& servers, + json_t** err_out) +{ + // Individual server errors have already been printed to the log. + // For JSON, gather the errors again. + const char MSG[] = "Could not redirect any slaves to the new master."; + MXS_ERROR(MSG); + if (err_out) + { + ServerVector failed_slaves; + if (first_server) + { + failed_slaves.push_back(first_server); + } + failed_slaves.insert(failed_slaves.end(), + servers.begin(), servers.end()); + string combined_error = get_connection_errors(failed_slaves); + *err_out = mxs_json_error_append(*err_out, + "%s Errors: %s.", MSG, combined_error.c_str()); + } +} + /** * Performs failover for a simple topology (1 master, N slaves, no intermediate masters). * @@ -3623,23 +3658,61 @@ static bool do_failover(MYSQL_MONITOR* mon, json_t** err_out) PRINT_MXS_JSON_ERROR(err_out, "Cluster gtid domain is unknown. Cannot failover."); return false; } + // Total time limit on how long this operation may take. Checked and modified after significant steps are + // completed. + int seconds_remaining = mon->failover_timeout; + time_t start_time = time(NULL); // Step 1: Select new master. Also populate a vector with all slaves not the selected master. - ServerVector slaves; - MXS_MONITORED_SERVER* new_master = select_new_master(mon, &slaves, err_out); + ServerVector redirectable_slaves; + MXS_MONITORED_SERVER* new_master = select_new_master(mon, &redirectable_slaves, err_out); if (new_master == NULL) { return false; } + time_t step1_time = time(NULL); + seconds_remaining -= difftime(step1_time, start_time); + bool rval = false; // Step 2: Wait until relay log consumed. - if (failover_wait_relay_log(mon, new_master, err_out) && - // Step 3: Stop and reset slave, set read-only to 0. - promote_new_master(new_master, err_out)) + if (failover_wait_relay_log(mon, new_master, seconds_remaining, err_out)) { - // Step 4: Redirect slaves. - int redirects = redirect_slaves(mon, new_master, slaves); - rval = slaves.empty() ? true : redirects > 0; + time_t step2_time = time(NULL); + int seconds_step2 = difftime(step2_time, step1_time); + MXS_DEBUG("Failover: relay log processing took %d seconds.", seconds_step2); + seconds_remaining -= seconds_step2; + + // Step 3: Stop and reset slave, set read-only to 0. + if (promote_new_master(new_master, err_out)) + { + // Step 4: Redirect slaves. + ServerVector redirected_slaves; + int redirects = redirect_slaves(mon, new_master, redirectable_slaves, &redirected_slaves); + bool success = redirectable_slaves.empty() ? true : redirects > 0; + if (success) + { + time_t step4_time = time(NULL); + seconds_remaining -= difftime(step4_time, step2_time); + + // Step 5: Finally, add an event to the new master to advance gtid and wait for the slaves + // to receive it. seconds_remaining can be 0 or less at this point. Even in such a case + // wait_cluster_stabilization() may succeed if replication is fast enough. + if (wait_cluster_stabilization(mon, new_master, redirected_slaves, seconds_remaining)) + { + rval = true; + time_t step5_time = time(NULL); + int seconds_step5 = difftime(step5_time, step4_time); + seconds_remaining -= seconds_step5; + MXS_DEBUG("Failover: slave replication confirmation took %d seconds with " + "%d seconds to spare.", seconds_step5, seconds_remaining); + } + } + else + { + print_redirect_errors(NULL, redirectable_slaves, err_out); + } + } } + return rval; } @@ -4141,25 +4214,7 @@ static bool do_switchover(MYSQL_MONITOR* mon, MXS_MONITORED_SERVER* current_mast redirectable_slaves, &redirected_slaves); bool success = redirectable_slaves.empty() ? start_ok : start_ok || redirects > 0; - if (success == false) - { - rval = false; - // This is a special case. Individual server errors have already been printed to the log. - // For JSON, gather the errors again. - const char MSG[] = "Could not redirect any slaves to the new master."; - MXS_ERROR(MSG); - if (err_out) - { - ServerVector failed_slaves; - failed_slaves.push_back(demotion_target); - failed_slaves.insert(failed_slaves.end(), - redirectable_slaves.begin(), redirectable_slaves.end()); - string combined_error = get_connection_errors(failed_slaves); - *err_out = mxs_json_error_append(*err_out, - "%s Errors: %s.", MSG, combined_error.c_str()); - } - } - else + if (success) { time_t step5_time = time(NULL); seconds_remaining -= difftime(step5_time, step3_time); @@ -4177,6 +4232,10 @@ static bool do_switchover(MYSQL_MONITOR* mon, MXS_MONITORED_SERVER* current_mast "%d seconds to spare.", seconds_step6, seconds_remaining); } } + else + { + print_redirect_errors(demotion_target, redirectable_slaves, err_out); + } } } From c4df28f64ac6df42a8b2b11ccaa0d716581b59e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Wed, 17 Jan 2018 09:56:50 +0200 Subject: [PATCH 35/47] MXS-1416: Skip directory creation with --config-check The log and data directories aren't created or checked when the --config-check option is given. --- server/core/config.c | 1 - server/core/gateway.cc | 60 ++++++++++++++++++++---------------------- 2 files changed, 28 insertions(+), 33 deletions(-) diff --git a/server/core/config.c b/server/core/config.c index a15c20e4c..19132f2c8 100644 --- a/server/core/config.c +++ b/server/core/config.c @@ -1624,7 +1624,6 @@ global_defaults() { uint8_t mac_addr[6] = ""; struct utsname uname_data; - gateway.config_check = false; gateway.n_threads = DEFAULT_NTHREADS; gateway.n_nbpoll = DEFAULT_NBPOLLS; gateway.pollsleep = DEFAULT_POLLSLEEP; diff --git a/server/core/gateway.cc b/server/core/gateway.cc index c9e006124..d7d726711 100644 --- a/server/core/gateway.cc +++ b/server/core/gateway.cc @@ -1338,8 +1338,6 @@ int main(int argc, char **argv) int rc = MAXSCALE_SHUTDOWN; int l; int i; - int n; - int ini_rval; intptr_t thread_id; int n_threads; /*< number of epoll listener threads */ int n_services; @@ -1353,24 +1351,24 @@ int main(int argc, char **argv) char* cnf_file_arg = NULL; /*< conf filename from cmd-line arg */ THREAD log_flush_thr; char* tmp_path; - char* tmp_var; int option_index; - int *syslog_enabled = &config_get_global_options()->syslog; /** Log to syslog */ - int *maxlog_enabled = &config_get_global_options()->maxlog; /** Log with MaxScale */ - int *log_to_shm = &config_get_global_options()->log_to_shm; /** Log to shared memory */ + MXS_CONFIG* cnf = config_get_global_options(); + ss_dassert(cnf); + int *syslog_enabled = &cnf->syslog; /** Log to syslog */ + int *maxlog_enabled = &cnf->maxlog; /** Log with MaxScale */ + int *log_to_shm = &cnf->log_to_shm; /** Log to shared memory */ ssize_t log_flush_timeout_ms = 0; sigset_t sigpipe_mask; sigset_t saved_mask; - bool config_check = false; bool to_stdout = false; void (*exitfunp[4])(void) = { mxs_log_finish, cleanup_process_datadir, write_footer, NULL }; - MXS_CONFIG* cnf = NULL; - int numlocks = 0; bool pid_file_created = false; + // NOTE: These are set here since global_defaults() is called inside config_load(). *syslog_enabled = 1; *maxlog_enabled = 1; *log_to_shm = 0; + cnf->config_check = false; maxscale_reset_starttime(); @@ -1657,7 +1655,7 @@ int main(int argc, char **argv) goto return_main; case 'c': - config_check = true; + cnf->config_check = true; break; default: @@ -1673,7 +1671,7 @@ int main(int argc, char **argv) } } - if (config_check) + if (cnf->config_check) { daemon_mode = false; to_stdout = true; @@ -1838,7 +1836,7 @@ int main(int argc, char **argv) { bool succp; - if (mkdir(get_logdir(), 0777) != 0 && errno != EEXIST) + if (!cnf->config_check && mkdir(get_logdir(), 0777) != 0 && errno != EEXIST) { fprintf(stderr, "Error: Cannot create log directory: %s\n", @@ -1885,20 +1883,23 @@ int main(int argc, char **argv) MXS_NOTICE("Commit: %s", MAXSCALE_COMMIT); #endif - /* - * Set the data directory. We use a unique directory name to avoid conflicts - * if multiple instances of MaxScale are being run on the same machine. - */ - if (create_datadir(get_datadir(), datadir)) + if (!cnf->config_check) { - set_process_datadir(datadir); - } - else - { - char errbuf[MXS_STRERROR_BUFLEN]; - MXS_ERROR("Cannot create data directory '%s': %d %s\n", - datadir, errno, strerror_r(errno, errbuf, sizeof(errbuf))); - goto return_main; + /* + * Set the data directory. We use a unique directory name to avoid conflicts + * if multiple instances of MaxScale are being run on the same machine. + */ + if (create_datadir(get_datadir(), datadir)) + { + set_process_datadir(datadir); + } + else + { + char errbuf[MXS_STRERROR_BUFLEN]; + MXS_ERROR("Cannot create data directory '%s': %d %s\n", + datadir, errno, strerror_r(errno, errbuf, sizeof(errbuf))); + goto return_main; + } } if (!daemon_mode) @@ -1940,9 +1941,6 @@ int main(int argc, char **argv) goto return_main; } - cnf = config_get_global_options(); - ss_dassert(cnf); - if (!qc_setup(cnf->qc_name, cnf->qc_args)) { const char* logerr = "Failed to initialise query classifier library."; @@ -1951,9 +1949,7 @@ int main(int argc, char **argv) goto return_main; } - cnf->config_check = config_check; - - if (!config_check) + if (!cnf->config_check) { /** Check if a MaxScale process is already running */ if (pid_file_exists()) @@ -2015,7 +2011,7 @@ int main(int argc, char **argv) goto return_main; } - if (config_check) + if (cnf->config_check) { MXS_NOTICE("Configuration was successfully verified."); rc = MAXSCALE_SHUTDOWN; From b8e15d2beaa11e2cc2602197836904823f5c8525 Mon Sep 17 00:00:00 2001 From: Esa Korhonen Date: Tue, 16 Jan 2018 17:10:32 +0200 Subject: [PATCH 36/47] Reformat and tune QLA filter documentation No additions, just tidying up. --- Documentation/Filters/Query-Log-All-Filter.md | 97 ++++++++++--------- 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/Documentation/Filters/Query-Log-All-Filter.md b/Documentation/Filters/Query-Log-All-Filter.md index 389bf50a5..dc768805a 100644 --- a/Documentation/Filters/Query-Log-All-Filter.md +++ b/Documentation/Filters/Query-Log-All-Filter.md @@ -2,11 +2,13 @@ ## Overview -The Query Log All (QLA) filter is a filter module for MariaDB MaxScale that is able to log all query content on a per client session basis. Logs are written in a csv format file that lists the time submitted and the SQL statement text. +The Query Log All (QLA) filter logs query content. Logs are written to a file in +CSV format. Log elements are configurable and include the time submitted and the +SQL statement text, among others. ## Configuration -The configuration block for the QLA filter requires the minimal filter options in its section within the maxscale.cnf file, stored in /etc/maxscale.cnf. +A minimal configuration is below. ``` [MyLogFilter] type=filter @@ -31,72 +33,64 @@ The QLA filter accepts the following options. case | Use case-sensitive matching extended | Use extended regular expression syntax (ERE) -To use multiple filter options, list them in a comma-separated list. If no options are given, default will be used. Multiple options can be enabled simultaneously. +To use multiple filter options, list them in a comma-separated list. If no +options are given, default will be used. Multiple options can be enabled +simultaneously. ``` options=case,extended ``` -**Note**: older the version of the QLA filter in 0.7 of MariaDB MaxScale used the `options` -to define the location of the log files. This functionality is not supported -anymore and the `filebase` parameter should be used instead. +**Note**: older the version of the QLA filter in 0.7 of MariaDB MaxScale used +the `options` to define the location of the log files. This functionality is not +supported anymore and the `filebase` parameter should be used instead. ## Filter Parameters -The QLA filter has one mandatory parameter, `filebase`, and a number of optional parameters. These were introduced in the 1.0 release of MariaDB MaxScale. +The QLA filter has one mandatory parameter, `filebase`, and a number of optional +parameters. These were introduced in the 1.0 release of MariaDB MaxScale. ### `filebase` -The basename of the output file created for each session. A session index is added to the filename for each file written. This is a mandatory parameter. +The basename of the output file created for each session. A session index is +added to the filename for each written session file. For unified log files, +*.unified* is appended. This is a mandatory parameter. ``` filebase=/tmp/SqlQueryLog ``` -The filebase may also be set as the filter option, the mechanism to set the filebase via the filter option is superseded by the parameter. If both are set the parameter setting will be used and the filter option ignored. +The filebase may also be set as the filter option. If both option and parameter +are set, the parameter setting will be used and the filter option ignored. -### `match` +### `match` and `exclude` -An optional parameter that can be used to limit the queries that will be logged by the QLA filter. The parameter value is a regular expression that is used to match against the SQL text. Only SQL statements that matches the text passed as the value of this parameter will be logged. +These optional parameters limit logging on a query level. The parameter values +are regular expressions which are matched against the SQL query text. Only SQL +statements that match the regular expression in *match* but do not match the +*exclude* expression are logged. ``` match=select.*from.*customer.*where +exclude=^insert ``` -All regular expressions are evaluated with the option to ignore the case of the text, therefore a match option of select will match both select, SELECT and any form of the word with upper or lowercase characters. +*match* is checked before *exclude*. If *match* is empty, all queries are +considered matching. If *exclude* is empty, no query is exluded. If both are +empty, all queries are logged. -### `exclude` +### `user` and `source` -An optional parameter that can be used to limit the queries that will be logged by the QLA filter. The parameter value is a regular expression that is used to match against the SQL text. SQL statements that match the text passed as the value of this parameter will be excluded from the log output. - -``` -exclude=where -``` - -All regular expressions are evaluated with the option to ignore the case of the text, therefore an exclude option of select will exclude statements that contain both select, SELECT or any form of the word with upper or lowercase characters. - -### `source` - -The optional source parameter defines an address that is used to match against the address from which the client connection to MariaDB MaxScale originates. Only sessions that originate from this address will be logged. - -``` -source=127.0.0.1 -``` - -### `user` - -The optional user parameter defines a user name that is used to match against the user from which the client connection to MariaDB MaxScale originates. Only sessions that are connected using this username are logged. +These optional parameters limit logging on a session level. If `user` is +defined, only the sessions with a matching client username are logged. If +`source` is defined, only sessions with a matching client source address are +logged. ``` user=john +source=127.0.0.1 ``` ------------------------------------------------------------ - -**The following parameters were added in MaxScale 2.1.0** - ------------------------------------------------------------ - ### `log_type` The type of log file to use. The default value is _session_. @@ -110,11 +104,13 @@ The type of log file to use. The default value is _session_. log_type=session ``` +If both logs are required, define `log_type=session,unified`. + ### `log_data` -Type of data to log in the log files. Parameter value is a comma separated list -of the following values. By default the _date_, _user_ and _query_ options are -enabled. +Type of data to log in the log files. The parameter value is a comma separated +list of the following elements. By default the _date_, _user_ and _query_ +options are enabled. | Value | Description | | -------- |--------------------------------------------------| @@ -129,8 +125,9 @@ enabled. log_data=date, user, query ``` -If *reply_time* is enabled, the log entry is written when the first reply from server is received. -Otherwise, the entry is written when receiving query from client. +If *reply_time* is enabled, the log entry is written when the first reply from +server is received. Otherwise, the entry is written when receiving query from +client. ### `flush` @@ -142,7 +139,8 @@ flush=true ### `append` -Append new entries to log files instead of overwriting them. The default is false. +Append new entries to log files instead of overwriting them. The default is +false. ``` append=true @@ -152,7 +150,10 @@ append=true ### Example 1 - Query without primary key -Imagine you have observed an issue with a particular table and you want to determine if there are queries that are accessing that table but not using the primary key of the table. Let's assume the table name is PRODUCTS and the primary key is called PRODUCT_ID. Add a filter with the following definition: +Imagine you have observed an issue with a particular table and you want to +determine if there are queries that are accessing that table but not using the +primary key of the table. Let's assume the table name is PRODUCTS and the +primary key is called PRODUCT_ID. Add a filter with the following definition: ``` [ProductsSelectLogger] @@ -171,8 +172,10 @@ passwd=mypasswd filters=ProductsSelectLogger ``` -The result of then putting this filter into the service used by the application would be a log file of all select queries that mentioned the table but did not mention the PRODUCT_ID primary key in the predicates for the query. -Executing `SELECT * FROM PRODUCTS` would log the following into `/var/logs/qla/SelectProducts`: +The result of using this filter with the service used by the application would +be a log file of all select queries querying PRODUCTS without using the +PRODUCT_ID primary key in the predicates of the query. Executing `SELECT * FROM +PRODUCTS` would log the following into `/var/logs/qla/SelectProducts`: ``` 07:12:56.324 7/01/2016, SELECT * FROM PRODUCTS ``` From ae949881a9e594b6010585f63f734b291317dc3c Mon Sep 17 00:00:00 2001 From: Esa Korhonen Date: Tue, 16 Jan 2018 17:26:08 +0200 Subject: [PATCH 37/47] MXS-1511: Add documentation about new parameters --- Documentation/Filters/Query-Log-All-Filter.md | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Documentation/Filters/Query-Log-All-Filter.md b/Documentation/Filters/Query-Log-All-Filter.md index dc768805a..4b27e7516 100644 --- a/Documentation/Filters/Query-Log-All-Filter.md +++ b/Documentation/Filters/Query-Log-All-Filter.md @@ -146,6 +146,27 @@ false. append=true ``` +### `separator` + +Default value is "," (a comma). Defines the separator string between elements of +a log entry. The value should be enclosed in quotes. + +``` +separator=" | " +``` + +### `newline_replacement` + +Default value is " " (one space). SQL-queries may include line breaks, which, if +printed directly to the log, may break automatic parsing. This parameter defines +what should be written in the place of a newline sequence (\r, \n or \r\n). If +this is set as the empty string, then newlines are not replaced and printed as +is to the output. The value should be enclosed in quotes. + +``` +newline_replacement=" NL " +``` + ## Examples ### Example 1 - Query without primary key From a4f6176ced2865df2c3b2417fc696105bc582625 Mon Sep 17 00:00:00 2001 From: Esa Korhonen Date: Wed, 17 Jan 2018 12:01:00 +0200 Subject: [PATCH 38/47] Fix bug in printing switchover/failover module command info The string constant passed to the register-function went invalid once CREATE_MODULE() completed, causing random characters to be printed. --- server/modules/monitor/mariadbmon/mysql_mon.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/modules/monitor/mariadbmon/mysql_mon.cc b/server/modules/monitor/mariadbmon/mysql_mon.cc index df32155f7..fce6c59e7 100644 --- a/server/modules/monitor/mariadbmon/mysql_mon.cc +++ b/server/modules/monitor/mariadbmon/mysql_mon.cc @@ -665,7 +665,7 @@ extern "C" MXS_MODULE* MXS_CREATE_MODULE() { MXS_NOTICE("Initialise the MySQL Monitor module."); - const char ARG_MONITOR_DESC[] = "MySQL Monitor name (from configuration file)"; + static const char ARG_MONITOR_DESC[] = "MySQL Monitor name (from configuration file)"; static modulecmd_arg_type_t switchover_argv[] = { { From c02da103d7ff59e867d68a458dbb6c731c07c7f4 Mon Sep 17 00:00:00 2001 From: Johan Wikman Date: Wed, 17 Jan 2018 12:05:04 +0200 Subject: [PATCH 39/47] MXS-1615 Fix access of wrong argument - Wrong argument accessed at command invocation. In debug mode leads to crash, in release mode probably leads to crash. - Log result of reload. --- server/modules/filter/masking/maskingfilter.cc | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/server/modules/filter/masking/maskingfilter.cc b/server/modules/filter/masking/maskingfilter.cc index da7eec3d8..9a344757f 100644 --- a/server/modules/filter/masking/maskingfilter.cc +++ b/server/modules/filter/masking/maskingfilter.cc @@ -39,9 +39,9 @@ char VERSION_STRING[] = "V1.0.0"; bool masking_command_reload(const MODULECMD_ARG* pArgs, json_t** output) { ss_dassert(pArgs->argc == 1); - ss_dassert(MODULECMD_GET_TYPE(&pArgs->argv[1].type) == MODULECMD_ARG_FILTER); + ss_dassert(MODULECMD_GET_TYPE(&pArgs->argv[0].type) == MODULECMD_ARG_FILTER); - const MXS_FILTER_DEF* pFilterDef = pArgs->argv[1].value.filter; + const MXS_FILTER_DEF* pFilterDef = pArgs->argv[0].value.filter; ss_dassert(pFilterDef); MaskingFilter* pFilter = reinterpret_cast(filter_def_get_instance(pFilterDef)); @@ -178,9 +178,17 @@ bool MaskingFilter::reload() if (sRules.get()) { + MXS_NOTICE("Rules for masking filter '%s' were reloaded from '%s'.", + m_config.name().c_str(), m_config.rules().c_str()); + m_sRules = sRules; rval = true; } + else + { + MXS_ERROR("Rules for masking filter '%s' could not be reloaded from '%s'.", + m_config.name().c_str(), m_config.rules().c_str()); + } return rval; } From 47184ea46d5956ee09fdd44fbd9bde8c8298d5b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Wed, 17 Jan 2018 12:27:50 +0200 Subject: [PATCH 40/47] Fix build failure on Ubuntu 17.10 The sprintf calls failed due to a warning about possible buffer overflow. Curiously enough, the same warnings do appear on Fedora 26 but only when the calls are changed to snprintf. --- server/modules/routing/binlogrouter/blr_slave.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/modules/routing/binlogrouter/blr_slave.c b/server/modules/routing/binlogrouter/blr_slave.c index 8b61cc103..3789c55e1 100644 --- a/server/modules/routing/binlogrouter/blr_slave.c +++ b/server/modules/routing/binlogrouter/blr_slave.c @@ -1051,8 +1051,8 @@ static int blr_slave_send_master_status(ROUTER_INSTANCE *router, ROUTER_SLAVE *slave) { GWBUF *pkt; - char file[40]; - char position[40]; + char file[BINLOG_FNAMELEN + 1]; + char position[BINLOG_FNAMELEN + 1]; uint8_t *ptr; int len, file_len; @@ -1069,10 +1069,10 @@ blr_slave_send_master_status(ROUTER_INSTANCE *router, ROUTER_SLAVE *slave) BLR_TYPE_STRING, 40, 6); blr_slave_send_eof(router, slave, 7); - sprintf(file, "%s", router->binlog_name); + snprintf(file, sizeof(file), "%s", router->binlog_name); file_len = strlen(file); - sprintf(position, "%lu", router->binlog_position); + 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) From beac04b8e7230bc7528be9843ed9379c9075f651 Mon Sep 17 00:00:00 2001 From: Johan Wikman Date: Wed, 17 Jan 2018 14:29:01 +0200 Subject: [PATCH 41/47] MXS-1616 Also use 'value' in partial matching As you can create regular expressions that have a fixed length, e.g. "....$", it makes perfect sense to replace using 'value' if the length of the string matches exactly. --- server/modules/filter/masking/maskingrules.cc | 41 +++++++++++++++---- server/modules/filter/masking/maskingrules.hh | 6 +++ 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/server/modules/filter/masking/maskingrules.cc b/server/modules/filter/masking/maskingrules.cc index 53a4a242f..34d97ca4f 100644 --- a/server/modules/filter/masking/maskingrules.cc +++ b/server/modules/filter/masking/maskingrules.cc @@ -485,9 +485,11 @@ MaskingRules::MatchRule::MatchRule(const std::string& column, const std::vector& applies_to, const std::vector& exempted, pcre2_code* regexp, + const std::string& value, const std::string& fill) : MaskingRules::Rule::Rule(column, table, database, applies_to, exempted) , m_regexp(regexp) + , m_value(value) , m_fill(fill) { } @@ -767,13 +769,15 @@ bool rule_get_values(json_t* pRule, * * @param pRule The Json rule doc * @param pMatch The string buffer for 'match'value + * @param pValue The string buffer for 'value' value * @param pFill The string buffer for 'fill' value * * @return True on success, false on errors */ -bool rule_get_match_fill(json_t* pRule, - std::string *pMatch, - std::string* pFill) +bool rule_get_match_value_fill(json_t* pRule, + std::string *pMatch, + std::string* pValue, + std::string* pFill) { // Get the 'with' key from the rule json_t* pWith = json_object_get(pRule, KEY_WITH); @@ -794,17 +798,22 @@ bool rule_get_match_fill(json_t* pRule, // Get fill from 'with' object json_t* pTheFill = rule_get_fill(pWith); + // Get value from 'with' object + json_t* pTheValue = json_object_get(pWith, KEY_VALUE); // Get 'match' from 'replace' ojbect json_t* pTheMatch = json_object_get(pKeyObj, KEY_MATCH); - // Check values + // Check values: 'match' and 'fill' are mandatory (if not provided, there will be + // a default 'fill'), while 'value' is optional, but if provided it must be a string. if ((!pTheFill || !json_is_string(pTheFill)) || + (pTheValue && !json_is_string(pTheValue)) || ((!pTheMatch || !json_is_string(pTheMatch)))) { - MXS_ERROR("A masking '%s' rule has '%s' and/or '%s' " + MXS_ERROR("A masking '%s' rule has '%s', '%s' and/or '%s' " "invalid Json strings.", KEY_REPLACE, KEY_MATCH, + KEY_VALUE, KEY_FILL); return false; } @@ -814,6 +823,11 @@ bool rule_get_match_fill(json_t* pRule, pFill->assign(json_string_value(pTheFill)); pMatch->assign(json_string_value(pTheMatch)); + if (pTheValue) + { + pValue->assign(json_string_value(pTheValue)); + } + return true; } } @@ -984,9 +998,10 @@ auto_ptr MaskingRules::MatchRule::create_from(json_t* pRule) &table, &database, KEY_REPLACE) && - rule_get_match_fill(pRule, // get match/fill - &match, - &fill)) + rule_get_match_value_fill(pRule, // get match/value/fill + &match, + &value, + &fill)) { if (!match.empty() && !fill.empty()) @@ -1004,6 +1019,7 @@ auto_ptr MaskingRules::MatchRule::create_from(json_t* pRule) applies_to, exempted, pCode, + value, fill)); // Ownership of pCode has been moved to the MatchRule object. @@ -1162,7 +1178,14 @@ void MaskingRules::MatchRule::rewrite(LEncString& s) const } // Copy the fill string into substring - fill_buffer(m_fill.begin(), m_fill.end(), i, i + substring_len); + if (m_value.length() == substring_len) + { + std::copy(m_value.begin(), m_value.end(), i); + } + else + { + fill_buffer(m_fill.begin(), m_fill.end(), i, i + substring_len); + } // Set offset to the end of Full Match substring or break startoffset = ovector[1]; diff --git a/server/modules/filter/masking/maskingrules.hh b/server/modules/filter/masking/maskingrules.hh index 162396c5a..a4d12de69 100644 --- a/server/modules/filter/masking/maskingrules.hh +++ b/server/modules/filter/masking/maskingrules.hh @@ -268,6 +268,7 @@ public: const std::vector& applies_to, const std::vector& exempted, pcre2_code* regexp, + const std::string& value, const std::string& fill); ~MatchRule(); @@ -277,6 +278,10 @@ public: return *m_regexp; } + const std::string& value() const + { + return m_value; + } const std::string& fill() const { return m_fill; @@ -301,6 +306,7 @@ public: private: pcre2_code* m_regexp; + std::string m_value; std::string m_fill; private: From 8c0d519caa16f33bce979960591d40f27ef33a2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Wed, 17 Jan 2018 14:11:31 +0200 Subject: [PATCH 42/47] Update Connector-C to 3.0.3 Updated connector from 3.0.2 to 3.0.3. --- cmake/BuildMariaDBConnector.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/BuildMariaDBConnector.cmake b/cmake/BuildMariaDBConnector.cmake index c8a99acbf..cf7b2ea93 100644 --- a/cmake/BuildMariaDBConnector.cmake +++ b/cmake/BuildMariaDBConnector.cmake @@ -8,8 +8,8 @@ set(MARIADB_CONNECTOR_C_REPO "https://github.com/MariaDB/mariadb-connector-c.git" CACHE STRING "MariaDB Connector-C Git repository") -# Release 2.3.3 (preliminary) of the Connector-C -set(MARIADB_CONNECTOR_C_TAG "v3.0.2" +# Connector-C tag to use +set(MARIADB_CONNECTOR_C_TAG "v3.0.3" CACHE STRING "MariaDB Connector-C Git tag") ExternalProject_Add(connector-c From d928f69cc74d93fb6add90de7c951ba1c874eaa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Wed, 17 Jan 2018 18:09:35 +0200 Subject: [PATCH 43/47] Update and extend luafilter documentation Fixed the exposed function name and added minimal configuration and script examples. --- Documentation/Filters/Luafilter.md | 45 +++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/Documentation/Filters/Luafilter.md b/Documentation/Filters/Luafilter.md index 42082cec9..81bdabf9e 100644 --- a/Documentation/Filters/Luafilter.md +++ b/Documentation/Filters/Luafilter.md @@ -75,7 +75,7 @@ matching entry point is made. The luafilter exposes three functions that can be called from the Lua script. -- `string lua_qc_get_type()` +- `string lua_qc_get_type_mask()` - Returns the type of the current query being executed as a string. The values are the string versions of the query types defined in _query_classifier.h_ @@ -94,3 +94,46 @@ The luafilter exposes three functions that can be called from the Lua script. - This function generates unique integers that can be used to distinct sessions from each other. + +## Example Configuration and Script + +Here is a minimal configuration entry for a luafilter definition. + +``` +[MyLuaFilter] +type=filter +module=luafilter +global_script=/path/to/script.lua +``` + +And here is a script that opens a file in `/tmp/` and logs output to it. + +``` +f = io.open("/tmp/test.log", "a+") + +function createInstance() + f:write("createInstance\n") +end + +function newSession(a, b) + f:write("newSession for: " .. a .. "@" .. b .. "\n") +end + +function closeSession() + f:write("closeSession\n") +end + +function routeQuery(string) + f:write("routeQuery: " .. string .. " -- type: " .. lua_qc_get_type_mask() .. " operation: " .. lua_qc_get_operation() .. "\n") +end + +function clientReply() + f:write("clientReply\n") +end + +function diagnostic() + f:write("diagnostics\n") + return "Hello from Lua!" +end + +``` From 7c7190e0c498516c3a6d53d0a587fd7cbf35e717 Mon Sep 17 00:00:00 2001 From: Johan Wikman Date: Fri, 19 Jan 2018 09:41:01 +0200 Subject: [PATCH 44/47] Fix index overflow in masking filter --- server/modules/filter/masking/maskingrules.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/modules/filter/masking/maskingrules.cc b/server/modules/filter/masking/maskingrules.cc index 34d97ca4f..5a56a0051 100644 --- a/server/modules/filter/masking/maskingrules.cc +++ b/server/modules/filter/masking/maskingrules.cc @@ -1210,7 +1210,7 @@ void MaskingRules::ObfuscateRule::rewrite(LEncString& s) const LEncString::iterator i = s.begin(); size_t c = *i + i_len; - for (LEncString::iterator i = s.begin(); i <= s.end(); i++) + for (LEncString::iterator i = s.begin(); i != s.end(); i++) { // ASCII 32 is first printable char unsigned char d = abs((char)(*i ^ c)) + 32; From 40dfd1e0705b422ccac43de04248023281e4cce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Fri, 19 Jan 2018 11:23:28 +0200 Subject: [PATCH 45/47] MXS-1620: Fix RPM packaging scripts Unconditionally replace the `strip` binary with a dummy executable. --- BUILD/build_rpm_local.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/BUILD/build_rpm_local.sh b/BUILD/build_rpm_local.sh index 39006c778..d7edbc9c7 100755 --- a/BUILD/build_rpm_local.sh +++ b/BUILD/build_rpm_local.sh @@ -18,11 +18,11 @@ then ctest --output-on-failure || exit 1 fi -if [ $remove_strip == "yes" ] ; then - sudo rm -rf /usr/bin/strip - sudo touch /usr/bin/strip - sudo chmod a+x /usr/bin/strip -fi +# Never strip binaries +sudo rm -rf /usr/bin/strip +sudo touch /usr/bin/strip +sudo chmod a+x /usr/bin/strip + sudo make package res=$? if [ $res != 0 ] ; then From bbe99e458d9b8fbaed0c67185d91badcfcb4a22a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Fri, 19 Jan 2018 09:30:21 +0200 Subject: [PATCH 46/47] Fix test build failure MXS-1543 test failed to build in 2.2. --- maxscale-system-test/mxs1543.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maxscale-system-test/mxs1543.cpp b/maxscale-system-test/mxs1543.cpp index 6d185f92d..84b3c23cb 100644 --- a/maxscale-system-test/mxs1543.cpp +++ b/maxscale-system-test/mxs1543.cpp @@ -26,7 +26,7 @@ int main(int argc, char** argv) // Wait for the avrorouter to process the data sleep(10); - test.check_log_err("Possible STATEMENT or MIXED", true); + test.check_log_err(0, "Possible STATEMENT or MIXED", true); return test.global_result; } From 753b97303a63748c32d66d535f4317a6871be199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Wed, 17 Jan 2018 11:22:33 +0200 Subject: [PATCH 47/47] MXS-1606: Create maxscale_schema database if missing The monitor will now also create the database if it is missing. Since it already creates the table, also creating the database is not a large addition. Cleaned up some of the related checking code and combined them into a simple utility function. --- .../modules/monitor/mariadbmon/mysql_mon.cc | 79 +++++++++++-------- 1 file changed, 44 insertions(+), 35 deletions(-) diff --git a/server/modules/monitor/mariadbmon/mysql_mon.cc b/server/modules/monitor/mariadbmon/mysql_mon.cc index fce6c59e7..4e7231b33 100644 --- a/server/modules/monitor/mariadbmon/mysql_mon.cc +++ b/server/modules/monitor/mariadbmon/mysql_mon.cc @@ -2538,10 +2538,36 @@ getSlaveOfNodeId(MXS_MONITORED_SERVER *ptr, long node_id, slave_down_setting_t s return NULL; } +/** + * Simple wrapper for mxs_mysql_query and mysql_num_rows + * + * @param database Database connection + * @param query Query to execute + * + * @return Number of rows or -1 on error + */ +static int get_row_count(MXS_MONITORED_SERVER *database, const char* query) +{ + int returned_rows = -1; + + if (mxs_mysql_query(database->con, query) == 0) + { + MYSQL_RES* result = mysql_store_result(database->con); + + if (result) + { + returned_rows = mysql_num_rows(result); + mysql_free_result(result); + } + } + + return returned_rows; +} + /******* * This function sets the replication heartbeat * into the maxscale_schema.replication_heartbeat table in the current master. - * The inserted values will be seen from all slaves replication from this master. + * The inserted values will be seen from all slaves replicating from this master. * * @param handle The monitor handle * @param database The number database server @@ -2562,42 +2588,25 @@ static void set_master_heartbeat(MYSQL_MONITOR *handle, MXS_MONITORED_SERVER *da return; } - /* check if the maxscale_schema database and replication_heartbeat table exist */ - if (mxs_mysql_query(database->con, "SELECT table_name FROM information_schema.tables " - "WHERE table_schema = 'maxscale_schema' AND table_name = 'replication_heartbeat'")) + int n_db = get_row_count(database, "SELECT schema_name FROM information_schema.schemata " + "WHERE schema_name = 'maxscale_schema'"); + int n_tbl = get_row_count(database, "SELECT table_name FROM information_schema.tables " + "WHERE table_schema = 'maxscale_schema' " + "AND table_name = 'replication_heartbeat'"); + + if (n_db == -1 || n_tbl == -1 || + (n_db == 0 && mxs_mysql_query(database->con, "CREATE DATABASE maxscale_schema")) || + (n_tbl == 0 && mxs_mysql_query(database->con, "CREATE TABLE IF NOT EXISTS " + "maxscale_schema.replication_heartbeat " + "(maxscale_id INT NOT NULL, " + "master_server_id INT NOT NULL, " + "master_timestamp INT UNSIGNED NOT NULL, " + "PRIMARY KEY ( master_server_id, maxscale_id ) )"))) { - MXS_ERROR( "Error checking for replication_heartbeat in Master server" - ": %s", mysql_error(database->con)); + MXS_ERROR("Error creating maxscale_schema.replication_heartbeat " + "table in Master server: %s", mysql_error(database->con)); database->server->rlag = MAX_RLAG_NOT_AVAILABLE; - } - - result = mysql_store_result(database->con); - - if (result == NULL) - { - returned_rows = 0; - } - else - { - returned_rows = mysql_num_rows(result); - mysql_free_result(result); - } - - if (0 == returned_rows) - { - /* create repl_heartbeat table in maxscale_schema database */ - if (mxs_mysql_query(database->con, "CREATE TABLE IF NOT EXISTS " - "maxscale_schema.replication_heartbeat " - "(maxscale_id INT NOT NULL, " - "master_server_id INT NOT NULL, " - "master_timestamp INT UNSIGNED NOT NULL, " - "PRIMARY KEY ( master_server_id, maxscale_id ) )")) - { - MXS_ERROR("Error creating maxscale_schema.replication_heartbeat " - "table in Master server: %s", mysql_error(database->con)); - - database->server->rlag = MAX_RLAG_NOT_AVAILABLE; - } + return; } /* auto purge old values after 48 hours*/