diff --git a/Documentation/Debug And Diagnostic Support.pdf b/Documentation/Debug And Diagnostic Support.pdf deleted file mode 100644 index d7518bbee..000000000 Binary files a/Documentation/Debug And Diagnostic Support.pdf and /dev/null differ diff --git a/Documentation/MaxScale Configuration And Usage Scenarios-Z3.pdf b/Documentation/MaxScale Configuration And Usage Scenarios-Z3.pdf new file mode 100644 index 000000000..b297e427c Binary files /dev/null and b/Documentation/MaxScale Configuration And Usage Scenarios-Z3.pdf differ diff --git a/Documentation/MaxScale Configuration And Usage Scenarios.pdf b/Documentation/MaxScale Configuration And Usage Scenarios.pdf deleted file mode 100644 index 7e588e2c9..000000000 Binary files a/Documentation/MaxScale Configuration And Usage Scenarios.pdf and /dev/null differ diff --git a/Documentation/MaxScale Debug And Diagnostic Support-Z3.pdf b/Documentation/MaxScale Debug And Diagnostic Support-Z3.pdf new file mode 100644 index 000000000..746a78583 Binary files /dev/null and b/Documentation/MaxScale Debug And Diagnostic Support-Z3.pdf differ diff --git a/Documentation/MaxScale MySQL Cluster setup-Z3.pdf b/Documentation/MaxScale MySQL Cluster setup-Z3.pdf new file mode 100644 index 000000000..5d4490fd6 Binary files /dev/null and b/Documentation/MaxScale MySQL Cluster setup-Z3.pdf differ diff --git a/Documentation/RabbitMQ Setup And MaxScale Integration-Z3.pdf b/Documentation/RabbitMQ Setup And MaxScale Integration-Z3.pdf new file mode 100644 index 000000000..e9c728087 Binary files /dev/null and b/Documentation/RabbitMQ Setup And MaxScale Integration-Z3.pdf differ diff --git a/Documentation/internal/hint_syntax.pdf b/Documentation/internal/hint_syntax.pdf new file mode 100644 index 000000000..f90b80acc Binary files /dev/null and b/Documentation/internal/hint_syntax.pdf differ diff --git a/Makefile b/Makefile index 328fa9b7a..63f92751f 100644 --- a/Makefile +++ b/Makefile @@ -42,6 +42,7 @@ all: (cd client; make) clean: + echo '#define MAXSCALE_VERSION "'`cat $(ROOT_PATH)/VERSION`'"' > $(ROOT_PATH)/server/include/version.h (cd log_manager; make clean) (cd query_classifier; make clean) (cd server; make clean) diff --git a/build_gateway.inc b/build_gateway.inc index 0e1d6b75f..035cf56d8 100644 --- a/build_gateway.inc +++ b/build_gateway.inc @@ -52,3 +52,17 @@ endif # ERRMSG := $(HOME)/usr/share/mysql +# +# Build a binary that produces profile data +# +PROFILE := N + +# +# Build a binary that produces code coverage data +# +GCOV := N + +# Build optional RabbitMQ filter +# Requires librabbitmq-devel +# +BUILD_RABBITMQ := N diff --git a/gcov.diff b/gcov.diff deleted file mode 100644 index c310d1b08..000000000 --- a/gcov.diff +++ /dev/null @@ -1,138 +0,0 @@ -diff --git a/makefile.inc b/makefile.inc -index f2d93bf..c7dbffa 100644 ---- a/makefile.inc -+++ b/makefile.inc -@@ -24,8 +24,8 @@ endif - - # -O2 -g -pipe -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fstack-protector --param=ssp-buffer-size=4 -fPIC - --CFLAGS := $(CFLAGS) -Wall --LDLIBS := $(LDLIBS) -pthread -+CFLAGS := $(CFLAGS) -Wall -fprofile-arcs -ftest-coverage -+LDLIBS := $(LDLIBS) -pthread -lgcov - LDMYSQL := -lmysqld - CPP_LDLIBS := -lstdc++ - -diff --git a/server/core/Makefile b/server/core/Makefile -index 9bf650c..9df75a7 100644 ---- a/server/core/Makefile -+++ b/server/core/Makefile -@@ -75,7 +75,7 @@ POBJS=maxpasswd.o secrets.o utils.o - LIBS=-L$(EMBEDDED_LIB) \ - -lmysqld \ - -lz -lm -lcrypt -lcrypto -ldl -laio -lrt -pthread -llog_manager \ -- -L../inih/extra -linih -lssl -lstdc++ -+ -L../inih/extra -linih -lssl -lstdc++ -lgcov - - all: maxscale maxkeys maxpasswd - -diff --git a/server/modules/filter/Makefile b/server/modules/filter/Makefile -index 931c35a..d5dcca9 100644 ---- a/server/modules/filter/Makefile -+++ b/server/modules/filter/Makefile -@@ -25,7 +25,7 @@ UTILSPATH := $(ROOT_PATH)/utils - - CC=cc - CFLAGS=-c -fPIC -I/usr/include -I../include -I../../include -I$(LOGPATH) \ -- -I$(UTILSPATH) -Wall -g -+ -I$(UTILSPATH) -Wall -g -fprofile-arcs -ftest-coverage - - include ../../../makefile.inc - -@@ -44,7 +44,7 @@ TEESRCS=tee.c - TEEOBJ=$(TEESRCS:.c=.o) - SRCS=$(TESTSRCS) $(QLASRCS) $(REGEXSRCS) $(TOPNSRCS) $(TEESRCS) - OBJ=$(SRCS:.c=.o) --LIBS=$(UTILSPATH)/skygw_utils.o -lssl -llog_manager -+LIBS=$(UTILSPATH)/skygw_utils.o -lssl -llog_manager -lgcov - MODULES= libtestfilter.so libqlafilter.so libregexfilter.so libtopfilter.so libtee.so - - -diff --git a/server/modules/monitor/Makefile b/server/modules/monitor/Makefile -index 7fdbc58..bca01de 100644 ---- a/server/modules/monitor/Makefile -+++ b/server/modules/monitor/Makefile -@@ -28,7 +28,7 @@ CFLAGS=-c -fPIC -I. -I/usr/include -I../include -I../../include -I$(LOGPATH) \ - - LDFLAGS=-shared -L$(LOGPATH) -Wl,-rpath,$(DEST)/lib \ - -Wl,-rpath,$(LOGPATH) -Wl,-rpath,$(UTILSPATH) \ -- -Wl,-rpath,$(EMBEDDED_LIB) -+ -Wl,-rpath,$(EMBEDDED_LIB) -fprofile-arcs -ftest-coverage - - - -@@ -39,7 +39,7 @@ GALERAOBJ=$(GALERASRCS:.c=.o) - SRCS=$(MYSQLSRCS) - OBJ=$(SRCS:.c=.o) - LIBS=$(UTILSPATH)/skygw_utils.o -llog_manager \ -- -L$(EMBEDDED_LIB) -lmysqld -+ -L$(EMBEDDED_LIB) -lmysqld -lgcov - MODULES=libmysqlmon.so libgaleramon.so - - -diff --git a/server/modules/protocol/Makefile b/server/modules/protocol/Makefile -index 54a8f8c..c8913ab 100644 ---- a/server/modules/protocol/Makefile -+++ b/server/modules/protocol/Makefile -@@ -31,7 +31,7 @@ UTILSPATH := $(ROOT_PATH)/utils - - CC=cc - CFLAGS=-c -fPIC -I/usr/include -I../include -I../../include -I$(LOGPATH) \ -- -I$(UTILSPATH) -Wall -g -+ -I$(UTILSPATH) -Wall -g -fprofile-arcs -ftest-coverage - - include ../../../makefile.inc - -@@ -51,7 +51,7 @@ MAXSCALEDOBJ=$(MAXSCALEDSRCS:.c=.o) - SRCS=$(MYSQLCLIENTSRCS) $(MYSQLBACKENDSRCS) $(TELNETDSRCS) $(HTTPDSRCS) \ - $(MAXSCALEDSRCS) - OBJ=$(SRCS:.c=.o) --LIBS=$(UTILSPATH)/skygw_utils.o -+LIBS=$(UTILSPATH)/skygw_utils.o -lgcov - MODULES=libMySQLClient.so libMySQLBackend.so libtelnetd.so libHTTPD.so \ - libmaxscaled.so - -diff --git a/server/modules/routing/Makefile b/server/modules/routing/Makefile -index 4feac68..afd1da7 100644 ---- a/server/modules/routing/Makefile -+++ b/server/modules/routing/Makefile -@@ -29,7 +29,7 @@ UTILSPATH := $(ROOT_PATH)/utils - - CC=cc - CFLAGS=-c -fPIC -I/usr/include -I../include -I../../include -I$(LOGPATH) \ -- -I$(UTILSPATH) -Wall -g -+ -I$(UTILSPATH) -Wall -g -fprofile-arcs -ftest-coverage - - include ../../../makefile.inc - -@@ -46,7 +46,7 @@ CLISRCS=cli.c debugcmd.c - CLIOBJ=$(CLISRCS:.c=.o) - SRCS=$(TESTSRCS) $(READCONSRCS) $(DEBUGCLISRCS) cli.c - OBJ=$(SRCS:.c=.o) --LIBS=$(UTILSPATH)/skygw_utils.o -lssl -llog_manager -+LIBS=$(UTILSPATH)/skygw_utils.o -lssl -llog_manager -lgcov - MODULES= libdebugcli.so libreadconnroute.so libtestroute.so libcli.so - - -diff --git a/server/modules/routing/readwritesplit/Makefile b/server/modules/routing/readwritesplit/Makefile -index c60f2ff..a3a643e 100644 ---- a/server/modules/routing/readwritesplit/Makefile -+++ b/server/modules/routing/readwritesplit/Makefile -@@ -27,7 +27,7 @@ QCLASSPATH := $(ROOT_PATH)/query_classifier - CC=cc - CFLAGS=-c -fPIC -I/usr/include -I../../include -I../../../include \ - -I$(LOGPATH) -I$(UTILSPATH) -I$(QCLASSPATH) \ -- $(MYSQL_HEADERS) -Wall -g -+ $(MYSQL_HEADERS) -Wall -g -fprofile-arcs -ftest-coverage - - include ../../../../makefile.inc - -@@ -38,7 +38,7 @@ LDFLAGS=-shared -L$(LOGPATH) -L$(QCLASSPATH) -L$(EMBEDDED_LIB) \ - - SRCS=readwritesplit.c - OBJ=$(SRCS:.c=.o) --LIBS=-lssl -pthread -llog_manager -lquery_classifier -lmysqld -+LIBS=-lssl -pthread -llog_manager -lquery_classifier -lmysqld -lgcov - MODULES=libreadwritesplit.so - - all: $(MODULES) diff --git a/log_manager/log_manager.cc b/log_manager/log_manager.cc index 733e9fffe..6c40190be 100644 --- a/log_manager/log_manager.cc +++ b/log_manager/log_manager.cc @@ -45,6 +45,12 @@ extern char *program_invocation_name; extern char *program_invocation_short_name; +#if defined(SS_DEBUG) +static int write_index; +static int block_start_index; +static int prevval; +static simple_mutex_t msg_mutex; +#endif /** * Variable holding the enabled logfiles information. * Used from log users to check enabled logs prior calling @@ -113,7 +119,7 @@ typedef struct blockbuf_st { skygw_chk_t bb_chk_top; #endif logfile_id_t bb_fileid; - bool bb_isfull; /**< closed for disk write */ + blockbuf_state_t bb_state; /**State of the block buffer*/ simple_mutex_t bb_mutex; /**< bb_buf_used, bb_isfull */ int bb_refcount; /**< protected by list mutex. #of clients */ // int bb_blankcount; /**< # of blanks used btw strings */ @@ -333,6 +339,10 @@ static bool logmanager_init_nomutex( #if defined(SS_DEBUG) lm->lm_chk_top = CHK_NUM_LOGMANAGER; lm->lm_chk_tail = CHK_NUM_LOGMANAGER; + write_index = 0; + block_start_index = 0; + prevval = -1; + simple_mutex_init(&msg_mutex, "Message mutex"); #endif lm->lm_clientmes = skygw_message_init(); lm->lm_logmes = skygw_message_init(); @@ -652,27 +662,73 @@ static int logmanager_write_log( /** Findout how much can be safely written with current block size */ if (timestamp_len-1+str_len > lf->lf_buf_size) - { - safe_str_len = lf->lf_buf_size; - } + { + safe_str_len = lf->lf_buf_size; + } else - { - safe_str_len = timestamp_len-1+str_len; - } + { + safe_str_len = timestamp_len-1+str_len; + } + /** * Seek write position and register to block buffer. * Then print formatted string to write position. */ + +#if defined (SS_LOG_DEBUG) + { + + char *copy,*tok; + int tokval; + + simple_mutex_lock(&msg_mutex,true); + + copy = strdup(str); + + tok = strtok(copy,"|"); + + tok = strtok(NULL,"|"); + + if(strstr(str,"message|") && tok){ + + tokval = atoi(tok); + + if(prevval > 0){ + ss_dassert(tokval == (prevval + 1)); + } + + prevval = tokval; + } + + free(copy); + simple_mutex_unlock(&msg_mutex); + + } +#endif + wp = blockbuf_get_writepos(&bb, id, safe_str_len, flush); + + +#if defined (SS_LOG_DEBUG) + { + sprintf(wp,"[msg:%d]",atomic_add(&write_index,1)); + safe_str_len -= strlen(wp); + wp += strlen(wp); + } +#endif + /** * Write timestamp with at most characters * to wp. * Returned timestamp_len doesn't include terminating null. */ timestamp_len = snprint_timestamp(wp, timestamp_len); + + + /** * Write next string to overwrite terminating null character * of the timestamp string. @@ -682,7 +738,7 @@ static int logmanager_write_log( } else { snprintf(wp+timestamp_len, safe_str_len-timestamp_len, "%s", str); } - + /** write to syslog */ if (lf->lf_write_syslog) { @@ -753,7 +809,7 @@ static int logmanager_write_log( wp_c[timestamp_len-1+str_len-1]='\n'; /** lock-free unregistration, includes flush if - * bb_isfull */ + * bb_state == BB_FULL */ blockbuf_unregister(bb_c); } } /* if (spread_down) */ @@ -784,7 +840,7 @@ static void blockbuf_unregister( /** * if this is the last client in a full buffer, send write request. */ - if (atomic_add(&bb->bb_refcount, -1) == 1 && bb->bb_isfull) { + if (atomic_add(&bb->bb_refcount, -1) == 1 && bb->bb_state == BB_FULL) { skygw_message_send(lf->lf_logmes); } ss_dassert(bb->bb_refcount >= 0); @@ -816,12 +872,12 @@ static char* blockbuf_get_writepos( size_t str_len, bool flush) { - logfile_t* lf; - mlist_t* bb_list; - char* pos = NULL; - mlist_node_t* node; - blockbuf_t* bb; - ss_debug(bool succp;) + logfile_t* lf; + mlist_t* bb_list; + char* pos = NULL; + mlist_node_t* node; + blockbuf_t* bb; + ss_debug(bool succp;) CHK_LOGMANAGER(lm); @@ -838,6 +894,7 @@ static char* blockbuf_get_writepos( * At least block buffer exists on the list. */ node = bb_list->mlist_first; + /** Loop over blockbuf list to find write position */ while (true) { @@ -852,22 +909,25 @@ static char* blockbuf_get_writepos( /** Lock buffer */ simple_mutex_lock(&bb->bb_mutex, true); - if (bb->bb_isfull || bb->bb_buf_left < str_len) { + if (bb->bb_state == BB_FULL || bb->bb_buf_left < str_len) { /** * This block buffer is too full. * Send flush request to file writer thread. This causes * flushing all buffers, and (eventually) frees buffer space. */ - blockbuf_register(bb); - bb->bb_isfull = true; - blockbuf_unregister(bb); + blockbuf_register(bb); + + bb->bb_state = BB_FULL; + + blockbuf_unregister(bb); + + /** Unlock buffer */ + simple_mutex_unlock(&bb->bb_mutex); + + /** Lock list */ + simple_mutex_lock(&bb_list->mlist_mutex, true); - /** Unlock buffer */ - simple_mutex_unlock(&bb->bb_mutex); - /** Lock list */ - simple_mutex_lock(&bb_list->mlist_mutex, true); - /** * If next node exists move forward. Else check if there is * space for a new block buffer on the list. @@ -916,7 +976,28 @@ static char* blockbuf_get_writepos( node = bb_list->mlist_first; continue; } - } else { + + + }else if(bb->bb_state == BB_CLEARED){ + + /** + *Move the full buffer to the end of the list + */ + + simple_mutex_unlock(&bb->bb_mutex); + simple_mutex_lock(&bb_list->mlist_mutex, true); + + if(node->mlnode_next){ + bb_list->mlist_first = node->mlnode_next; + bb_list->mlist_last->mlnode_next = node; + node->mlnode_next = NULL; + bb_list->mlist_last = node; + node = bb_list->mlist_first; + } + + bb->bb_state = BB_READY; + + }else if (bb->bb_state == BB_READY){ /** * There is space for new log string. */ @@ -924,9 +1005,11 @@ static char* blockbuf_get_writepos( } } /** while (true) */ } else { + /** * Create the first block buffer to logfile's blockbuf list. */ + bb = blockbuf_init(id); CHK_BLOCKBUF(bb); @@ -952,7 +1035,7 @@ static char* blockbuf_get_writepos( } /* if (bb_list->mlist_nodecount > 0) */ ss_dassert(pos == NULL); - ss_dassert(!(bb->bb_isfull || bb->bb_buf_left < str_len)); + ss_dassert(!(bb->bb_state == BB_FULL || bb->bb_buf_left < str_len)); ss_dassert(bb_list->mlist_nodecount <= bb_list->mlist_nodecount_max); /** @@ -991,7 +1074,7 @@ static char* blockbuf_get_writepos( * If flush flag is set, set buffer full. As a consequence, no-one * can write to it before it is flushed to disk. */ - bb->bb_isfull = (flush == true ? true : bb->bb_isfull); + bb->bb_state = (flush == true ? BB_FULL : bb->bb_state); /** Unlock buffer */ simple_mutex_unlock(&bb->bb_mutex); @@ -1021,6 +1104,12 @@ static blockbuf_t* blockbuf_init( bb->bb_buf_left = MAX_LOGSTRLEN; bb->bb_buf_size = MAX_LOGSTRLEN; +#if defined(SS_LOG_DEBUG) + sprintf(bb->bb_buf,"[block:%d]",atomic_add(&block_start_index,1)); + bb->bb_buf_used += strlen(bb->bb_buf); + bb->bb_buf_left -= strlen(bb->bb_buf); +#endif + CHK_BLOCKBUF(bb); return bb; } @@ -1170,6 +1259,9 @@ int skygw_log_write_flush( /** * Find out the length of log string (to be formatted str). */ + + + va_start(valist, str); len = vsnprintf(NULL, 0, str, valist); va_end(valist); @@ -1220,6 +1312,9 @@ int skygw_log_write( err = 1; goto return_unregister; } + + + /** * Find out the length of log string (to be formatted str). */ @@ -1233,6 +1328,7 @@ int skygw_log_write( /** * Write log string to buffer and add to file write list. */ + va_start(valist, str); err = logmanager_write_log(id, false, true, true, len, str, valist); va_end(valist); @@ -2257,7 +2353,7 @@ static void filewriter_done( * lists of each logfile object. * * Block buffer is written to log file if - * 1. bb_isfull == true, + * 1. bb_state == true, * 2. logfile object's lf_flushflag == true, or * 3. skygw_thread_must_exit returns true. * @@ -2297,7 +2393,7 @@ static void* thr_filewriter_fun( blockbuf_t* bb; mlist_node_t* node; int i; - bool flush_blockbuf; /**< flush single block buffer. */ + blockbuf_state_t flush_blockbuf; /**< flush single block buffer. */ bool flush_logfile; /**< flush logfile */ bool flushall_logfiles;/**< flush all logfiles */ size_t vn1; @@ -2356,10 +2452,10 @@ static void* thr_filewriter_fun( /** Lock block buffer */ simple_mutex_lock(&bb->bb_mutex, true); - flush_blockbuf = bb->bb_isfull; + flush_blockbuf = bb->bb_state; if (bb->bb_buf_used != 0 && - (flush_blockbuf || + (flush_blockbuf == BB_FULL || flush_logfile || flushall_logfiles)) { @@ -2374,7 +2470,7 @@ static void* thr_filewriter_fun( &bb->bb_mutex, true); } - + skygw_file_write(file, (void *)bb->bb_buf, bb->bb_buf_used, @@ -2387,7 +2483,13 @@ static void* thr_filewriter_fun( bb->bb_buf_left = bb->bb_buf_size; bb->bb_buf_used = 0; memset(bb->bb_buf, 0, bb->bb_buf_size); - bb->bb_isfull = false; + bb->bb_state = BB_CLEARED; +#if defined(SS_LOG_DEBUG) + sprintf(bb->bb_buf,"[block:%d]",atomic_add(&block_start_index,1)); + bb->bb_buf_used += strlen(bb->bb_buf); + bb->bb_buf_left -= strlen(bb->bb_buf); +#endif + } /** Release lock to block buffer */ simple_mutex_unlock(&bb->bb_mutex); diff --git a/log_manager/log_manager.h b/log_manager/log_manager.h index b96669fa2..d9932d640 100644 --- a/log_manager/log_manager.h +++ b/log_manager/log_manager.h @@ -22,6 +22,12 @@ typedef struct logfile_st logfile_t; typedef struct fnames_conf_st fnames_conf_t; typedef struct logmanager_st logmanager_t; +typedef enum { + BB_READY = 0x00, + BB_FULL, + BB_CLEARED +} blockbuf_state_t; + typedef enum { LOGFILE_ERROR = 1, LOGFILE_FIRST = LOGFILE_ERROR, diff --git a/log_manager/makefile b/log_manager/makefile index a020c7932..7f35c13f6 100644 --- a/log_manager/makefile +++ b/log_manager/makefile @@ -8,6 +8,11 @@ SRCS := log_manager.cc UTILS_PATH := $(ROOT_PATH)/utils CUR_DIR := $(shell pwd) +ifeq ($(ADD_DEBUG_TAGS),Y) +CFLAGS += -DSS_LOG_DEBUG +endif + + makeall: clean all clean: diff --git a/log_manager/test/logorder.sh b/log_manager/test/logorder.sh new file mode 100755 index 000000000..bbdcaf0f7 --- /dev/null +++ b/log_manager/test/logorder.sh @@ -0,0 +1,61 @@ +#! /bin/bash + +if [[ $# -lt 4 ]] +then + echo "Usage: logorder.sh " + echo "To disable log flushing, use 0 for flush frequency" + exit +fi + +rm *.log + +#Create large messages +$PWD/testorder $1 $2 $3 + +TESTLOG=$4 +MCOUNT=$1 + +BLOCKS=`cat skygw_err1.log |tr -s ' '|grep -o 'block:[[:digit:]]\+'|cut -d ':' -f 2` +MESSAGES=`cat skygw_err1.log |tr -s ' '|grep -o 'message|[[:digit:]]\+'|cut -d '|' -f 2` + +prev=0 +error=0 + +for i in $BLOCKS +do + + if [[ $i -le $prev ]] + then + error=1 + echo "block mismatch: $i was after $prev." >> $TESTLOG + fi + prev=$i +done + +if [[ error -eq 0 ]] +then + echo "Block buffers were in order" >> $TESTLOG +else + echo "Error: block buffers were written in the wrong order" >> $TESTLOG +fi + +prev=0 +error=0 + +for i in $MESSAGES +do + + if [[ $i -ne $(( prev + 1 )) ]] + then + error=1 + echo "message mismatch: $i was after $prev." >> $TESTLOG + fi + prev=$i +done + +if [[ error -eq 0 ]] +then + echo "Block buffer messages were in order" >> $TESTLOG +else + echo "Error: block buffer messages were written in the wrong order" >> $TESTLOG +fi diff --git a/log_manager/test/makefile b/log_manager/test/makefile index 75df90c60..f3e0dc362 100644 --- a/log_manager/test/makefile +++ b/log_manager/test/makefile @@ -28,10 +28,13 @@ testall: cleantests: - $(DEL) *.o + - $(DEL) *.log - $(DEL) testlog + - $(DEL) testorder - $(DEL) *~ buildtests: + $(MAKE) -C $(LOG_MANAGER_PATH) ADD_DEBUG_TAGS=Y $(CC) $(CFLAGS) \ -L$(LOG_MANAGER_PATH) \ -Wl,-rpath,$(DEST)/lib \ @@ -39,8 +42,19 @@ buildtests: -o testlog \ -I$(MARIADB_SRC_PATH)/include \ -I$(LOG_MANAGER_PATH) -I$(UTILS_PATH) testlog.c \ - -llog_manager $(LDLIBS) \ + -lstdc++ -llog_manager $(LDLIBS) \ $(UTILS_PATH)/skygw_utils.o + $(CC) $(CFLAGS) \ + -L$(LOG_MANAGER_PATH) \ + -Wl,-rpath,$(DEST)/lib \ + -Wl,-rpath,$(LOG_MANAGER_PATH)/ \ + -o testorder \ + -I$(MARIADB_SRC_PATH)/include \ + -I$(LOG_MANAGER_PATH) -I$(UTILS_PATH) testorder.c \ + -lstdc++ -llog_manager $(LDLIBS) \ + $(UTILS_PATH)/skygw_utils.o + + runtests: @@ -61,6 +75,10 @@ runtests: @echo "Use 16 threads" >> $(TESTLOG) @echo "" >> $(TESTLOG) @-$(LAUNCH_DEBUGGER) $(TESTAPP) "-t 16" 2>>$(TESTLOG) + @echo "" >> $(TEST_MAXSCALE_LOG) + @echo "Test Message Order" >> $(TEST_MAXSCALE_LOG) + @echo "" >> $(TEST_MAXSCALE_LOG) + ./logorder.sh 500 0 500 $(TEST_MAXSCALE_LOG) @echo "Log Manager PASSED" >> $(TESTLOG) @echo "" >> $(TESTLOG) diff --git a/log_manager/test/testorder.c b/log_manager/test/testorder.c new file mode 100644 index 000000000..e2bb94a7b --- /dev/null +++ b/log_manager/test/testorder.c @@ -0,0 +1,105 @@ +/* + * This file is distributed as part of the SkySQL Gateway. It is free + * software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, + * version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright SkySQL Ab 2013 + */ + +#include +#include +#include +#include +#include +#include + +int main(int argc, char** argv) +{ + int iterations = 0, i, interval = 10; + int block_size; + int succp = 0, err = 0; + char cwd[1024]; + char tmp[2048]; + char *message; + char** optstr; + long msg_index = 1; + + + memset(cwd,0,1024); + if( argc <4){ + fprintf(stderr, + "Log Manager Log Order Test\n" + "Writes an ascending number into the error log to determine if log writes are in order.\n" + "Usage:\t testorder \n"); + return 1; + } + + block_size = atoi(argv[3]); + if(block_size < 1){ + fprintf(stderr,"Message size too small, must be at least 1 byte long."); + } + + + if(getcwd(cwd,sizeof(cwd)) == NULL || + (optstr = (char**)malloc(sizeof(char*)*4)) == NULL || + (message = (char*)malloc(sizeof(char)*block_size))== NULL){ + fprintf(stderr,"Fatal Error, exiting..."); + return 1; + } + + memset(tmp,0,1024); + + sprintf(tmp,"%s",cwd); + optstr[0] = strdup("log_manager"); + optstr[1] = strdup("-j"); + optstr[2] = strdup(tmp); + optstr[3] = NULL; + + iterations = atoi(argv[1]); + interval = atoi(argv[2]); + + succp = skygw_logmanager_init( 3, optstr); + ss_dassert(succp); + + skygw_log_disable(LOGFILE_TRACE); + skygw_log_disable(LOGFILE_MESSAGE); + skygw_log_disable(LOGFILE_DEBUG); + + for(i = 0;i 0 && i % interval == 0){ + err = skygw_log_write_flush(LOGFILE_ERROR, message); + }else{ + err = skygw_log_write(LOGFILE_ERROR, message); + } + if(err){ + fprintf(stderr,"Error: log_manager returned %d",err); + break; + } + usleep(100); + //printf("%s\n",message); + } + + skygw_log_flush(LOGFILE_ERROR); + skygw_logmanager_done(); + free(message); + free(optstr[0]); + free(optstr[1]); + free(optstr[2]); + free(optstr[3]); + free(optstr); + return 0; +} diff --git a/makefile.inc b/makefile.inc index f2d93bf84..279cea6a4 100644 --- a/makefile.inc +++ b/makefile.inc @@ -41,3 +41,13 @@ endif ifdef PROF CFLAGS := $(CFLAGS) -DSS_PROF endif + +ifeq "$(PROFILE)" "Y" + CFLAGS += -pg + LDFLAGS += -pg +endif + +ifeq "$(GCOV)" "Y" + CFLAGS += -fprofile-arcs -ftest-coverage + LIBS += -lgcov +endif diff --git a/maxscale.spec b/maxscale.spec index dcdc16b7b..4519d77ad 100644 --- a/maxscale.spec +++ b/maxscale.spec @@ -16,9 +16,9 @@ Group: Development/Tools #Requires: %if 0%{?suse_version} -BuildRequires: gcc gcc-c++ ncurses-devel bison glibc-devel cmake libgcc_s1 perl make libtool libopenssl-devel libaio libaio-devel mariadb libedit-devel +BuildRequires: gcc gcc-c++ ncurses-devel bison glibc-devel cmake libgcc_s1 perl make libtool libopenssl-devel libaio libaio-devel mariadb libedit-devel librabbitmq-devel %else -BuildRequires: gcc gcc-c++ ncurses-devel bison glibc-devel cmake libgcc perl make libtool openssl-devel libaio libaio-devel +BuildRequires: gcc gcc-c++ ncurses-devel bison glibc-devel cmake libgcc perl make libtool openssl-devel libaio libaio-devel librabbitmq-devel %if 0%{?rhel} == 6 BuildRequires: libedit-devel %endif diff --git a/query_classifier/makefile b/query_classifier/makefile index b08f0a9bd..4f8cf34c8 100644 --- a/query_classifier/makefile +++ b/query_classifier/makefile @@ -8,6 +8,8 @@ SRCS := query_classifier.cc UTILS_PATH := $(ROOT_PATH)/utils QUERY_CLASSIFIER_PATH := $(ROOT_PATH)/query_classifier LOG_MANAGER_PATH := $(ROOT_PATH)/log_manager +SERVER_INC_PATH := $(ROOT_PATH)/server/include +MODULE_INC_PATH := $(ROOT_PATH)/server/modules/include makeall: clean all @@ -32,8 +34,7 @@ runtests: testall: $(MAKE) -C test testall - - + utils: $(MAKE) -C $(UTILS_PATH) clean all @@ -43,6 +44,9 @@ libcomp: $(CPP) -c $(CFLAGS) \ $(MYSQL_HEADERS) \ -I$(LOG_MANAGER_PATH) \ + -I$(SERVER_INC_PATH) \ + -I$(MODULE_INC_PATH) \ + -I$(UTILS_PATH) \ -I./ \ -fPIC ./query_classifier.cc -o query_classifier.o @@ -66,6 +70,9 @@ depend: $(CPP) -M $(CFLAGS) \ $(MYSQL_HEADERS) \ -I$(LOG_MANAGER_PATH) \ + -I$(SERVER_INC_PATH) \ + -I$(MODULE_INC_PATH) \ + -I$(UTILS_PATH) \ -I./ \ $(SRCS) > depend diff --git a/query_classifier/query_classifier.cc b/query_classifier/query_classifier.cc index 889940e00..02d693ac1 100644 --- a/query_classifier/query_classifier.cc +++ b/query_classifier/query_classifier.cc @@ -34,6 +34,7 @@ #include "../utils/skygw_types.h" #include "../utils/skygw_debug.h" #include +#include #include #include @@ -83,122 +84,158 @@ static bool skygw_stmt_causes_implicit_commit( static int is_autocommit_stmt( LEX* lex); -/** - * @node (write brief function description here) - * - * Parameters: - * @param query_str - - * - * - * @param client_flag - - * - * - * @return - * +static void parsing_info_set_plain_str(void* ptr, + char* str); + +/** + * Calls parser for the query includede in the buffer. Creates and adds parsing + * information to buffer if it doesn't exist already. Resolves the query type. * - * @details (write detailed description here) - * + * @param querybuf buffer including the query and possibly the parsing information + * + * @return query type */ -skygw_query_type_t skygw_query_classifier_get_type( - const char* query, - unsigned long client_flags, - MYSQL** p_mysql) +skygw_query_type_t query_classifier_get_type( + GWBUF* querybuf) { - MYSQL* mysql; - char* query_str; - const char* user = "skygw"; - const char* db = "skygw"; - THD* thd; + MYSQL* mysql; skygw_query_type_t qtype = QUERY_TYPE_UNKNOWN; - bool failp = FALSE; - - ss_info_dassert(query != NULL, ("query_str is NULL")); + bool succp; - query_str = const_cast(query); - LOGIF(LT, (skygw_log_write( - LOGFILE_TRACE, - "Query : \"%s\"", query_str))); + ss_info_dassert(querybuf != NULL, ("querybuf is NULL")); - /** Get server handle */ - mysql = mysql_init(NULL); + /** Create parsing info for the query and store it to buffer */ + succp = query_is_parsed(querybuf); - if (mysql == NULL) { - LOGIF(LE, (skygw_log_write_flush( - LOGFILE_ERROR, - "Error : call to mysql_real_connect failed due %d, %s.", - mysql_errno(mysql), - mysql_error(mysql)))); + if (!succp) + { + succp = parse_query(querybuf); + } + /** Read thd pointer and resolve the query type with it. */ + if (succp) + { + parsing_info_t* pi; - mysql_library_end(); - goto return_qtype; - } + pi = (parsing_info_t*)gwbuf_get_buffer_object_data(querybuf, + GWBUF_PARSING_INFO); + + if (pi != NULL) + { + mysql = (MYSQL *)pi->pi_handle; - if (p_mysql != NULL) - { - *p_mysql = mysql; + /** Find out the query type */ + if (mysql != NULL) + { + qtype = resolve_query_type((THD *)mysql->thd); + } + } } - /** Set methods and authentication to mysql */ - mysql_options(mysql, MYSQL_READ_DEFAULT_GROUP, "libmysqld_skygw"); - mysql_options(mysql, MYSQL_OPT_USE_EMBEDDED_CONNECTION, NULL); - mysql->methods = &embedded_methods; - mysql->user = my_strdup(user, MYF(0)); - mysql->db = my_strdup(db, MYF(0)); - mysql->passwd = NULL; - - /** Get one or create new THD object to be use in parsing */ - thd = get_or_create_thd_for_parsing(mysql, query_str); - - if (thd == NULL) - { - skygw_query_classifier_free(mysql); - *p_mysql = NULL; - goto return_qtype; - } - /** - * Create parse_tree inside thd. - * thd and even lex are readable even if parser failed so let it - * continue despite failure. - */ - failp = create_parse_tree(thd); - qtype = resolve_query_type(thd); - - if (p_mysql == NULL) - { - skygw_query_classifier_free(mysql); - } -return_qtype: return qtype; } - -void skygw_query_classifier_free( - MYSQL* mysql) +/** + * Create parsing info and try to parse the query included in the query buffer. + * Store pointer to created parse_tree_t object to buffer. + * + * @param querybuf buffer including the query and possibly the parsing information + * + * @return true if succeed, false otherwise + */ +bool parse_query ( + GWBUF* querybuf) { - if (mysql->thd != NULL) + bool succp; + THD* thd; + uint8_t* data; + size_t len; + char* query_str; + parsing_info_t* pi; + + CHK_GWBUF(querybuf); + /** Do not parse without releasing previous parse info first */ + ss_dassert(!query_is_parsed(querybuf)); + + if (query_is_parsed(querybuf)) { - (*mysql->methods->free_embedded_thd)(mysql); - mysql->thd = NULL; + return false; } - mysql_close(mysql); - mysql_thread_end(); -} + /** Create parsing info */ + pi = parsing_info_init(parsing_info_done); + + if (pi == NULL) + { + succp = false; + goto retblock; + } + /** Extract query and copy it to different buffer */ + data = (uint8_t*)GWBUF_DATA(querybuf); + len = MYSQL_GET_PACKET_LEN(data)-1; /*< distract 1 for packet type byte */ + query_str = (char *)malloc(len+1); + + if (query_str == NULL) + { + /** Free parsing info data */ + parsing_info_done(pi); + succp = false; + goto retblock; + } + memcpy(query_str, &data[5], len); + memset(&query_str[len], 0, 1); + parsing_info_set_plain_str(pi, query_str); + + /** Get one or create new THD object to be use in parsing */ + thd = get_or_create_thd_for_parsing((MYSQL *)pi->pi_handle, query_str); + + if (thd == NULL) + { + /** Free parsing info data */ + parsing_info_done(pi); + succp = false; + goto retblock; + } + /** + * Create parse_tree inside thd. + * thd and lex are readable even if creating parse tree fails. + */ + create_parse_tree(thd); + /** Add complete parsing info struct to the query buffer */ + gwbuf_add_buffer_object(querybuf, + GWBUF_PARSING_INFO, + (void *)pi, + parsing_info_done); + + succp = true; +retblock: + return succp; +} +/** + * If buffer has non-NULL gwbuf_parsing_info it is parsed and it has parsing + * information included. + * + * @param buf buffer being examined + * + * @return true or false + */ +bool query_is_parsed( + GWBUF* buf) +{ + CHK_GWBUF(buf); + return GWBUF_IS_PARSED(buf); +} -/** - * @node (write brief function description here) + +/** + * Create a thread context, thd, init embedded server, connect to it, and allocate + * query to thd. * * Parameters: - * @param mysql - - * - * - * @param query_str - - * - * - * @return - * + * @param mysql Database handle * - * @details (write detailed description here) + * @param query_str Query in plain txt string + * + * @return Thread context pointer * */ static THD* get_or_create_thd_for_parsing( @@ -369,9 +406,9 @@ return_here: * restrictive, for example, QUERY_TYPE_READ is smaller than QUERY_TYPE_WRITE. * */ -static u_int16_t set_query_type( - u_int16_t* qtype, - u_int16_t new_type) +static u_int32_t set_query_type( + u_int32_t* qtype, + u_int32_t new_type) { *qtype = MAX(*qtype, new_type); return *qtype; @@ -397,7 +434,7 @@ static skygw_query_type_t resolve_query_type( THD* thd) { skygw_query_type_t qtype = QUERY_TYPE_UNKNOWN; - u_int16_t type = QUERY_TYPE_UNKNOWN; + u_int32_t type = QUERY_TYPE_UNKNOWN; int set_autocommit_stmt = -1; /*< -1 no, 0 disable, 1 enable */ LEX* lex; Item* item; @@ -412,7 +449,7 @@ static skygw_query_type_t resolve_query_type( ss_info_dassert(thd != NULL, ("thd is NULL\n")); - force_data_modify_op_replication = FALSE; + force_data_modify_op_replication = FALSE; lex = thd->lex; /** SELECT ..INTO variable|OUTFILE|DUMPFILE */ @@ -464,19 +501,51 @@ static skygw_query_type_t resolve_query_type( type |= QUERY_TYPE_DISABLE_AUTOCOMMIT; type |= QUERY_TYPE_BEGIN_TRX; } - /** - * REVOKE ALL, ASSIGN_TO_KEYCACHE, - * PRELOAD_KEYS, FLUSH, RESET, CREATE|ALTER|DROP SERVER - */ + if (lex->option_type == OPT_GLOBAL) { - type |= QUERY_TYPE_GLOBAL_WRITE; - goto return_qtype; + /** + * SHOW syntax http://dev.mysql.com/doc/refman/5.6/en/show.html + */ + if (lex->sql_command == SQLCOM_SHOW_VARIABLES) + { + type |= QUERY_TYPE_GSYSVAR_READ; + } + /** + * SET syntax http://dev.mysql.com/doc/refman/5.6/en/set-statement.html + */ + else if (lex->sql_command == SQLCOM_SET_OPTION) + { + type |= QUERY_TYPE_GSYSVAR_WRITE; + } + /** + * REVOKE ALL, ASSIGN_TO_KEYCACHE, + * PRELOAD_KEYS, FLUSH, RESET, CREATE|ALTER|DROP SERVER + */ + else + { + type |= QUERY_TYPE_GSYSVAR_WRITE; + } + goto return_qtype; } else if (lex->option_type == OPT_SESSION) { - type |= QUERY_TYPE_SESSION_WRITE; - goto return_qtype; + /** + * SHOW syntax http://dev.mysql.com/doc/refman/5.6/en/show.html + */ + if (lex->sql_command == SQLCOM_SHOW_VARIABLES) + { + type |= QUERY_TYPE_SYSVAR_READ; + } + /** + * SET syntax http://dev.mysql.com/doc/refman/5.6/en/set-statement.html + */ + else if (lex->sql_command == SQLCOM_SET_OPTION) + { + /** Either user- or system variable write */ + type |= QUERY_TYPE_SESSION_WRITE; + } + goto return_qtype; } /** * 1:ALTER TABLE, TRUNCATE, REPAIR, OPTIMIZE, ANALYZE, CHECK. @@ -493,23 +562,26 @@ static skygw_query_type_t resolve_query_type( if (thd->variables.sql_log_bin == 0 && force_data_modify_op_replication) { + /** Not replicated */ type |= QUERY_TYPE_SESSION_WRITE; - } else { - type |= QUERY_TYPE_WRITE; + } + else + { + /** Written to binlog, that is, replicated except tmp tables */ + type |= QUERY_TYPE_WRITE; /*< to master */ + + if (lex->create_info.options & HA_LEX_CREATE_TMP_TABLE && + lex->sql_command == SQLCOM_CREATE_TABLE) + { + type |= QUERY_TYPE_CREATE_TMP_TABLE; /*< remember in router */ + } } - goto return_qtype; } /** Try to catch session modifications here */ switch (lex->sql_command) { - case SQLCOM_SET_OPTION: /*< SET commands. */ - if (lex->option_type == OPT_GLOBAL) - { - type |= QUERY_TYPE_GLOBAL_WRITE; - break; - } - /**name != NULL && + strcmp(item->name, "last_insert_id()") == 0) + { + func_qtype |= QUERY_TYPE_MASTER_READ; + } + else + { + func_qtype |= QUERY_TYPE_READ; + } /** * Many built-in functions are of this * type, for example, rand(), soundex(), @@ -684,6 +802,7 @@ static skygw_query_type_t resolve_query_type( /**< Set new query type */ type |= set_query_type(&type, func_qtype); } +#if defined(UPDATE_VAR_SUPPORT) /** * Write is as restrictive as it gets due functions, * so break. @@ -691,8 +810,9 @@ static skygw_query_type_t resolve_query_type( if ((type & QUERY_TYPE_WRITE) == QUERY_TYPE_WRITE) { break; } +#endif } /**< for */ - } /**< if */ + } /**< if */ return_qtype: qtype = (skygw_query_type_t)type; return qtype; @@ -816,3 +936,412 @@ char* skygw_query_classifier_get_stmtname( return ((THD *)(mysql->thd))->lex->prepared_stmt_name.str; } + +/** + *Returns the LEX struct of the parsed GWBUF + *@param The parsed GWBUF + *@return Pointer to the LEX struct or NULL if an error occurred or the query was not parsed + */ +LEX* get_lex(GWBUF* querybuf) +{ + + parsing_info_t* pi; + MYSQL* mysql; + THD* thd; + + if (!GWBUF_IS_PARSED(querybuf)) + { + return NULL; + } + pi = (parsing_info_t *)gwbuf_get_buffer_object_data(querybuf, + GWBUF_PARSING_INFO); + + if (pi == NULL) + { + return NULL; + } + + if ((mysql = (MYSQL *)pi->pi_handle) == NULL || + (thd = (THD *)mysql->thd) == NULL) + { + ss_dassert(mysql != NULL && + thd != NULL); + return NULL; + } + + return thd->lex; +} + + + +/** + * Finds the head of the list of tables affected by the current select statement. + * @param thd Pointer to a valid THD + * @return Pointer to the head of the TABLE_LIST chain or NULL in case of an error + */ +void* skygw_get_affected_tables(void* lexptr) +{ + LEX* lex = (LEX*)lexptr; + + if(lex == NULL || + lex->current_select == NULL) + { + ss_dassert(lex != NULL && + lex->current_select != NULL); + return NULL; + } + + return (void*)lex->current_select->table_list.first; +} + + +/** + * Reads the parsetree and lists all the affected tables and views in the query. + * In the case of an error, the size of the table is set to zero and no memory is allocated. + * The caller must free the allocated memory. + * + * @param querybuf GWBUF where the table names are extracted from + * @param tblsize Pointer where the number of tables is written + * @return Array of null-terminated strings with the table names + */ +char** skygw_get_table_names(GWBUF* querybuf,int* tblsize, bool fullnames) +{ + LEX* lex; + TABLE_LIST* tbl; + int i = 0, + currtblsz = 0; + char **tables, + **tmp; + + if((lex = get_lex(querybuf)) == NULL) + { + goto retblock; + } + + lex->current_select = lex->all_selects_list; + + while(lex->current_select){ + + tbl = (TABLE_LIST*)skygw_get_affected_tables(lex); + + while (tbl) + { + if(i >= currtblsz){ + + tmp = (char**)malloc(sizeof(char*)*(currtblsz*2+1)); + + if(tmp){ + if(currtblsz > 0){ + int x; + for(x = 0;xdb && strcmp(tbl->db,"skygw_virtual") != 0) + { + catnm = (char*)calloc(strlen(tbl->db) + strlen(tbl->table_name) + 2,sizeof(char)); + strcpy(catnm,tbl->db); + strcat(catnm,"."); + strcat(catnm,tbl->table_name); + } + } + + if(catnm) + { + tables[i++] = catnm; + } + else + { + tables[i++] = strdup(tbl->table_name); + } + + tbl=tbl->next_local; + } + lex->current_select = lex->current_select->next_select_in_list(); + } + + retblock: + *tblsize = i; + return tables; +} + +/** + * Extract, allocate memory and copy the name of the created table. + * @param querybuf Buffer to use. + * @return A pointer to the name if a table was created, otherwise NULL + */ +char* skygw_get_created_table_name(GWBUF* querybuf) +{ + LEX* lex; + + if((lex = get_lex(querybuf)) == NULL) + { + return NULL; + } + + if(lex->create_last_non_select_table && + lex->create_last_non_select_table->table_name){ + char* name = strdup(lex->create_last_non_select_table->table_name); + return name; + }else{ + return NULL; + } + +} + +/** + * Checks whether the query is a "real" query ie. SELECT,UPDATE,INSERT,DELETE or any variation of these. + * Queries that affect the underlying database are not considered as real queries and the queries that target + * specific row or variable data are regarded as the real queries. + * @param GWBUF to analyze + * @return true if the query is a real query, otherwise false + */ +bool skygw_is_real_query(GWBUF* querybuf) +{ + LEX* lex = get_lex(querybuf); + if(lex){ + switch(lex->sql_command){ + case SQLCOM_SELECT: + return lex->all_selects_list->table_list.elements > 0; + case SQLCOM_UPDATE: + case SQLCOM_INSERT: + case SQLCOM_INSERT_SELECT: + case SQLCOM_DELETE: + case SQLCOM_TRUNCATE: + case SQLCOM_REPLACE: + case SQLCOM_REPLACE_SELECT: + case SQLCOM_PREPARE: + case SQLCOM_EXECUTE: + return true; + default: + return false; + } + } + return false; +} + + +/** + * Checks whether the buffer contains a DROP TABLE... query. + * @param querybuf Buffer to inspect + * @return true if it contains the query otherwise false + */ +bool is_drop_table_query(GWBUF* querybuf) +{ + LEX* lex; + + return (lex = get_lex(querybuf)) != NULL && + lex->sql_command == SQLCOM_DROP_TABLE; +} + +/* + * Replace user-provided literals with question marks. Return a copy of the + * querystr with replacements. + * + * @param querybuf GWBUF buffer including necessary parsing info + * + * @return Copy of querystr where literals are replaces with question marks or + * NULL if querystr is NULL, thread context or lex are NULL or if replacement + * function fails. + * + * Replaced literal types are STRING_ITEM,INT_ITEM,DECIMAL_ITEM,REAL_ITEM, + * VARBIN_ITEM,NULL_ITEM + */ +char* skygw_get_canonical( + GWBUF* querybuf) +{ + parsing_info_t* pi; + MYSQL* mysql; + THD* thd; + LEX* lex; + Item* item; + char* querystr; + + if (!GWBUF_IS_PARSED(querybuf)) + { + querystr = NULL; + goto retblock; + } + pi = (parsing_info_t *)gwbuf_get_buffer_object_data(querybuf, + GWBUF_PARSING_INFO); + CHK_PARSING_INFO(pi); + + if (pi == NULL) + { + querystr = NULL; + goto retblock; + } + + if (pi->pi_query_plain_str == NULL || + (mysql = (MYSQL *)pi->pi_handle) == NULL || + (thd = (THD *)mysql->thd) == NULL || + (lex = thd->lex) == NULL) + { + ss_dassert(pi->pi_query_plain_str != NULL && + mysql != NULL && + thd != NULL && + lex != NULL); + querystr = NULL; + goto retblock; + } + querystr = strdup(pi->pi_query_plain_str); + + for (item=thd->free_list; item != NULL; item=item->next) + { + Item::Type itype; + + if (item->name == NULL) + { + continue; + } + itype = item->type(); + + if (itype == Item::STRING_ITEM) + { + String tokenstr; + String* res = item->val_str_ascii(&tokenstr); + + if (res->is_empty()) /*< empty string */ + { + querystr = replace_literal(querystr, "\"\"", "\"?\""); + } + else + { + querystr = replace_literal(querystr, res->ptr(), "?"); + } + } + else if (itype == Item::INT_ITEM || + itype == Item::DECIMAL_ITEM || + itype == Item::REAL_ITEM || + itype == Item::VARBIN_ITEM || + itype == Item::NULL_ITEM) + { + querystr = replace_literal(querystr, item->name, "?"); + } + } /*< for */ +retblock: + return querystr; +} + + +/** + * Create parsing information; initialize mysql handle, allocate parsing info + * struct and set handle and free function pointer to it. + * + * @param donefun pointer to free function + * + * @return pointer to parsing information + */ +parsing_info_t* parsing_info_init( + void (*donefun)(void *)) +{ + parsing_info_t* pi = NULL; + MYSQL* mysql; + const char* user = "skygw"; + const char* db = "skygw"; + + ss_dassert(donefun != NULL); + + /** Get server handle */ + mysql = mysql_init(NULL); + ss_dassert(mysql != NULL); + + if (mysql == NULL) { + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Error : call to mysql_real_connect failed due %d, %s.", + mysql_errno(mysql), + mysql_error(mysql)))); + + goto retblock; + } + /** Set methods and authentication to mysql */ + mysql_options(mysql, MYSQL_READ_DEFAULT_GROUP, "libmysqld_skygw"); + mysql_options(mysql, MYSQL_OPT_USE_EMBEDDED_CONNECTION, NULL); + mysql->methods = &embedded_methods; + mysql->user = my_strdup(user, MYF(0)); + mysql->db = my_strdup(db, MYF(0)); + mysql->passwd = NULL; + + pi = (parsing_info_t*)calloc(1, sizeof(parsing_info_t)); + + if (pi == NULL) + { + mysql_close(mysql); + goto retblock; + } +#if defined(SS_DEBUG) + pi->pi_chk_top = CHK_NUM_PINFO; + pi->pi_chk_tail = CHK_NUM_PINFO; +#endif + /** Set handle and free function to parsing info struct */ + pi->pi_handle = mysql; + pi->pi_done_fp = donefun; + +retblock: + return pi; +} + +/** + * Free function for parsing info. Called by gwbuf_free or in case initialization + * of parsing information fails. + * + * @param ptr Pointer to parsing information, cast required + * + * @return void + * + */ +void parsing_info_done( + void* ptr) +{ + parsing_info_t* pi = (parsing_info_t *)ptr; + + if (pi->pi_handle != NULL) + { + MYSQL* mysql = (MYSQL *)pi->pi_handle; + + if (mysql->thd != NULL) + { + (*mysql->methods->free_embedded_thd)(mysql); + mysql->thd = NULL; + } + mysql_close(mysql); + } + /** Free plain text query string */ + if (pi->pi_query_plain_str != NULL) + { + free(pi->pi_query_plain_str); + } + free(pi); +} + +/** + * Add plain text query string to parsing info. + * + * @param ptr Pointer to parsing info struct, cast required + * @param str String to be added + * + * @return void + */ +static void parsing_info_set_plain_str( + void* ptr, + char* str) +{ + parsing_info_t* pi = (parsing_info_t *)ptr; + CHK_PARSING_INFO(pi); + + pi->pi_query_plain_str = str; +} diff --git a/query_classifier/query_classifier.h b/query_classifier/query_classifier.h index 31f9cf44e..4ad960524 100644 --- a/query_classifier/query_classifier.h +++ b/query_classifier/query_classifier.h @@ -20,7 +20,8 @@ Copyright SkySQL Ab /** getpid */ #include #include -#include "../utils/skygw_utils.h" +#include +#include EXTERN_C_BLOCK_BEGIN @@ -30,36 +31,67 @@ EXTERN_C_BLOCK_BEGIN * is modified */ typedef enum { - QUERY_TYPE_UNKNOWN = 0x0000, /*< Initial value, can't be tested bitwisely */ - QUERY_TYPE_LOCAL_READ = 0x0001, /*< Read non-database data, execute in MaxScale */ - QUERY_TYPE_READ = 0x0002, /*< No updates */ - QUERY_TYPE_WRITE = 0x0004, /*< Master data will be modified */ - QUERY_TYPE_SESSION_WRITE = 0x0008, /*< Session data will be modified */ - QUERY_TYPE_GLOBAL_WRITE = 0x0010, /*< Global system variable modification */ - QUERY_TYPE_BEGIN_TRX = 0x0020, /*< BEGIN or START TRANSACTION */ - QUERY_TYPE_ENABLE_AUTOCOMMIT = 0x0040, /*< SET autocommit=1 */ - QUERY_TYPE_DISABLE_AUTOCOMMIT = 0x0080, /*< SET autocommit=0 */ - QUERY_TYPE_ROLLBACK = 0x0100, /*< ROLLBACK */ - QUERY_TYPE_COMMIT = 0x0200, /*< COMMIT */ - QUERY_TYPE_PREPARE_NAMED_STMT = 0x0400, /*< Prepared stmt with name from user */ - QUERY_TYPE_PREPARE_STMT = 0x0800, /*< Prepared stmt with id provided by server */ - QUERY_TYPE_EXEC_STMT = 0x1000 /*< Execute prepared statement */ + QUERY_TYPE_UNKNOWN = 0x000000, /*< Initial value, can't be tested bitwisely */ + QUERY_TYPE_LOCAL_READ = 0x000001, /*< Read non-database data, execute in MaxScale:any */ + QUERY_TYPE_READ = 0x000002, /*< Read database data:any */ + QUERY_TYPE_WRITE = 0x000004, /*< Master data will be modified:master */ + QUERY_TYPE_MASTER_READ = 0x000008, /*< Read from the master:master */ + QUERY_TYPE_SESSION_WRITE = 0x000010, /*< Session data will be modified:master or all */ + /** Not implemented yet */ +// QUERY_TYPE_USERVAR_WRITE = 0x000020, /*< Write a user variable:master or all */ + QUERY_TYPE_USERVAR_READ = 0x000040, /*< Read a user variable:master or any */ + QUERY_TYPE_SYSVAR_READ = 0x000080, /*< Read a system variable:master or any */ + /** Not implemented yet */ +// QUERY_TYPE_SYSVAR_WRITE = 0x000100, /*< Write a system variable:master or all */ + QUERY_TYPE_GSYSVAR_READ = 0x000200, /*< Read global system variable:master or any */ + QUERY_TYPE_GSYSVAR_WRITE = 0x000400, /*< Write global system variable:master or all */ + QUERY_TYPE_BEGIN_TRX = 0x000800, /*< BEGIN or START TRANSACTION */ + QUERY_TYPE_ENABLE_AUTOCOMMIT = 0x001000, /*< SET autocommit=1 */ + QUERY_TYPE_DISABLE_AUTOCOMMIT = 0x002000, /*< SET autocommit=0 */ + QUERY_TYPE_ROLLBACK = 0x004000, /*< ROLLBACK */ + QUERY_TYPE_COMMIT = 0x008000, /*< COMMIT */ + QUERY_TYPE_PREPARE_NAMED_STMT = 0x010000, /*< Prepared stmt with name from user:all */ + QUERY_TYPE_PREPARE_STMT = 0x020000, /*< Prepared stmt with id provided by server:all */ + QUERY_TYPE_EXEC_STMT = 0x040000, /*< Execute prepared statement:master or any */ + QUERY_TYPE_CREATE_TMP_TABLE = 0x080000, /*< Create temporary table:master (could be all) */ + QUERY_TYPE_READ_TMP_TABLE = 0x100000 /*< Read temporary table:master (could be any) */ } skygw_query_type_t; + +typedef struct parsing_info_st { +#if defined(SS_DEBUG) + skygw_chk_t pi_chk_top; +#endif + void* pi_handle; /*< parsing info object pointer */ + char* pi_query_plain_str; /*< query as plain string */ + void (*pi_done_fp)(void *); /*< clean-up function for parsing info */ +#if defined(SS_DEBUG) + skygw_chk_t pi_chk_tail; +#endif +} parsing_info_t; + + #define QUERY_IS_TYPE(mask,type) ((mask & type) == type) /** * Create THD and use it for creating parse tree. Examine parse tree and * classify the query. */ -skygw_query_type_t skygw_query_classifier_get_type( - const char* query_str, - unsigned long client_flags, - MYSQL** mysql); +skygw_query_type_t query_classifier_get_type(GWBUF* querybuf); /** Free THD context and close MYSQL */ -void skygw_query_classifier_free(MYSQL* mysql); -char* skygw_query_classifier_get_stmtname(MYSQL* mysql); +char* skygw_query_classifier_get_stmtname(MYSQL* mysql); +char* skygw_get_created_table_name(GWBUF* querybuf); +bool is_drop_table_query(GWBUF* querybuf); +bool skygw_is_real_query(GWBUF* querybuf); +void* skygw_get_affected_tables(void* lexptr); +char** skygw_get_table_names(GWBUF* querybuf,int* tblsize,bool fullnames); +char* skygw_get_canonical(GWBUF* querybuf); +bool parse_query (GWBUF* querybuf); +parsing_info_t* parsing_info_init(void (*donefun)(void *)); +void parsing_info_done(void* ptr); +bool query_is_parsed(GWBUF* buf); + EXTERN_C_BLOCK_END diff --git a/query_classifier/test/canonical_tests/Makefile b/query_classifier/test/canonical_tests/Makefile new file mode 100644 index 000000000..c5fe44fe9 --- /dev/null +++ b/query_classifier/test/canonical_tests/Makefile @@ -0,0 +1,62 @@ +# cleantests - clean local and subdirectories' tests +# buildtests - build all local and subdirectories' tests +# runtests - run all local tests +# testall - clean, build and run local and subdirectories' tests + +include ../../../build_gateway.inc +include ../../../makefile.inc +include ../../../test.inc + +CC = gcc +CPP = g++ + +TESTPATH := $(shell pwd) +TESTLOG := $(TESTPATH)/testqclass.log +QUERY_CLASSIFIER_PATH := $(ROOT_PATH)/query_classifier +LOG_MANAGER_PATH := $(ROOT_PATH)/log_manager +UTILS_PATH := $(ROOT_PATH)/utils +CORE_PATH := $(ROOT_PATH)/server/core +TESTAPP = $(TESTPATH)/canonizer + +LDFLAGS=-L$(QUERY_CLASSIFIER_PATH) \ + -L$(LOG_MANAGER_PATH) \ + -L$(EMBEDDED_LIB) \ + -Wl,-rpath,$(DEST)/lib \ + -Wl,-rpath,$(EMBEDDED_LIB) \ + -Wl,-rpath,$(LOG_MANAGER_PATH) \ + -Wl,-rpath,$(QUERY_CLASSIFIER_PATH) + +LIBS=-lstdc++ -lpthread -lquery_classifier -lz -ldl -lssl -laio -lcrypt -lcrypto -lrt -lm \ + -llog_manager $(UTILS_PATH)/skygw_utils.o $(CORE_PATH)/buffer.o $(CORE_PATH)/atomic.o $(CORE_PATH)/spinlock.o $(CORE_PATH)/hint.o + +CFLAGS=-g $(MYSQL_HEADERS) \ + -I$(QUERY_CLASSIFIER_PATH) \ + $(MYSQL_HEADERS) \ + -I$(ROOT_PATH)/server/include \ + -I$(UTILS_PATH) + + +testall: + $(MAKE) cleantests + $(MAKE) buildtests + $(MAKE) runtests + +cleantests: + - $(DEL) *.o + - $(DEL) *~ + - $(DEL) canonizer + - $(DEL) aria_log* + - $(DEL) ib* + +buildtests: $(OBJS) + cp $(ERRMSG)/errmsg.sys . + $(CC) $(CFLAGS) $(LDFLAGS) $(LIBS) canonizer.c -o $(TESTAPP) $(LDLIBS) $(LDMYSQL) + +runtests: + @echo "" > $(TESTLOG) + @echo "-------------------------------" >> $(TESTLOG) + @echo $(shell date) >> $(TESTLOG) + @echo "Canonical Query Tests" >> $(TESTLOG) + @echo "-------------------------------" >> $(TESTLOG) + @echo "" >> $(TESTLOG) + ./canontest.sh $(TESTLOG) input.sql output.sql expected.sql diff --git a/query_classifier/test/canonical_tests/canonizer.c b/query_classifier/test/canonical_tests/canonizer.c new file mode 100644 index 000000000..b9b17222f --- /dev/null +++ b/query_classifier/test/canonical_tests/canonizer.c @@ -0,0 +1,120 @@ +#include +#include +#include +#include +#include +#include +#include + +static char* server_options[] = { + "SkySQL Gateway", + "--datadir=./", + "--language=./", + "--skip-innodb", + "--default-storage-engine=myisam", + NULL +}; + +const int num_elements = (sizeof(server_options) / sizeof(char *)) - 1; + +static char* server_groups[] = { + "embedded", + "server", + "server", + NULL +}; + +int main(int argc, char** argv) +{ + + int fdin,fdout,i=0,fnamelen,fsz,lines = 0; + unsigned int psize; + GWBUF** qbuff; + char *qin, *outnm, *buffer, *tok; + + if(argc != 3){ + printf("Usage: canonizer \n"); + return 1; + } + + + + bool failed = mysql_library_init(num_elements, server_options, server_groups); + + if(failed){ + printf("Embedded server init failed.\n"); + return 1; + } + + fnamelen = strlen(argv[1]) + 16; + fdin = open(argv[1],O_RDONLY); + fsz = lseek(fdin,0,SEEK_END); + lseek(fdin,0,SEEK_SET); + + if(!(buffer = malloc(sizeof(char)*fsz))){ + printf("Error: Failed to allocate memory."); + return 1; + } + + read(fdin,buffer,fsz); + + + + i = 0; + int bsz = 4,z=0; + qbuff = calloc(bsz,sizeof(GWBUF*)); + tok = strtok(buffer,"\n"); + + while(tok){ + + if(i>=bsz){ + GWBUF** tmp = calloc(bsz*2,sizeof(GWBUF*)); + if(!tmp){ + printf("Error: Failed to allocate memory."); + return 1; + } + + for(z=0;z 0){ + qin = strdup(tok); + psize = strlen(qin); + qbuff[i] = gwbuf_alloc(psize + 6); + *(qbuff[i]->sbuf->data + 0) = (unsigned char)psize; + *(qbuff[i]->sbuf->data + 1) = (unsigned char)(psize>>8); + *(qbuff[i]->sbuf->data + 2) = (unsigned char)(psize>>16); + *(qbuff[i]->sbuf->data + 4) = 0x03; + memcpy(qbuff[i]->sbuf->data + 5,qin,psize); + *(qbuff[i]->sbuf->data + 5 + psize) = 0x00; + tok = strtok(NULL,"\n\0"); + free(qin); + i++; + } + } + + fdout = open(argv[2],O_TRUNC|O_CREAT|O_WRONLY,S_IRWXU|S_IXGRP|S_IXOTH); + + for(i = 0;i " + exit 0 +fi +TESTLOG=$1 +INPUT=$2 +OUTPUT=$3 +EXPECTED=$4 +DIFFLOG=diff.out +$PWD/canonizer $INPUT $OUTPUT +diff $OUTPUT $EXPECTED > $DIFFLOG +if [ $? -eq 0 ] +then + echo "PASSED" >> $TESTLOG +else + echo "FAILED" >> $TESTLOG + echo "Diff output: " >> $TESTLOG + cat $DIFFLOG >> $TESTLOG +fi diff --git a/query_classifier/test/canonical_tests/expected.sql b/query_classifier/test/canonical_tests/expected.sql new file mode 100755 index 000000000..fabb27dc7 --- /dev/null +++ b/query_classifier/test/canonical_tests/expected.sql @@ -0,0 +1,17 @@ +select md5(?) =?, sleep(?), rand(?); +select * from my1 where md5(?) =?; +select md5(?) =?; +select * from my1 where md5(?) =?; +select sleep(?) +select * from tst where lname='?' +select ?,?,?,?,?,? from tst +select * from tst where fname like '?' +select * from tst where lname like '?' order by fname +insert into tst values ("?","?"),("?",?),("?","?") +drop table if exists tst +create table tst(fname varchar(30), lname varchar(30)) +update tst set lname="?" where fname like '?' or lname like '?' +delete from tst where lname like '?' and fname like '?' +select ? from tst where fname='?' or lname like '?' +select ?,?,?,? from tst where name='?' or name='?' or name='?' +select count(?),count(?),count(?),count(?),count (?),count(?) from tst diff --git a/query_classifier/test/canonical_tests/input.sql b/query_classifier/test/canonical_tests/input.sql new file mode 100755 index 000000000..59e9a2496 --- /dev/null +++ b/query_classifier/test/canonical_tests/input.sql @@ -0,0 +1,17 @@ +select md5("200000foo") =10, sleep(2), rand(100); +select * from my1 where md5("110") =10; +select md5("100foo") =10; +select * from my1 where md5("100") =10; +select sleep(2); +select * from tst where lname='Doe'; +select 1,2,3,4,5,6 from tst; +select * from tst where fname like '%a%'; +select * from tst where lname like '%e%' order by fname; +insert into tst values ("John","Doe"),("Plato",null),("Nietzsche",""); +drop table if exists tst; +create table tst(fname varchar(30), lname varchar(30)); +update tst set lname="Human" where fname like '%a%' or lname like '%a%'; +delete from tst where lname like '%man%' and fname like '%ard%'; +select 100 from tst where fname='10' or lname like '%100%'; +select 1,20,300,4000 from tst where name='1000' or name='200' or name='30' or name='4'; +select count(1),count(10),count(100),count(2),count (20),count(200) from tst; diff --git a/query_classifier/test/makefile b/query_classifier/test/makefile index d6a5cce52..c42a795e3 100644 --- a/query_classifier/test/makefile +++ b/query_classifier/test/makefile @@ -18,7 +18,7 @@ UTILS_PATH := $(ROOT_PATH)/utils TESTAPP = $(TESTPATH)/testmain testall:buildtests - + $(MAKE) -C canonical_tests testall testalllaters: $(MAKE) cleantests $(MAKE) DEBUG=Y DYNLIB=Y buildtests @@ -80,4 +80,4 @@ ifeq ($?, 0) else @echo "Query Classifier FAILED" >> $(TESTLOG) endif - @cat $(TESTLOG) >> $(TEST_MAXSCALE_LOG) \ No newline at end of file + @cat $(TESTLOG) >> $(TEST_MAXSCALE_LOG) diff --git a/rabbitmq_consumer/CMakeLists.txt b/rabbitmq_consumer/CMakeLists.txt new file mode 100644 index 000000000..30d16d630 --- /dev/null +++ b/rabbitmq_consumer/CMakeLists.txt @@ -0,0 +1,44 @@ +cmake_minimum_required (VERSION 2.6) + +set(CMAKE_LIBRARY_PATH ${CMAKE_LIBRARY_PATH} /usr/lib /usr/lib64 /usr/local/lib /usr/local/lib64 /usr/lib/mariadb /usr/lib64/mariadb) +set(CMAKE_INCLUDE_PATH ${CMAKE_INCLUDE_PATH} /usr/include /usr/local/include /usr/include/mysql /usr/local/include/mysql /usr/include/mariadb /usr/local/include/mariadb) + +include(InstallRequiredSystemLibraries) + +project (consumer) + +find_path(MYSQL_INCLUDE_DIRS mysql.h) +find_library(MYSQL_LIBRARIES NAMES mysqlclient) +find_library(RABBITMQ_C_LIBRARIES NAMES rabbitmq) + +include_directories(${MYSQL_INCLUDE_DIRS}) +include_directories(${RABBITMQ_C_INCLUDE_DIRS}) +include_directories(${CMAKE_SOURCE_DIR}/inih) + +add_subdirectory (inih) +link_directories(${CMAKE_SOURCE_DIR}/inih) + +if(RABBITMQ_C_LIBRARIES AND MYSQL_LIBRARIES AND MYSQL_INCLUDE_DIRS) + +add_executable (consumer consumer.c ${MYSQL_LIBRARIES} ${RABBITMQ_C_LIBRARIES}) +target_link_libraries(consumer mysqlclient) +target_link_libraries(consumer rabbitmq) +target_link_libraries(consumer inih) +install(TARGETS consumer DESTINATION bin) +install(FILES consumer.cnf DESTINATION share/consumer) + + +else(RABBITMQ_C_LIBRARIES AND MYSQL_LIBRARIES AND MYSQL_INCLUDE_DIRS) +message(FATAL_ERROR "Error: Can not find requred libraries: libmysqld, librabbitmq.") + +endif(RABBITMQ_C_LIBRARIES AND MYSQL_LIBRARIES AND MYSQL_INCLUDE_DIRS) + +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "RabbitMQ Consumer Client") +set(CPACK_PACKAGE_NAME "RabbitMQ Consumer") +set(CPACK_GENERATOR "RPM") +set(CPACK_PACKAGE_VERSION_MAJOR "1") +set(CPACK_PACKAGE_VERSION_MINOR "0") +set(CPACK_RPM_PACKAGE_NAME "rabbitmq-consumer") +set(CPACK_RPM_PACKAGE_VENDOR "SkySQL Ab") +set(CPACK_RPM_PACKAGE_AUTOREQPROV " no") +include(CPack) \ No newline at end of file diff --git a/rabbitmq_consumer/Makefile b/rabbitmq_consumer/Makefile new file mode 100644 index 000000000..456727ef8 --- /dev/null +++ b/rabbitmq_consumer/Makefile @@ -0,0 +1,15 @@ +include buildconfig.inc + +CC=gcc +CFLAGS=-c -Wall -g -Iinih $(INCLUDE_DIRS) +LDFLAGS= $(LIBRARY_DIRS) -lrabbitmq -lmysqlclient +SRCS= inih/ini.c consumer.c +OBJ=$(SRCS:.c=.o) +all:$(OBJ) + $(CC) $(LDFLAGS) $(OBJ) -o consumer `mysql_config --cflags --libs` +%.o:%.c + $(CC) $(CFLAGS) $< -o $@ + +clean: + -rm *.o + -rm *~ diff --git a/rabbitmq_consumer/README b/rabbitmq_consumer/README new file mode 100644 index 000000000..6dc2fb982 --- /dev/null +++ b/rabbitmq_consumer/README @@ -0,0 +1,39 @@ +This program requires the librabbitmq and libmysqlclient libraries. + +librabbitmq-c - https://github.com/alanxz/rabbitmq-c +MariaDB Client Library for C 2.0 Series - https://mariadb.com/kb/en/mariadb/client-libraries/client-library-for-c/ + +Building with CMake: +'cmake .' + +Variables to pass for CMake: + +Path to headers -DCMAKE_INCLUDE_PATH= +Path to libraries -DCMAKE_LIBRARY_PATH= +Install prefix -DCMAKE_INSTALL_PREFIX= + + +Separate multiple folders with colons, for example: +'path1:path2:path3' + +After running CMake run 'make' to build the binaries and 'make package' to build RPMs. + +To build without CMake, use the provided makefile and update the +include and library directories 'in buildvars.inc' + +The configuration for the consumer client are red from 'consumer.cnf'. + +Options for the configuration file: + +hostname Hostname of the RabbitMQ server +port Port of the RabbitMQ server +vhost Virtual host location of the RabbitMQ server +user Username for the RabbitMQ server +passwd Password for the RabbitMQ server +queue Queue to consume from +dbserver Hostname of the SQL server +dbport Port of the SQL server +dbname Name of the SQL database to use +dbuser Database username +dbpasswd Database passwork +logfile Message log filename diff --git a/rabbitmq_consumer/buildconfig.inc b/rabbitmq_consumer/buildconfig.inc new file mode 100644 index 000000000..ab0f2f887 --- /dev/null +++ b/rabbitmq_consumer/buildconfig.inc @@ -0,0 +1,8 @@ +#Use the '-I' prefix for include and '-L' for library directories +#You can use multiple library and include directories + +#Path to the rabbitmq-c and mysqlclient libraries +LIBRARY_DIRS :=-L/usr/lib64 + +#path to headers +INCLUDE_DIRS :=-I/usr/include -I/usr/include/mysql \ No newline at end of file diff --git a/rabbitmq_consumer/consumer.c b/rabbitmq_consumer/consumer.c new file mode 100644 index 000000000..8ccabf401 --- /dev/null +++ b/rabbitmq_consumer/consumer.c @@ -0,0 +1,524 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef struct delivery_t +{ + uint64_t dtag; + amqp_message_t* message; + struct delivery_t *next,*prev; +}DELIVERY; + +typedef struct consumer_t +{ + char *hostname,*vhost,*user,*passwd,*queue,*dbserver,*dbname,*dbuser,*dbpasswd; + DELIVERY* query_stack; + int port,dbport; +}CONSUMER; + +static int all_ok; +static FILE* out_fd; +static CONSUMER* c_inst; +static char* DB_DATABASE = "CREATE DATABASE IF NOT EXISTS %s;"; +static char* DB_TABLE = "CREATE TABLE IF NOT EXISTS pairs (tag VARCHAR(64) PRIMARY KEY NOT NULL, query VARCHAR(2048), reply VARCHAR(2048), date_in DATETIME NOT NULL, date_out DATETIME DEFAULT NULL, counter INT DEFAULT 1)"; +static char* DB_INSERT = "INSERT INTO pairs(tag, query, date_in) VALUES ('%s','%s',FROM_UNIXTIME(%s))"; +static char* DB_UPDATE = "UPDATE pairs SET reply='%s', date_out=FROM_UNIXTIME(%s) WHERE tag='%s'"; +static char* DB_INCREMENT = "UPDATE pairs SET counter = counter+1, date_out=FROM_UNIXTIME(%s) WHERE query='%s'"; + +void sighndl(int signum) +{ + if(signum == SIGINT){ + all_ok = 0; + alarm(1); + } +} + +int handler(void* user, const char* section, const char* name, + const char* value) +{ + if(strcmp(section,"consumer") == 0){ + + if(strcmp(name,"hostname") == 0){ + c_inst->hostname = strdup(value); + }else if(strcmp(name,"vhost") == 0){ + c_inst->vhost = strdup(value); + }else if(strcmp(name,"port") == 0){ + c_inst->port = atoi(value); + }else if(strcmp(name,"user") == 0){ + c_inst->user = strdup(value); + }else if(strcmp(name,"passwd") == 0){ + c_inst->passwd = strdup(value); + }else if(strcmp(name,"queue") == 0){ + c_inst->queue = strdup(value); + }else if(strcmp(name,"dbserver") == 0){ + c_inst->dbserver = strdup(value); + }else if(strcmp(name,"dbport") == 0){ + c_inst->dbport = atoi(value); + }else if(strcmp(name,"dbname") == 0){ + c_inst->dbname = strdup(value); + }else if(strcmp(name,"dbuser") == 0){ + c_inst->dbuser = strdup(value); + }else if(strcmp(name,"dbpasswd") == 0){ + c_inst->dbpasswd = strdup(value); + }else if(strcmp(name,"logfile") == 0){ + out_fd = fopen(value,"ab"); + } + + } + + return 1; +} + +int isPair(amqp_message_t* a, amqp_message_t* b) +{ + int keylen = a->properties.correlation_id.len >= + b->properties.correlation_id.len ? + a->properties.correlation_id.len : + b->properties.correlation_id.len; + + return strncmp(a->properties.correlation_id.bytes, + b->properties.correlation_id.bytes, + keylen) == 0 ? 1 : 0; +} + +int connectToServer(MYSQL* server) +{ + + + mysql_init(server); + + mysql_options(server,MYSQL_READ_DEFAULT_GROUP,"client"); + mysql_options(server,MYSQL_OPT_USE_REMOTE_CONNECTION,0); + my_bool tr = 1; + mysql_options(server,MYSQL_OPT_RECONNECT,&tr); + + + MYSQL* result = mysql_real_connect(server, + c_inst->dbserver, + c_inst->dbuser, + c_inst->dbpasswd, + NULL, + c_inst->dbport, + NULL, + 0); + + + if(result==NULL){ + fprintf(out_fd,"Error: Could not connect to MySQL server: %s\n",mysql_error(server)); + return 0; + } + + int bsz = 1024; + char *qstr = calloc(bsz,sizeof(char)); + + + if(!qstr){ + fprintf(stderr, "Fatal Error: Cannot allocate enough memory.\n"); + return 0; + } + + + /**Connection ok, check that the database and table exist*/ + + memset(qstr,0,bsz); + sprintf(qstr,DB_DATABASE,c_inst->dbname); + if(mysql_query(server,qstr)){ + fprintf(stderr,"Error: Could not send query MySQL server: %s\n",mysql_error(server)); + } + memset(qstr,0,bsz); + sprintf(qstr,"USE %s;",c_inst->dbname); + if(mysql_query(server,qstr)){ + fprintf(stderr,"Error: Could not send query MySQL server: %s\n",mysql_error(server)); + } + + memset(qstr,0,bsz); + sprintf(qstr,DB_TABLE); + if(mysql_query(server,qstr)){ + fprintf(stderr,"Error: Could not send query MySQL server: %s\n",mysql_error(server)); + } + + free(qstr); + return 1; +} + +int sendMessage(MYSQL* server, amqp_message_t* msg) +{ + int buffsz = (int)((msg->body.len + 1)*2+1) + + (int)((msg->properties.correlation_id.len + 1)*2+1) + + strlen(DB_INSERT), + rval = 0; + char *qstr = calloc(buffsz,sizeof(char)), + *rawmsg = calloc((msg->body.len + 1),sizeof(char)), + *clnmsg = calloc(((msg->body.len + 1)*2+1),sizeof(char)), + *rawdate = calloc((msg->body.len + 1),sizeof(char)), + *clndate = calloc(((msg->body.len + 1)*2+1),sizeof(char)), + *rawtag = calloc((msg->properties.correlation_id.len + 1),sizeof(char)), + *clntag = calloc(((msg->properties.correlation_id.len + 1)*2+1),sizeof(char)); + + + + sprintf(qstr,"%.*s",(int)msg->body.len,(char *)msg->body.bytes); + fprintf(out_fd,"Received: %s\n",qstr); + char *ptr = strtok(qstr,"|"); + sprintf(rawdate,"%s",ptr); + ptr = strtok(NULL,"\n\0"); + if(ptr == NULL){ + fprintf(out_fd,"Message content not valid.\n"); + rval = 1; + goto cleanup; + } + sprintf(rawmsg,"%s",ptr); + sprintf(rawtag,"%.*s",(int)msg->properties.correlation_id.len,(char *)msg->properties.correlation_id.bytes); + memset(qstr,0,buffsz); + + mysql_real_escape_string(server,clnmsg,rawmsg,strnlen(rawmsg,msg->body.len + 1)); + mysql_real_escape_string(server,clndate,rawdate,strnlen(rawdate,msg->body.len + 1)); + mysql_real_escape_string(server,clntag,rawtag,strnlen(rawtag,msg->properties.correlation_id.len + 1)); + + if(strncmp(msg->properties.message_id.bytes, + "query",msg->properties.message_id.len) == 0) + { + + sprintf(qstr,DB_INCREMENT,clndate,clnmsg); + rval = mysql_query(server,qstr); + + if(mysql_affected_rows(server) == 0){ + memset(qstr,0,buffsz); + sprintf(qstr,DB_INSERT,clntag,clnmsg,clndate); + rval = mysql_query(server,qstr); + } + + }else if(strncmp(msg->properties.message_id.bytes, + "reply",msg->properties.message_id.len) == 0){ + + sprintf(qstr,DB_UPDATE,clnmsg,clndate,clntag); + rval = mysql_query(server,qstr); + + }else{ + rval = 1; + goto cleanup; + } + + + if(rval){ + fprintf(stderr,"Could not send query to SQL server:%s\n",mysql_error(server)); + goto cleanup; + } + + cleanup: + free(qstr); + free(rawmsg); + free(clnmsg); + free(rawdate); + free(clndate); + free(rawtag); + free(clntag); + + return rval; +} + +int sendToServer(MYSQL* server, amqp_message_t* a, amqp_message_t* b){ + + amqp_message_t *msg, *reply; + int buffsz = 2048; + char *qstr = calloc(buffsz,sizeof(char)); + + if(!qstr){ + fprintf(out_fd, "Fatal Error: Cannot allocate enough memory.\n"); + free(qstr); + return 0; + } + + if( a->properties.message_id.len == strlen("query") && + strncmp(a->properties.message_id.bytes,"query", + a->properties.message_id.len) == 0){ + + msg = a; + reply = b; + + }else{ + + msg = b; + reply = a; + + } + + + printf("pair: %.*s\nquery: %.*s\nreply: %.*s\n", + (int)msg->properties.correlation_id.len, + (char *)msg->properties.correlation_id.bytes, + (int)msg->body.len, + (char *)msg->body.bytes, + (int)reply->body.len, + (char *)reply->body.bytes); + + if((int)msg->body.len + + (int)reply->body.len + + (int)msg->properties.correlation_id.len + 50 >= buffsz) + { + char *qtmp = calloc(buffsz*2,sizeof(char)); + free(qstr); + + if(qtmp){ + qstr = qtmp; + buffsz *= 2; + }else{ + fprintf(stderr, "Fatal Error: Cannot allocate enough memory.\n"); + return 0; + } + + } + + char *rawmsg = calloc((msg->body.len + 1),sizeof(char)), + *clnmsg = calloc(((msg->body.len + 1)*2+1),sizeof(char)), + *rawrpl = calloc((reply->body.len + 1),sizeof(char)), + *clnrpl = calloc(((reply->body.len + 1)*2+1),sizeof(char)), + *rawtag = calloc((msg->properties.correlation_id.len + 1),sizeof(char)), + *clntag = calloc(((msg->properties.correlation_id.len + 1)*2+1),sizeof(char)); + + sprintf(rawmsg,"%.*s",(int)msg->body.len,(char *)msg->body.bytes); + sprintf(rawrpl,"%.*s",(int)reply->body.len,(char *)reply->body.bytes); + sprintf(rawtag,"%.*s",(int)msg->properties.correlation_id.len,(char *)msg->properties.correlation_id.bytes); + + char *ptr; + while((ptr = strchr(rawmsg,'\n'))){ + *ptr = ' '; + } + while((ptr = strchr(rawrpl,'\n'))){ + *ptr = ' '; + } + while((ptr = strchr(rawtag,'\n'))){ + *ptr = ' '; + } + + mysql_real_escape_string(server,clnmsg,rawmsg,strnlen(rawmsg,msg->body.len + 1)); + mysql_real_escape_string(server,clnrpl,rawrpl,strnlen(rawrpl,reply->body.len + 1)); + mysql_real_escape_string(server,clntag,rawtag,strnlen(rawtag,msg->properties.correlation_id.len + 1)); + + + + sprintf(qstr,"INSERT INTO pairs VALUES ('%s','%s','%s');",clnmsg,clnrpl,clntag); + free(rawmsg); + free(clnmsg); + free(rawrpl); + free(clnrpl); + free(rawtag); + free(clntag); + + if(mysql_query(server,qstr)){ + fprintf(stderr,"Could not send query to SQL server:%s\n",mysql_error(server)); + free(qstr); + return 0; + } + + free(qstr); + return 1; +} +int main(int argc, char** argv) +{ + int channel = 1, status = AMQP_STATUS_OK, cnfnlen; + amqp_socket_t *socket = NULL; + amqp_connection_state_t conn; + amqp_rpc_reply_t ret; + amqp_message_t *reply = NULL; + amqp_frame_t frame; + struct timeval timeout; + MYSQL db_inst; + char ch, *cnfname = NULL, *cnfpath = NULL; + static const char* fname = "consumer.cnf"; + + if((c_inst = calloc(1,sizeof(CONSUMER))) == NULL){ + fprintf(stderr, "Fatal Error: Cannot allocate enough memory.\n"); + return 1; + } + + if(signal(SIGINT,sighndl) == SIG_IGN){ + signal(SIGINT,SIG_IGN); + } + + while((ch = getopt(argc,argv,"c:"))!= -1){ + switch(ch){ + case 'c': + cnfnlen = strlen(optarg); + cnfpath = strdup(optarg); + break; + default: + + break; + } + } + + cnfname = calloc(cnfnlen + strlen(fname) + 1,sizeof(char)); + + if(cnfpath){ + + /**Config file path as argument*/ + strcpy(cnfname,cnfpath); + if(cnfpath[cnfnlen-1] != '/'){ + strcat(cnfname,"/"); + } + + } + + strcat(cnfname,fname); + + timeout.tv_sec = 1; + timeout.tv_usec = 0; + all_ok = 1; + out_fd = NULL; + + + + /**Parse the INI file*/ + if(ini_parse(cnfname,handler,NULL) < 0){ + + /**Try to parse a config in the same directory*/ + if(ini_parse(fname,handler,NULL) < 0){ + fprintf(stderr, "Fatal Error: Error parsing configuration file!\n"); + goto fatal_error; + + } + } + + if(out_fd == NULL){ + out_fd = stdout; + } + + fprintf(out_fd,"\n--------------------------------------------------------------\n"); + + /**Confirm that all parameters were in the configuration file*/ + if(!c_inst->hostname||!c_inst->vhost||!c_inst->user|| + !c_inst->passwd||!c_inst->dbpasswd||!c_inst->queue|| + !c_inst->dbserver||!c_inst->dbname||!c_inst->dbuser){ + fprintf(stderr, "Fatal Error: Inadequate configuration file!\n"); + goto fatal_error; + } + + connectToServer(&db_inst); + + if((conn = amqp_new_connection()) == NULL || + (socket = amqp_tcp_socket_new(conn)) == NULL){ + fprintf(stderr, "Fatal Error: Cannot create connection object or socket.\n"); + goto fatal_error; + } + + if(amqp_socket_open(socket, c_inst->hostname, c_inst->port)){ + fprintf(stderr, "RabbitMQ Error: Cannot open socket.\n"); + goto error; + } + + ret = amqp_login(conn, c_inst->vhost, 0, 131072, 0, AMQP_SASL_METHOD_PLAIN, c_inst->user, c_inst->passwd); + + if(ret.reply_type != AMQP_RESPONSE_NORMAL){ + fprintf(stderr, "RabbitMQ Error: Cannot login to server.\n"); + goto error; + } + + amqp_channel_open(conn, channel); + ret = amqp_get_rpc_reply(conn); + + if(ret.reply_type != AMQP_RESPONSE_NORMAL){ + fprintf(stderr, "RabbitMQ Error: Cannot open channel.\n"); + goto error; + } + + reply = malloc(sizeof(amqp_message_t)); + if(!reply){ + fprintf(stderr, "Error: Cannot allocate enough memory.\n"); + goto error; + } + amqp_basic_consume(conn,channel,amqp_cstring_bytes(c_inst->queue),amqp_empty_bytes,0,0,0,amqp_empty_table); + + while(all_ok){ + + status = amqp_simple_wait_frame_noblock(conn,&frame,&timeout); + + /**No frames to read from server, possibly out of messages*/ + if(status == AMQP_STATUS_TIMEOUT){ + sleep(timeout.tv_sec); + continue; + } + + if(frame.payload.method.id == AMQP_BASIC_DELIVER_METHOD){ + + amqp_basic_deliver_t* decoded = (amqp_basic_deliver_t*)frame.payload.method.decoded; + + amqp_read_message(conn,channel,reply,0); + + if(sendMessage(&db_inst,reply)){ + + fprintf(stderr,"RabbitMQ Error: Received malformed message.\n"); + amqp_basic_reject(conn,channel,decoded->delivery_tag,0); + amqp_destroy_message(reply); + + }else{ + + amqp_basic_ack(conn,channel,decoded->delivery_tag,0); + amqp_destroy_message(reply); + + } + + }else{ + fprintf(stderr,"RabbitMQ Error: Received method from server: %s\n",amqp_method_name(frame.payload.method.id)); + all_ok = 0; + goto error; + } + + } + + fprintf(out_fd,"Shutting down...\n"); + error: + + mysql_close(&db_inst); + mysql_library_end(); + if(c_inst && c_inst->query_stack){ + + while(c_inst->query_stack){ + DELIVERY* d = c_inst->query_stack->next; + amqp_destroy_message(c_inst->query_stack->message); + free(c_inst->query_stack); + c_inst->query_stack = d; + } + + } + + amqp_channel_close(conn, channel, AMQP_REPLY_SUCCESS); + amqp_connection_close(conn, AMQP_REPLY_SUCCESS); + amqp_destroy_connection(conn); + fatal_error: + + if(out_fd){ + fclose(out_fd); + } + + + if(c_inst){ + + free(c_inst->hostname); + free(c_inst->vhost); + free(c_inst->user); + free(c_inst->passwd); + free(c_inst->queue); + free(c_inst->dbserver); + free(c_inst->dbname); + free(c_inst->dbuser); + free(c_inst->dbpasswd); + free(c_inst); + + } + + + + return all_ok; +} diff --git a/rabbitmq_consumer/consumer.cnf b/rabbitmq_consumer/consumer.cnf new file mode 100644 index 000000000..82296edc6 --- /dev/null +++ b/rabbitmq_consumer/consumer.cnf @@ -0,0 +1,28 @@ +# +#The options for the consumer are: +#hostname RabbitMQ hostname +#port RabbitMQ port +#vhost RabbitMQ virtual host +#user RabbitMQ username +#passwd RabbitMQ password +#queue Name of the queue to use +#dbserver SQL server name +#dbport SQL server port +#dbname Name of the databse to use +#dbuser SQL server username +#dbpasswd SQL server password +#logfile Message log filename +# +[consumer] +hostname=127.0.0.1 +port=5673 +vhost=/ +user=guest +passwd=guest +queue=q1 +dbserver=127.0.0.1 +dbport=3000 +dbname=mqpairs +dbuser=maxuser +dbpasswd=maxpwd +#logfile=consumer.log \ No newline at end of file diff --git a/rabbitmq_consumer/inih/._LICENSE.txt b/rabbitmq_consumer/inih/._LICENSE.txt new file mode 100755 index 000000000..7fa6eb8a6 Binary files /dev/null and b/rabbitmq_consumer/inih/._LICENSE.txt differ diff --git a/rabbitmq_consumer/inih/._README.txt b/rabbitmq_consumer/inih/._README.txt new file mode 100755 index 000000000..7fa6eb8a6 Binary files /dev/null and b/rabbitmq_consumer/inih/._README.txt differ diff --git a/rabbitmq_consumer/inih/._cpp b/rabbitmq_consumer/inih/._cpp new file mode 100755 index 000000000..17b8574a4 Binary files /dev/null and b/rabbitmq_consumer/inih/._cpp differ diff --git a/rabbitmq_consumer/inih/._examples b/rabbitmq_consumer/inih/._examples new file mode 100755 index 000000000..17b8574a4 Binary files /dev/null and b/rabbitmq_consumer/inih/._examples differ diff --git a/rabbitmq_consumer/inih/._extra b/rabbitmq_consumer/inih/._extra new file mode 100755 index 000000000..17b8574a4 Binary files /dev/null and b/rabbitmq_consumer/inih/._extra differ diff --git a/rabbitmq_consumer/inih/._ini.c b/rabbitmq_consumer/inih/._ini.c new file mode 100755 index 000000000..17b8574a4 Binary files /dev/null and b/rabbitmq_consumer/inih/._ini.c differ diff --git a/rabbitmq_consumer/inih/._ini.h b/rabbitmq_consumer/inih/._ini.h new file mode 100755 index 000000000..17b8574a4 Binary files /dev/null and b/rabbitmq_consumer/inih/._ini.h differ diff --git a/rabbitmq_consumer/inih/._tests b/rabbitmq_consumer/inih/._tests new file mode 100755 index 000000000..7fa6eb8a6 Binary files /dev/null and b/rabbitmq_consumer/inih/._tests differ diff --git a/rabbitmq_consumer/inih/.gitignore b/rabbitmq_consumer/inih/.gitignore new file mode 100644 index 000000000..2a5429025 --- /dev/null +++ b/rabbitmq_consumer/inih/.gitignore @@ -0,0 +1,3 @@ +*.o +*.a +make.depend diff --git a/rabbitmq_consumer/inih/CMakeLists.txt b/rabbitmq_consumer/inih/CMakeLists.txt new file mode 100644 index 000000000..492566fd2 --- /dev/null +++ b/rabbitmq_consumer/inih/CMakeLists.txt @@ -0,0 +1 @@ +add_library(inih ini.c) diff --git a/rabbitmq_consumer/inih/LICENSE.txt b/rabbitmq_consumer/inih/LICENSE.txt new file mode 100755 index 000000000..44a3093a3 --- /dev/null +++ b/rabbitmq_consumer/inih/LICENSE.txt @@ -0,0 +1,27 @@ + +The "inih" library is distributed under the New BSD license: + +Copyright (c) 2009, Brush Technology +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Brush Technology nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY BRUSH TECHNOLOGY ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL BRUSH TECHNOLOGY BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/rabbitmq_consumer/inih/README.txt b/rabbitmq_consumer/inih/README.txt new file mode 100755 index 000000000..4bff76126 --- /dev/null +++ b/rabbitmq_consumer/inih/README.txt @@ -0,0 +1,5 @@ + +inih is a simple .INI file parser written in C, released under the New BSD +license (see LICENSE.txt). Go to the project home page for more info: + +http://code.google.com/p/inih/ diff --git a/rabbitmq_consumer/inih/cpp/._INIReader.cpp b/rabbitmq_consumer/inih/cpp/._INIReader.cpp new file mode 100755 index 000000000..17b8574a4 Binary files /dev/null and b/rabbitmq_consumer/inih/cpp/._INIReader.cpp differ diff --git a/rabbitmq_consumer/inih/cpp/._INIReader.h b/rabbitmq_consumer/inih/cpp/._INIReader.h new file mode 100755 index 000000000..17b8574a4 Binary files /dev/null and b/rabbitmq_consumer/inih/cpp/._INIReader.h differ diff --git a/rabbitmq_consumer/inih/cpp/._INIReaderTest.cpp b/rabbitmq_consumer/inih/cpp/._INIReaderTest.cpp new file mode 100755 index 000000000..17b8574a4 Binary files /dev/null and b/rabbitmq_consumer/inih/cpp/._INIReaderTest.cpp differ diff --git a/rabbitmq_consumer/inih/cpp/INIReader.cpp b/rabbitmq_consumer/inih/cpp/INIReader.cpp new file mode 100755 index 000000000..43f695149 --- /dev/null +++ b/rabbitmq_consumer/inih/cpp/INIReader.cpp @@ -0,0 +1,67 @@ +// Read an INI file into easy-to-access name/value pairs. + +#include +#include +#include +#include "../ini.h" +#include "INIReader.h" + +using std::string; + +INIReader::INIReader(string filename) +{ + _error = ini_parse(filename.c_str(), ValueHandler, this); +} + +int INIReader::ParseError() +{ + return _error; +} + +string INIReader::Get(string section, string name, string default_value) +{ + string key = MakeKey(section, name); + return _values.count(key) ? _values[key] : default_value; +} + +long INIReader::GetInteger(string section, string name, long default_value) +{ + string valstr = Get(section, name, ""); + const char* value = valstr.c_str(); + char* end; + // This parses "1234" (decimal) and also "0x4D2" (hex) + long n = strtol(value, &end, 0); + return end > value ? n : default_value; +} + +bool INIReader::GetBoolean(string section, string name, bool default_value) +{ + string valstr = Get(section, name, ""); + // Convert to lower case to make string comparisons case-insensitive + std::transform(valstr.begin(), valstr.end(), valstr.begin(), ::tolower); + if (valstr == "true" || valstr == "yes" || valstr == "on" || valstr == "1") + return true; + else if (valstr == "false" || valstr == "no" || valstr == "off" || valstr == "0") + return false; + else + return default_value; +} + +string INIReader::MakeKey(string section, string name) +{ + string key = section + "." + name; + // Convert to lower case to make section/name lookups case-insensitive + std::transform(key.begin(), key.end(), key.begin(), ::tolower); + return key; +} + +int INIReader::ValueHandler(void* user, const char* section, const char* name, + const char* value) +{ + INIReader* reader = (INIReader*)user; + string key = MakeKey(section, name); + if (reader->_values[key].size() > 0) + reader->_values[key] += "\n"; + reader->_values[key] += value; + return 1; +} diff --git a/rabbitmq_consumer/inih/cpp/INIReader.h b/rabbitmq_consumer/inih/cpp/INIReader.h new file mode 100755 index 000000000..7571a29d2 --- /dev/null +++ b/rabbitmq_consumer/inih/cpp/INIReader.h @@ -0,0 +1,48 @@ +// Read an INI file into easy-to-access name/value pairs. + +// inih and INIReader are released under the New BSD license (see LICENSE.txt). +// Go to the project home page for more info: +// +// http://code.google.com/p/inih/ + +#ifndef __INIREADER_H__ +#define __INIREADER_H__ + +#include +#include + +// Read an INI file into easy-to-access name/value pairs. (Note that I've gone +// for simplicity here rather than speed, but it should be pretty decent.) +class INIReader +{ +public: + // Construct INIReader and parse given filename. See ini.h for more info + // about the parsing. + INIReader(std::string filename); + + // Return the result of ini_parse(), i.e., 0 on success, line number of + // first error on parse error, or -1 on file open error. + int ParseError(); + + // Get a string value from INI file, returning default_value if not found. + std::string Get(std::string section, std::string name, + std::string default_value); + + // Get an integer (long) value from INI file, returning default_value if + // not found or not a valid integer (decimal "1234", "-1234", or hex "0x4d2"). + long GetInteger(std::string section, std::string name, long default_value); + + // Get a boolean value from INI file, returning default_value if not found or if + // not a valid true/false value. Valid true values are "true", "yes", "on", "1", + // and valid false values are "false", "no", "off", "0" (not case sensitive). + bool GetBoolean(std::string section, std::string name, bool default_value); + +private: + int _error; + std::map _values; + static std::string MakeKey(std::string section, std::string name); + static int ValueHandler(void* user, const char* section, const char* name, + const char* value); +}; + +#endif // __INIREADER_H__ diff --git a/rabbitmq_consumer/inih/cpp/INIReaderTest.cpp b/rabbitmq_consumer/inih/cpp/INIReaderTest.cpp new file mode 100755 index 000000000..cb13b62c1 --- /dev/null +++ b/rabbitmq_consumer/inih/cpp/INIReaderTest.cpp @@ -0,0 +1,20 @@ +// Example that shows simple usage of the INIReader class + +#include +#include "INIReader.h" + +int main() +{ + INIReader reader("../examples/test.ini"); + + if (reader.ParseError() < 0) { + std::cout << "Can't load 'test.ini'\n"; + return 1; + } + std::cout << "Config loaded from 'test.ini': version=" + << reader.GetInteger("protocol", "version", -1) << ", name=" + << reader.Get("user", "name", "UNKNOWN") << ", email=" + << reader.Get("user", "email", "UNKNOWN") << ", active=" + << reader.GetBoolean("user", "active", true) << "\n"; + return 0; +} diff --git a/rabbitmq_consumer/inih/examples/._config.def b/rabbitmq_consumer/inih/examples/._config.def new file mode 100755 index 000000000..17b8574a4 Binary files /dev/null and b/rabbitmq_consumer/inih/examples/._config.def differ diff --git a/rabbitmq_consumer/inih/examples/._ini_dump.c b/rabbitmq_consumer/inih/examples/._ini_dump.c new file mode 100755 index 000000000..17b8574a4 Binary files /dev/null and b/rabbitmq_consumer/inih/examples/._ini_dump.c differ diff --git a/rabbitmq_consumer/inih/examples/._ini_example.c b/rabbitmq_consumer/inih/examples/._ini_example.c new file mode 100755 index 000000000..17b8574a4 Binary files /dev/null and b/rabbitmq_consumer/inih/examples/._ini_example.c differ diff --git a/rabbitmq_consumer/inih/examples/._ini_xmacros.c b/rabbitmq_consumer/inih/examples/._ini_xmacros.c new file mode 100755 index 000000000..17b8574a4 Binary files /dev/null and b/rabbitmq_consumer/inih/examples/._ini_xmacros.c differ diff --git a/rabbitmq_consumer/inih/examples/._test.ini b/rabbitmq_consumer/inih/examples/._test.ini new file mode 100755 index 000000000..17b8574a4 Binary files /dev/null and b/rabbitmq_consumer/inih/examples/._test.ini differ diff --git a/rabbitmq_consumer/inih/examples/config.def b/rabbitmq_consumer/inih/examples/config.def new file mode 100755 index 000000000..f5c7ed5fe --- /dev/null +++ b/rabbitmq_consumer/inih/examples/config.def @@ -0,0 +1,8 @@ +// CFG(section, name, default) + +CFG(protocol, version, "0") + +CFG(user, name, "Fatty Lumpkin") +CFG(user, email, "fatty@lumpkin.com") + +#undef CFG diff --git a/rabbitmq_consumer/inih/examples/ini_dump.c b/rabbitmq_consumer/inih/examples/ini_dump.c new file mode 100755 index 000000000..5c8c6d115 --- /dev/null +++ b/rabbitmq_consumer/inih/examples/ini_dump.c @@ -0,0 +1,40 @@ +/* ini.h example that simply dumps an INI file without comments */ + +#include +#include +#include "../ini.h" + +static int dumper(void* user, const char* section, const char* name, + const char* value) +{ + static char prev_section[50] = ""; + + if (strcmp(section, prev_section)) { + printf("%s[%s]\n", (prev_section[0] ? "\n" : ""), section); + strncpy(prev_section, section, sizeof(prev_section)); + prev_section[sizeof(prev_section) - 1] = '\0'; + } + printf("%s = %s\n", name, value); + return 1; +} + +int main(int argc, char* argv[]) +{ + int error; + + if (argc <= 1) { + printf("Usage: ini_dump filename.ini\n"); + return 1; + } + + error = ini_parse(argv[1], dumper, NULL); + if (error < 0) { + printf("Can't read '%s'!\n", argv[1]); + return 2; + } + else if (error) { + printf("Bad config file (first error on line %d)!\n", error); + return 3; + } + return 0; +} diff --git a/rabbitmq_consumer/inih/examples/ini_example.c b/rabbitmq_consumer/inih/examples/ini_example.c new file mode 100755 index 000000000..962cef57a --- /dev/null +++ b/rabbitmq_consumer/inih/examples/ini_example.c @@ -0,0 +1,44 @@ +/* Example: parse a simple configuration file */ + +#include +#include +#include +#include "../ini.h" + +typedef struct +{ + int version; + const char* name; + const char* email; +} configuration; + +static int handler(void* user, const char* section, const char* name, + const char* value) +{ + configuration* pconfig = (configuration*)user; + + #define MATCH(s, n) strcmp(section, s) == 0 && strcmp(name, n) == 0 + if (MATCH("protocol", "version")) { + pconfig->version = atoi(value); + } else if (MATCH("user", "name")) { + pconfig->name = strdup(value); + } else if (MATCH("user", "email")) { + pconfig->email = strdup(value); + } else { + return 0; /* unknown section/name, error */ + } + return 1; +} + +int main(int argc, char* argv[]) +{ + configuration config; + + if (ini_parse("test.ini", handler, &config) < 0) { + printf("Can't load 'test.ini'\n"); + return 1; + } + printf("Config loaded from 'test.ini': version=%d, name=%s, email=%s\n", + config.version, config.name, config.email); + return 0; +} diff --git a/rabbitmq_consumer/inih/examples/ini_xmacros.c b/rabbitmq_consumer/inih/examples/ini_xmacros.c new file mode 100755 index 000000000..7d867acd6 --- /dev/null +++ b/rabbitmq_consumer/inih/examples/ini_xmacros.c @@ -0,0 +1,46 @@ +/* Parse a configuration file into a struct using X-Macros */ + +#include +#include +#include "../ini.h" + +/* define the config struct type */ +typedef struct { + #define CFG(s, n, default) char *s##_##n; + #include "config.def" +} config; + +/* create one and fill in its default values */ +config Config = { + #define CFG(s, n, default) default, + #include "config.def" +}; + +/* process a line of the INI file, storing valid values into config struct */ +int handler(void *user, const char *section, const char *name, + const char *value) +{ + config *cfg = (config *)user; + + if (0) ; + #define CFG(s, n, default) else if (strcmp(section, #s)==0 && \ + strcmp(name, #n)==0) cfg->s##_##n = strdup(value); + #include "config.def" + + return 1; +} + +/* print all the variables in the config, one per line */ +void dump_config(config *cfg) +{ + #define CFG(s, n, default) printf("%s_%s = %s\n", #s, #n, cfg->s##_##n); + #include "config.def" +} + +int main(int argc, char* argv[]) +{ + if (ini_parse("test.ini", handler, &Config) < 0) + printf("Can't load 'test.ini', using defaults\n"); + dump_config(&Config); + return 0; +} diff --git a/rabbitmq_consumer/inih/examples/test.ini b/rabbitmq_consumer/inih/examples/test.ini new file mode 100755 index 000000000..e06e7f9ba --- /dev/null +++ b/rabbitmq_consumer/inih/examples/test.ini @@ -0,0 +1,9 @@ +; Test config file for ini_example.c and INIReaderTest.cpp + +[protocol] ; Protocol configuration +version=6 ; IPv6 + +[user] +name = Bob Smith ; Spaces around '=' are stripped +email = bob@smith.com ; And comments (like this) ignored +active = true ; Test a boolean diff --git a/rabbitmq_consumer/inih/extra/._Makefile.static b/rabbitmq_consumer/inih/extra/._Makefile.static new file mode 100755 index 000000000..17b8574a4 Binary files /dev/null and b/rabbitmq_consumer/inih/extra/._Makefile.static differ diff --git a/rabbitmq_consumer/inih/extra/Makefile.static b/rabbitmq_consumer/inih/extra/Makefile.static new file mode 100755 index 000000000..0d6519e38 --- /dev/null +++ b/rabbitmq_consumer/inih/extra/Makefile.static @@ -0,0 +1,19 @@ +# Simple makefile to build inih as a static library using g++ + +SRC = ../ini.c +OBJ = $(SRC:.c=.o) +OUT = libinih.a +INCLUDES = -I.. +CCFLAGS = -g -O2 +CC = g++ + +default: $(OUT) + +.c.o: + $(CC) $(INCLUDES) $(CCFLAGS) $(EXTRACCFLAGS) -c $< -o $@ + +$(OUT): $(OBJ) + ar rcs $(OUT) $(OBJ) $(EXTRAARFLAGS) + +clean: + rm -f $(OBJ) $(OUT) diff --git a/rabbitmq_consumer/inih/ini.c b/rabbitmq_consumer/inih/ini.c new file mode 100755 index 000000000..9f9110eaf --- /dev/null +++ b/rabbitmq_consumer/inih/ini.c @@ -0,0 +1,176 @@ +/* inih -- simple .INI file parser + +inih is released under the New BSD license (see LICENSE.txt). Go to the project +home page for more info: + +http://code.google.com/p/inih/ + +*/ + +#include +#include +#include + +#include "ini.h" + +#if !INI_USE_STACK +#include +#endif + +#define MAX_SECTION 50 +#define MAX_NAME 50 + +/* Strip whitespace chars off end of given string, in place. Return s. */ +static char* rstrip(char* s) +{ + char* p = s + strlen(s); + while (p > s && isspace((unsigned char)(*--p))) + *p = '\0'; + return s; +} + +/* Return pointer to first non-whitespace char in given string. */ +static char* lskip(const char* s) +{ + while (*s && isspace((unsigned char)(*s))) + s++; + return (char*)s; +} + +/* Return pointer to first char c or ';' comment in given string, or pointer to + null at end of string if neither found. ';' must be prefixed by a whitespace + character to register as a comment. */ +static char* find_char_or_comment(const char* s, char c) +{ + int was_whitespace = 0; + while (*s && *s != c && !(was_whitespace && *s == ';')) { + was_whitespace = isspace((unsigned char)(*s)); + s++; + } + return (char*)s; +} + +/* Version of strncpy that ensures dest (size bytes) is null-terminated. */ +static char* strncpy0(char* dest, const char* src, size_t size) +{ + strncpy(dest, src, size); + dest[size - 1] = '\0'; + return dest; +} + +/* See documentation in header file. */ +int ini_parse_file(FILE* file, + int (*handler)(void*, const char*, const char*, + const char*), + void* user) +{ + /* Uses a fair bit of stack (use heap instead if you need to) */ +#if INI_USE_STACK + char line[INI_MAX_LINE]; +#else + char* line; +#endif + char section[MAX_SECTION] = ""; + char prev_name[MAX_NAME] = ""; + + char* start; + char* end; + char* name; + char* value; + int lineno = 0; + int error = 0; + +#if !INI_USE_STACK + line = (char*)malloc(INI_MAX_LINE); + if (!line) { + return -2; + } +#endif + + /* Scan through file line by line */ + while (fgets(line, INI_MAX_LINE, file) != NULL) { + lineno++; + + start = line; +#if INI_ALLOW_BOM + if (lineno == 1 && (unsigned char)start[0] == 0xEF && + (unsigned char)start[1] == 0xBB && + (unsigned char)start[2] == 0xBF) { + start += 3; + } +#endif + start = lskip(rstrip(start)); + + if (*start == ';' || *start == '#') { + /* Per Python ConfigParser, allow '#' comments at start of line */ + } +#if INI_ALLOW_MULTILINE + else if (*prev_name && *start && start > line) { + /* Non-black line with leading whitespace, treat as continuation + of previous name's value (as per Python ConfigParser). */ + if (!handler(user, section, prev_name, start) && !error) + error = lineno; + } +#endif + else if (*start == '[') { + /* A "[section]" line */ + end = find_char_or_comment(start + 1, ']'); + if (*end == ']') { + *end = '\0'; + strncpy0(section, start + 1, sizeof(section)); + *prev_name = '\0'; + } + else if (!error) { + /* No ']' found on section line */ + error = lineno; + } + } + else if (*start && *start != ';') { + /* Not a comment, must be a name[=:]value pair */ + end = find_char_or_comment(start, '='); + if (*end != '=') { + end = find_char_or_comment(start, ':'); + } + if (*end == '=' || *end == ':') { + *end = '\0'; + name = rstrip(start); + value = lskip(end + 1); + end = find_char_or_comment(value, '\0'); + if (*end == ';') + *end = '\0'; + rstrip(value); + + /* Valid name[=:]value pair found, call handler */ + strncpy0(prev_name, name, sizeof(prev_name)); + if (!handler(user, section, name, value) && !error) + error = lineno; + } + else if (!error) { + /* No '=' or ':' found on name[=:]value line */ + error = lineno; + } + } + } + +#if !INI_USE_STACK + free(line); +#endif + + return error; +} + +/* See documentation in header file. */ +int ini_parse(const char* filename, + int (*handler)(void*, const char*, const char*, const char*), + void* user) +{ + FILE* file; + int error; + + file = fopen(filename, "r"); + if (!file) + return -1; + error = ini_parse_file(file, handler, user); + fclose(file); + return error; +} diff --git a/rabbitmq_consumer/inih/ini.h b/rabbitmq_consumer/inih/ini.h new file mode 100755 index 000000000..b3a494a24 --- /dev/null +++ b/rabbitmq_consumer/inih/ini.h @@ -0,0 +1,72 @@ +/* inih -- simple .INI file parser + +inih is released under the New BSD license (see LICENSE.txt). Go to the project +home page for more info: + +http://code.google.com/p/inih/ + +*/ + +#ifndef __INI_H__ +#define __INI_H__ + +/* Make this header file easier to include in C++ code */ +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/* Parse given INI-style file. May have [section]s, name=value pairs + (whitespace stripped), and comments starting with ';' (semicolon). Section + is "" if name=value pair parsed before any section heading. name:value + pairs are also supported as a concession to Python's ConfigParser. + + For each name=value pair parsed, call handler function with given user + pointer as well as section, name, and value (data only valid for duration + of handler call). Handler should return nonzero on success, zero on error. + + Returns 0 on success, line number of first error on parse error (doesn't + stop on first error), -1 on file open error, or -2 on memory allocation + error (only when INI_USE_STACK is zero). +*/ +int ini_parse(const char* filename, + int (*handler)(void* user, const char* section, + const char* name, const char* value), + void* user); + +/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't + close the file when it's finished -- the caller must do that. */ +int ini_parse_file(FILE* file, + int (*handler)(void* user, const char* section, + const char* name, const char* value), + void* user); + +/* Nonzero to allow multi-line value parsing, in the style of Python's + ConfigParser. If allowed, ini_parse() will call the handler with the same + name for each subsequent line parsed. */ +#ifndef INI_ALLOW_MULTILINE +#define INI_ALLOW_MULTILINE 1 +#endif + +/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of + the file. See http://code.google.com/p/inih/issues/detail?id=21 */ +#ifndef INI_ALLOW_BOM +#define INI_ALLOW_BOM 1 +#endif + +/* Nonzero to use stack, zero to use heap (malloc/free). */ +#ifndef INI_USE_STACK +#define INI_USE_STACK 1 +#endif + +/* Maximum line length for any line in INI file. */ +#ifndef INI_MAX_LINE +#define INI_MAX_LINE 200 +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* __INI_H__ */ diff --git a/rabbitmq_consumer/inih/tests/._bad_comment.ini b/rabbitmq_consumer/inih/tests/._bad_comment.ini new file mode 100755 index 000000000..7fa6eb8a6 Binary files /dev/null and b/rabbitmq_consumer/inih/tests/._bad_comment.ini differ diff --git a/rabbitmq_consumer/inih/tests/._bad_multi.ini b/rabbitmq_consumer/inih/tests/._bad_multi.ini new file mode 100755 index 000000000..7fa6eb8a6 Binary files /dev/null and b/rabbitmq_consumer/inih/tests/._bad_multi.ini differ diff --git a/rabbitmq_consumer/inih/tests/._bad_section.ini b/rabbitmq_consumer/inih/tests/._bad_section.ini new file mode 100755 index 000000000..7fa6eb8a6 Binary files /dev/null and b/rabbitmq_consumer/inih/tests/._bad_section.ini differ diff --git a/rabbitmq_consumer/inih/tests/._baseline_multi.txt b/rabbitmq_consumer/inih/tests/._baseline_multi.txt new file mode 100755 index 000000000..7fa6eb8a6 Binary files /dev/null and b/rabbitmq_consumer/inih/tests/._baseline_multi.txt differ diff --git a/rabbitmq_consumer/inih/tests/._baseline_single.txt b/rabbitmq_consumer/inih/tests/._baseline_single.txt new file mode 100755 index 000000000..7fa6eb8a6 Binary files /dev/null and b/rabbitmq_consumer/inih/tests/._baseline_single.txt differ diff --git a/rabbitmq_consumer/inih/tests/._bom.ini b/rabbitmq_consumer/inih/tests/._bom.ini new file mode 100755 index 000000000..7fa6eb8a6 Binary files /dev/null and b/rabbitmq_consumer/inih/tests/._bom.ini differ diff --git a/rabbitmq_consumer/inih/tests/._multi_line.ini b/rabbitmq_consumer/inih/tests/._multi_line.ini new file mode 100755 index 000000000..7fa6eb8a6 Binary files /dev/null and b/rabbitmq_consumer/inih/tests/._multi_line.ini differ diff --git a/rabbitmq_consumer/inih/tests/._normal.ini b/rabbitmq_consumer/inih/tests/._normal.ini new file mode 100755 index 000000000..7fa6eb8a6 Binary files /dev/null and b/rabbitmq_consumer/inih/tests/._normal.ini differ diff --git a/rabbitmq_consumer/inih/tests/._unittest.bat b/rabbitmq_consumer/inih/tests/._unittest.bat new file mode 100755 index 000000000..7fa6eb8a6 Binary files /dev/null and b/rabbitmq_consumer/inih/tests/._unittest.bat differ diff --git a/rabbitmq_consumer/inih/tests/._unittest.c b/rabbitmq_consumer/inih/tests/._unittest.c new file mode 100755 index 000000000..7fa6eb8a6 Binary files /dev/null and b/rabbitmq_consumer/inih/tests/._unittest.c differ diff --git a/rabbitmq_consumer/inih/tests/._user_error.ini b/rabbitmq_consumer/inih/tests/._user_error.ini new file mode 100755 index 000000000..7fa6eb8a6 Binary files /dev/null and b/rabbitmq_consumer/inih/tests/._user_error.ini differ diff --git a/rabbitmq_consumer/inih/tests/bad_comment.ini b/rabbitmq_consumer/inih/tests/bad_comment.ini new file mode 100755 index 000000000..7f4602eb9 --- /dev/null +++ b/rabbitmq_consumer/inih/tests/bad_comment.ini @@ -0,0 +1 @@ +This is an error diff --git a/rabbitmq_consumer/inih/tests/bad_multi.ini b/rabbitmq_consumer/inih/tests/bad_multi.ini new file mode 100755 index 000000000..655017500 --- /dev/null +++ b/rabbitmq_consumer/inih/tests/bad_multi.ini @@ -0,0 +1 @@ + indented diff --git a/rabbitmq_consumer/inih/tests/bad_section.ini b/rabbitmq_consumer/inih/tests/bad_section.ini new file mode 100755 index 000000000..90e31ac09 --- /dev/null +++ b/rabbitmq_consumer/inih/tests/bad_section.ini @@ -0,0 +1,5 @@ +[section1] +name1=value1 +[section2 +[section3 ; comment ] +name2=value2 diff --git a/rabbitmq_consumer/inih/tests/baseline_multi.txt b/rabbitmq_consumer/inih/tests/baseline_multi.txt new file mode 100755 index 000000000..637f75258 --- /dev/null +++ b/rabbitmq_consumer/inih/tests/baseline_multi.txt @@ -0,0 +1,47 @@ +no_file.ini: e=-1 user=0 +... [section1] +... one=This is a test; +... two=1234; +... [ section 2 ] +... happy=4; +... sad=; +... [comment_test] +... test1=1;2;3; +... test2=2;3;4;this won't be a comment, needs whitespace before ';'; +... test;3=345; +... test4=4#5#6; +... [colon_tests] +... Content-Type=text/html; +... foo=bar; +... adams=42; +normal.ini: e=0 user=101 +... [section1] +... name1=value1; +... name2=value2; +bad_section.ini: e=3 user=102 +bad_comment.ini: e=1 user=102 +... [section] +... a=b; +... user=parse_error; +... c=d; +user_error.ini: e=3 user=104 +... [section1] +... single1=abc; +... multi=this is a; +... multi=multi-line value; +... single2=xyz; +... [section2] +... multi=a; +... multi=b; +... multi=c; +... [section3] +... single=ghi; +... multi=the quick; +... multi=brown fox; +... name=bob smith; +multi_line.ini: e=0 user=105 +bad_multi.ini: e=1 user=105 +... [bom_section] +... bom_name=bom_value; +... key“=value“; +bom.ini: e=0 user=107 diff --git a/rabbitmq_consumer/inih/tests/baseline_single.txt b/rabbitmq_consumer/inih/tests/baseline_single.txt new file mode 100755 index 000000000..30d8a2600 --- /dev/null +++ b/rabbitmq_consumer/inih/tests/baseline_single.txt @@ -0,0 +1,43 @@ +no_file.ini: e=-1 user=0 +... [section1] +... one=This is a test; +... two=1234; +... [ section 2 ] +... happy=4; +... sad=; +... [comment_test] +... test1=1;2;3; +... test2=2;3;4;this won't be a comment, needs whitespace before ';'; +... test;3=345; +... test4=4#5#6; +... [colon_tests] +... Content-Type=text/html; +... foo=bar; +... adams=42; +normal.ini: e=0 user=101 +... [section1] +... name1=value1; +... name2=value2; +bad_section.ini: e=3 user=102 +bad_comment.ini: e=1 user=102 +... [section] +... a=b; +... user=parse_error; +... c=d; +user_error.ini: e=3 user=104 +... [section1] +... single1=abc; +... multi=this is a; +... single2=xyz; +... [section2] +... multi=a; +... [section3] +... single=ghi; +... multi=the quick; +... name=bob smith; +multi_line.ini: e=4 user=105 +bad_multi.ini: e=1 user=105 +... [bom_section] +... bom_name=bom_value; +... key“=value“; +bom.ini: e=0 user=107 diff --git a/rabbitmq_consumer/inih/tests/bom.ini b/rabbitmq_consumer/inih/tests/bom.ini new file mode 100755 index 000000000..44c519f47 --- /dev/null +++ b/rabbitmq_consumer/inih/tests/bom.ini @@ -0,0 +1,3 @@ +[bom_section] +bom_name=bom_value +key“ = value“ diff --git a/rabbitmq_consumer/inih/tests/multi_line.ini b/rabbitmq_consumer/inih/tests/multi_line.ini new file mode 100755 index 000000000..d6eb10445 --- /dev/null +++ b/rabbitmq_consumer/inih/tests/multi_line.ini @@ -0,0 +1,15 @@ +[section1] +single1 = abc +multi = this is a + multi-line value +single2 = xyz +[section2] +multi = a + b + c +[section3] +single: ghi +multi: the quick + brown fox +name = bob smith ; comment line 1 + ; comment line 2 diff --git a/rabbitmq_consumer/inih/tests/normal.ini b/rabbitmq_consumer/inih/tests/normal.ini new file mode 100755 index 000000000..787ff8174 --- /dev/null +++ b/rabbitmq_consumer/inih/tests/normal.ini @@ -0,0 +1,25 @@ +; This is an INI file +[section1] ; section comment +one=This is a test ; name=value comment +two = 1234 +; x=y + +[ section 2 ] +happy = 4 +sad = + +[empty] +; do nothing + +[comment_test] +test1 = 1;2;3 ; only this will be a comment +test2 = 2;3;4;this won't be a comment, needs whitespace before ';' +test;3 = 345 ; key should be "test;3" +test4 = 4#5#6 ; '#' only starts a comment at start of line +#test5 = 567 ; entire line commented + # test6 = 678 ; entire line commented, except in MULTILINE mode + +[colon_tests] +Content-Type: text/html +foo:bar +adams : 42 diff --git a/rabbitmq_consumer/inih/tests/unittest.bat b/rabbitmq_consumer/inih/tests/unittest.bat new file mode 100755 index 000000000..90969fe3f --- /dev/null +++ b/rabbitmq_consumer/inih/tests/unittest.bat @@ -0,0 +1,2 @@ +@call tcc ..\ini.c -I..\ -run unittest.c > baseline_multi.txt +@call tcc ..\ini.c -I..\ -DINI_ALLOW_MULTILINE=0 -run unittest.c > baseline_single.txt diff --git a/rabbitmq_consumer/inih/tests/unittest.c b/rabbitmq_consumer/inih/tests/unittest.c new file mode 100755 index 000000000..5e8f8904c --- /dev/null +++ b/rabbitmq_consumer/inih/tests/unittest.c @@ -0,0 +1,58 @@ +/* inih -- unit tests + +This works simply by dumping a bunch of info to standard output, which is +redirected to an output file (baseline_*.txt) and checked into the Subversion +repository. This baseline file is the test output, so the idea is to check it +once, and if it changes -- look at the diff and see which tests failed. + +Here's how I produced the two baseline files (with Tiny C Compiler): + +tcc -DINI_ALLOW_MULTILINE=1 ../ini.c -run unittest.c > baseline_multi.txt +tcc -DINI_ALLOW_MULTILINE=0 ../ini.c -run unittest.c > baseline_single.txt + +*/ + +#include +#include +#include +#include "../ini.h" + +int User; +char Prev_section[50]; + +int dumper(void* user, const char* section, const char* name, + const char* value) +{ + User = (int)user; + if (strcmp(section, Prev_section)) { + printf("... [%s]\n", section); + strncpy(Prev_section, section, sizeof(Prev_section)); + Prev_section[sizeof(Prev_section) - 1] = '\0'; + } + printf("... %s=%s;\n", name, value); + + return strcmp(name, "user")==0 && strcmp(value, "parse_error")==0 ? 0 : 1; +} + +void parse(const char* fname) { + static int u = 100; + int e; + + *Prev_section = '\0'; + e = ini_parse(fname, dumper, (void*)u); + printf("%s: e=%d user=%d\n", fname, e, User); + u++; +} + +int main(void) +{ + parse("no_file.ini"); + parse("normal.ini"); + parse("bad_section.ini"); + parse("bad_comment.ini"); + parse("user_error.ini"); + parse("multi_line.ini"); + parse("bad_multi.ini"); + parse("bom.ini"); + return 0; +} diff --git a/rabbitmq_consumer/inih/tests/user_error.ini b/rabbitmq_consumer/inih/tests/user_error.ini new file mode 100755 index 000000000..9798af35e --- /dev/null +++ b/rabbitmq_consumer/inih/tests/user_error.ini @@ -0,0 +1,4 @@ +[section] +a = b +user = parse_error +c = d diff --git a/rabbitmq_consumer/rabbitmq-message-consumer.spec b/rabbitmq_consumer/rabbitmq-message-consumer.spec new file mode 100644 index 000000000..bc43a7715 --- /dev/null +++ b/rabbitmq_consumer/rabbitmq-message-consumer.spec @@ -0,0 +1,55 @@ +%define _topdir %(echo $PWD)/ +%define name rabbitmq-message-consumer +%define release beta +%define version 1.0 +%define install_path /usr/local/skysql/maxscale/extra/consumer/ + +BuildRoot: %{buildroot} +Summary: rabbitmq-message-consumer +License: GPL +Name: %{name} +Version: %{version} +Release: %{release} +Source: %{name}-%{version}-%{release}.tar.gz +Prefix: / +Group: Development/Tools +Requires: maxscale + +%if 0%{?suse_version} +BuildRequires: gcc gcc-c++ ncurses-devel bison glibc-devel cmake libgcc_s1 perl make libtool libopenssl-devel libaio libaio-devel mariadb libedit-devel librabbitmq-devel MariaDB-shared +%else +BuildRequires: gcc gcc-c++ ncurses-devel bison glibc-devel cmake libgcc perl make libtool openssl-devel libaio libaio-devel librabbitmq-devel MariaDB-shared +%if 0%{?rhel} == 6 +BuildRequires: libedit-devel +%endif +%if 0%{?rhel} == 7 +BuildRequires: mariadb-devel mariadb-embedded-devel libedit-devel +%else +BuildRequires: MariaDB-devel MariaDB-server +%endif +%endif + +%description +rabbitmq-message-consumer + +%prep + +%setup -q + +%build +make clean +make + +%install +mkdir -p $RPM_BUILD_ROOT%{install_path} +cp consumer $RPM_BUILD_ROOT%{install_path} +cp consumer.cnf $RPM_BUILD_ROOT%{install_path} + +%clean + +%files +%defattr(-,root,root) +%{install_path}/consumer +%{install_path}/consumer.cnf + +%changelog diff --git a/server/MaxScale_template.cnf b/server/MaxScale_template.cnf index 124fc923d..c322fec8f 100644 --- a/server/MaxScale_template.cnf +++ b/server/MaxScale_template.cnf @@ -13,7 +13,7 @@ threads=1 # Define a monitor that can be used to determine the state and role of # the servers. # -# Valid options are: +# Valid options for all monitors are: # # module= # servers=,,... @@ -29,6 +29,16 @@ module=mysqlmon servers=server1,server2,server3 user=maxuser passwd=maxpwd +# +# options for mysql_monitor only +# +# detect_replication_lag= +# detect_stale_master= # A series of service definition # @@ -41,7 +51,8 @@ passwd=maxpwd # enable_root_user=<0 or 1, default is 0> # version_string= -# +# +# use_sql_variables_in=[master|all] (default all) # router_options=,,... # where value=[master|slave|synced] # @@ -60,6 +71,7 @@ router=readwritesplit servers=server1,server2,server3 user=maxuser passwd=maxpwd +use_sql_variables_in=all max_slave_connections=50% max_slave_replication_lag=30 router_options=slave_selection_criteria=LEAST_BEHIND_MASTER diff --git a/server/core/Makefile b/server/core/Makefile index 9a807a925..7ad4e1b01 100644 --- a/server/core/Makefile +++ b/server/core/Makefile @@ -33,7 +33,9 @@ # 29/06/13 Vilho Raatikka Reverted Query classifier changes because # gateway needs mysql client lib, not qc. # 24/07/13 Mark Ridoch Addition of encryption routines -# 30/05/14 Mark Ridoch Filter API added +# 30/05/14 Mark Riddoch Filter API added +# 25/07/14 Mark Riddoch Addition of hints +# 29/08/14 Mark Riddoch Added housekeeper include ../../build_gateway.inc @@ -47,17 +49,23 @@ CFLAGS=-c -I/usr/include -I../include -I../modules/include -I../inih \ -I$(LOGPATH) -I$(UTILSPATH) \ -Wall -g -include ../../makefile.inc - LDFLAGS=-rdynamic -L$(LOGPATH) \ -Wl,-rpath,$(DEST)/lib \ -Wl,-rpath,$(LOGPATH) -Wl,-rpath,$(UTILSPATH) \ -Wl,-rpath,$(EMBEDDED_LIB) + +LIBS=-L$(EMBEDDED_LIB) \ + -lmysqld \ + -lz -lm -lcrypt -lcrypto -ldl -laio -lrt -pthread -llog_manager \ + -L../inih/extra -linih -lssl -lstdc++ + +include ../../makefile.inc + SRCS= atomic.c buffer.c spinlock.c gateway.c \ gw_utils.c utils.c dcb.c load_utils.c session.c service.c server.c \ poll.c config.c users.c hashtable.c dbusers.c thread.c gwbitmask.c \ - monitor.c adminusers.c secrets.c filter.c modutil.c + monitor.c adminusers.c secrets.c filter.c modutil.c hint.c housekeeper.c HDRS= ../include/atomic.h ../include/buffer.h ../include/dcb.h \ ../include/gw.h ../modules/include/mysql_client_server_protocol.h \ @@ -65,18 +73,13 @@ HDRS= ../include/atomic.h ../include/buffer.h ../include/dcb.h \ ../include/modules.h ../include/poll.h ../include/config.h \ ../include/users.h ../include/hashtable.h ../include/gwbitmask.h \ ../include/adminusers.h ../include/version.h ../include/maxscale.h \ - ../include/filter.h modutil.h + ../include/filter.h ../include/modutil.h ../hint.h ../include/housekeeper.h OBJ=$(SRCS:.c=.o) KOBJS=maxkeys.o secrets.o utils.o POBJS=maxpasswd.o secrets.o utils.o -LIBS=-L$(EMBEDDED_LIB) \ - -lmysqld \ - -lz -lm -lcrypt -lcrypto -ldl -laio -lrt -pthread -llog_manager \ - -L../inih/extra -linih -lssl -lstdc++ - all: maxscale maxkeys maxpasswd cleantests: diff --git a/server/core/buffer.c b/server/core/buffer.c index 26c2c1439..290da6bde 100644 --- a/server/core/buffer.c +++ b/server/core/buffer.c @@ -32,6 +32,9 @@ * 11/07/13 Mark Riddoch Add reference count mechanism * 16/07/2013 Massimiliano Pinto Added command type to gwbuf struct * 24/06/2014 Mark Riddoch Addition of gwbuf_trim + * 15/07/2014 Mark Riddoch Addition of properties + * 28/08/2014 Mark Riddoch Adition of tail pointer to speed + * the gwbuf_append process * * @endverbatim */ @@ -40,6 +43,11 @@ #include #include +static buffer_object_t* gwbuf_remove_buffer_object( + GWBUF* buf, + buffer_object_t* bufobj); + + /** * Allocate a new gateway buffer structure of size bytes. * @@ -77,13 +85,18 @@ SHARED_BUF *sbuf; free(sbuf); return NULL; } + spinlock_init(&rval->gwbuf_lock); rval->start = sbuf->data; rval->end = rval->start + size; sbuf->refcount = 1; rval->sbuf = sbuf; rval->next = NULL; + rval->tail = rval; + rval->hint = NULL; + rval->properties = NULL; rval->gwbuf_type = GWBUF_TYPE_UNDEFINED; - rval->command = 0; + rval->gwbuf_info = GWBUF_INFO_NONE; + rval->gwbuf_bufobj = NULL; CHK_GWBUF(rval); return rval; } @@ -96,12 +109,38 @@ SHARED_BUF *sbuf; void gwbuf_free(GWBUF *buf) { +BUF_PROPERTY *prop; + + buffer_object_t* bo; + CHK_GWBUF(buf); if (atomic_add(&buf->sbuf->refcount, -1) == 1) { free(buf->sbuf->data); free(buf->sbuf); + bo = buf->gwbuf_bufobj; + + while (bo != NULL) + { + bo = gwbuf_remove_buffer_object(buf, bo); + } + } + while (buf->properties) + { + prop = buf->properties; + buf->properties = prop->next; + free(prop->name); + free(prop->value); + free(prop); + } + /** Release the hint */ + while (buf->hint) + { + HINT* h = buf->hint; + buf->hint = buf->hint->next; + hint_free(h); + } free(buf); } @@ -130,7 +169,12 @@ GWBUF *rval; rval->start = buf->start; rval->end = buf->end; rval->gwbuf_type = buf->gwbuf_type; + rval->properties = NULL; + rval->hint = NULL; + rval->gwbuf_info = buf->gwbuf_info; + rval->gwbuf_bufobj = buf->gwbuf_bufobj; rval->next = NULL; + rval->tail = rval; CHK_GWBUF(rval); return rval; } @@ -156,7 +200,12 @@ GWBUF *gwbuf_clone_portion( clonebuf->start = (void *)((char*)buf->start)+start_offset; clonebuf->end = (void *)((char *)clonebuf->start)+length; clonebuf->gwbuf_type = buf->gwbuf_type; /*< clone the type for now */ + clonebuf->properties = NULL; + clonebuf->hint = NULL; + clonebuf->gwbuf_info = buf->gwbuf_info; + clonebuf->gwbuf_bufobj = buf->gwbuf_bufobj; clonebuf->next = NULL; + clonebuf->tail = clonebuf; CHK_GWBUF(clonebuf); return clonebuf; @@ -233,11 +282,8 @@ GWBUF *ptr = head; if (!head) return tail; CHK_GWBUF(head); - while (ptr->next) - { - ptr = ptr->next; - } - ptr->next = tail; + head->tail->next = tail; + head->tail = tail->tail; return head; } @@ -262,6 +308,7 @@ GWBUF * gwbuf_consume(GWBUF *head, unsigned int length) { GWBUF *rval = head; + CHK_GWBUF(head); GWBUF_CONSUME(head, length); CHK_GWBUF(head); @@ -269,8 +316,13 @@ GWBUF *rval = head; if (GWBUF_EMPTY(head)) { rval = head->next; + if (head->next) + head->next->tail = head->tail; + gwbuf_free(head); } + + ss_dassert(rval == NULL || (rval->end > rval->start)); return rval; } @@ -302,6 +354,8 @@ int rval = 0; * buffer has n_bytes or less then it will be freed and * NULL will be returned. * + * This routine assumes the buffer is not part of a chain + * * @param buf The buffer to trim * @param n_bytes The number of bytes to trim off * @return The buffer chain or NULL if buffer has <= n_bytes @@ -309,6 +363,8 @@ int rval = 0; GWBUF * gwbuf_trim(GWBUF *buf, unsigned int n_bytes) { + ss_dassert(buf->next == NULL); + if (GWBUF_LENGTH(buf) <= n_bytes) { gwbuf_consume(buf, GWBUF_LENGTH(buf)); @@ -338,5 +394,192 @@ void gwbuf_set_type( } } +/** + * Add a buffer object to GWBUF buffer. + * + * @param buf GWBUF where object is added + * @param id Type identifier for object + * @param data Object data + * @param donefun_dp Clean-up function to be executed before buffer is freed. + */ +void gwbuf_add_buffer_object( + GWBUF* buf, + bufobj_id_t id, + void* data, + void (*donefun_fp)(void *)) +{ + buffer_object_t** p_b; + buffer_object_t* newb; + + CHK_GWBUF(buf); + newb = (buffer_object_t *)malloc(sizeof(buffer_object_t)); + newb->bo_id = id; + newb->bo_data = data; + newb->bo_donefun_fp = donefun_fp; + newb->bo_next = NULL; + /** Lock */ + spinlock_acquire(&buf->gwbuf_lock); + p_b = &buf->gwbuf_bufobj; + /** Search the end of the list and add there */ + while (*p_b != NULL) + { + p_b = &(*p_b)->bo_next; + } + *p_b = newb; + /** Set flag */ + buf->gwbuf_info |= GWBUF_INFO_PARSED; + /** Unlock */ + spinlock_release(&buf->gwbuf_lock); +} + +/** + * Search buffer object which matches with the id. + * + * @param buf GWBUF to be searched + * @param id Identifier for the object + * + * @return Searched buffer object or NULL if not found + */ +void* gwbuf_get_buffer_object_data( + GWBUF* buf, + bufobj_id_t id) +{ + buffer_object_t* bo; + + CHK_GWBUF(buf); + /** Lock */ + spinlock_acquire(&buf->gwbuf_lock); + bo = buf->gwbuf_bufobj; + + while (bo != NULL && bo->bo_id != id) + { + bo = bo->bo_next; + } + /** Unlock */ + spinlock_release(&buf->gwbuf_lock); + + return bo->bo_data; +} + +/** + * @return pointer to next buffer object or NULL + */ +static buffer_object_t* gwbuf_remove_buffer_object( + GWBUF* buf, + buffer_object_t* bufobj) +{ + buffer_object_t* next; + + next = bufobj->bo_next; + /** Call corresponding clean-up function to clean buffer object's data */ + bufobj->bo_donefun_fp(bufobj->bo_data); + free(bufobj); + return next; +} + + + +/** + * Add a property to a buffer. + * + * @param buf The buffer to add the property to + * @param name The property name + * @param value The property value + * @return Non-zero on success + */ +int +gwbuf_add_property(GWBUF *buf, char *name, char *value) +{ +BUF_PROPERTY *prop; + + if ((prop = malloc(sizeof(BUF_PROPERTY))) == NULL) + return 0; + + prop->name = strdup(name); + prop->value = strdup(value); + spinlock_acquire(&buf->gwbuf_lock); + prop->next = buf->properties; + buf->properties = prop; + spinlock_release(&buf->gwbuf_lock); + return 1; +} + +/** + * Return the value of a buffer property + * @param buf The buffer itself + * @param name The name of the property to return + * @return The property value or NULL if the property was not found. + */ +char * +gwbuf_get_property(GWBUF *buf, char *name) +{ +BUF_PROPERTY *prop; + + spinlock_acquire(&buf->gwbuf_lock); + prop = buf->properties; + while (prop && strcmp(prop->name, name) != 0) + prop = prop->next; + spinlock_release(&buf->gwbuf_lock); + if (prop) + return prop->value; + return NULL; +} +/** + * Convert a chain of GWBUF structures into a single GWBUF structure + * + * @param orig The chain to convert + * @return The contiguous buffer + */ +GWBUF * +gwbuf_make_contiguous(GWBUF *orig) +{ +GWBUF *newbuf; +char *ptr; +int len; + + if (orig->next == NULL) + return orig; + + if ((newbuf = gwbuf_alloc(gwbuf_length(orig))) != NULL) + { + ptr = GWBUF_DATA(newbuf); + while (orig) + { + len = GWBUF_LENGTH(orig); + memcpy(ptr, GWBUF_DATA(orig), len); + ptr += len; + orig = gwbuf_consume(orig, len); + } + } + return newbuf; +} + +/** + * Add hint to a buffer. + * + * @param buf The buffer to add the hint to + * @param hint The hint itself + * @return Non-zero on success + */ +int +gwbuf_add_hint(GWBUF *buf, HINT *hint) +{ +HINT *ptr; + + spinlock_acquire(&buf->gwbuf_lock); + if (buf->hint) + { + ptr = buf->hint; + while (ptr->next) + ptr = ptr->next; + ptr->next = hint; + } + else + { + buf->hint = hint; + } + spinlock_release(&buf->gwbuf_lock); + return 1; +} diff --git a/server/core/config.c b/server/core/config.c index 48ade63ea..96d05eeef 100644 --- a/server/core/config.c +++ b/server/core/config.c @@ -34,6 +34,7 @@ * 29/05/14 Mark Riddoch Addition of filter definition * 23/05/14 Massimiliano Pinto Added automatic set of maxscale-id: first listening ipv4_raw + port + pid * 28/05/14 Massimiliano Pinto Added detect_replication_lag parameter + * 28/08/14 Massimiliano Pinto Added detect_stale_master parameter * 09/09/14 Massimiliano Pinto Added localhost_match_any parameter * * @endverbatim @@ -41,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -264,18 +266,28 @@ int error_count = 0; { char* max_slave_conn_str; char* max_slave_rlag_str; + char *user; + char *auth; + char *enable_root_user; + char *weightby; + char *version_string; + bool is_rwsplit = false; obj->element = service_alloc(obj->object, router); - char *user = - config_get_value(obj->parameters, "user"); - char *auth = - config_get_value(obj->parameters, "passwd"); - char *enable_root_user = - config_get_value(obj->parameters, "enable_root_user"); - char *weightby = - config_get_value(obj->parameters, "weightby"); + user = config_get_value(obj->parameters, "user"); + auth = config_get_value(obj->parameters, "passwd"); + enable_root_user = config_get_value( + obj->parameters, + "enable_root_user"); + weightby = config_get_value(obj->parameters, "weightby"); - char *version_string = config_get_value(obj->parameters, "version_string"); + version_string = config_get_value(obj->parameters, + "version_string"); + /** flag for rwsplit-specific parameters */ + if (strncmp(router, "readwritesplit", strlen("readwritesplit")+1) == 0) + { + is_rwsplit = true; + } char *allow_localhost_match_any = config_get_value(obj->parameters, "localhost_match_any"); @@ -347,13 +359,20 @@ int error_count = 0; param = config_get_param(obj->parameters, "max_slave_connections"); - succp = service_set_param_value( - obj->element, - param, - max_slave_conn_str, - COUNT_ATMOST, - (COUNT_TYPE|PERCENT_TYPE)); - + if (param == NULL) + { + succp = false; + } + else + { + succp = service_set_param_value( + obj->element, + param, + max_slave_conn_str, + COUNT_ATMOST, + (COUNT_TYPE|PERCENT_TYPE)); + } + if (!succp) { LOGIF(LM, (skygw_log_write( @@ -379,13 +398,20 @@ int error_count = 0; obj->parameters, "max_slave_replication_lag"); - succp = service_set_param_value( - obj->element, - param, - max_slave_rlag_str, - COUNT_ATMOST, - COUNT_TYPE); - + if (param == NULL) + { + succp = false; + } + else + { + succp = service_set_param_value( + obj->element, + param, + max_slave_rlag_str, + COUNT_ATMOST, + COUNT_TYPE); + } + if (!succp) { LOGIF(LM, (skygw_log_write( @@ -399,7 +425,51 @@ int error_count = 0; param->value))); } } - } + /** Parameters for rwsplit router only */ + if (is_rwsplit) + { + CONFIG_PARAMETER* param; + char* use_sql_variables_in; + bool succp; + + use_sql_variables_in = + config_get_value(obj->parameters, + "use_sql_variables_in"); + + if (use_sql_variables_in != NULL) + { + param = config_get_param( + obj->parameters, + "use_sql_variables_in"); + + if (param == NULL) + { + succp = false; + } + else + { + succp = service_set_param_value(obj->element, + param, + use_sql_variables_in, + COUNT_NONE, + SQLVAR_TARGET_TYPE); + } + + if (!succp) + { + LOGIF(LM, (skygw_log_write( + LOGFILE_MESSAGE, + "* Warning : invalid value type " + "for parameter \'%s.%s = %s\'\n\tExpected " + "type is [master|all] for " + "use sql variables in.", + ((SERVICE*)obj->element)->name, + param->name, + param->value))); + } + } + } /*< if (rw_split) */ + } /*< if (router) */ else { obj->element = NULL; @@ -555,17 +625,29 @@ int error_count = 0; while (s) { CONFIG_CONTEXT *obj1 = context; + int found = 0; while (obj1) { if (strcmp(trim(s), obj1->object) == 0 && obj->element && obj1->element) { + found = 1; serviceAddBackend( obj->element, obj1->element); } obj1 = obj1->next; } + if (!found) + { + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Error: Unable to find " + "server '%s' that is " + "configured as part of " + "service '%s'.", + s, obj->object))); + } s = strtok(NULL, ","); } } @@ -573,7 +655,7 @@ int error_count = 0; { LOGIF(LE, (skygw_log_write_flush( LOGFILE_ERROR, - "Error : The service '%s' is missing a " + "Warning: The service '%s' is missing a " "definition of the servers that provide " "the service.", obj->object))); @@ -676,6 +758,7 @@ int error_count = 0; char *passwd; unsigned long interval = 0; int replication_heartbeat = 0; + int detect_stale_master = 0; module = config_get_value(obj->parameters, "module"); servers = config_get_value(obj->parameters, "servers"); @@ -689,6 +772,10 @@ int error_count = 0; replication_heartbeat = atoi(config_get_value(obj->parameters, "detect_replication_lag")); } + if (config_get_value(obj->parameters, "detect_stale_master")) { + detect_stale_master = atoi(config_get_value(obj->parameters, "detect_stale_master")); + } + if (module) { obj->element = monitor_alloc(obj->object, module); @@ -712,22 +799,38 @@ int error_count = 0; if(replication_heartbeat == 1) monitorSetReplicationHeartbeat(obj->element, replication_heartbeat); + /* detect stale master */ + if(detect_stale_master == 1) + monitorDetectStaleMaster(obj->element, detect_stale_master); + /* get the servers to monitor */ s = strtok(servers, ","); while (s) { CONFIG_CONTEXT *obj1 = context; + int found = 0; while (obj1) { if (strcmp(s, obj1->object) == 0 && obj->element && obj1->element) { + found = 1; monitorAddServer( obj->element, obj1->element); } obj1 = obj1->next; } + if (!found) + LOGIF(LE, + (skygw_log_write_flush( + LOGFILE_ERROR, + "Error: Unable to find " + "server '%s' that is " + "configured in the " + "monitor '%s'.", + s, obj->object))); + s = strtok(NULL, ","); } } @@ -829,12 +932,15 @@ config_param_type_t config_get_paramtype( return param->qfd_param_type; } -int config_get_valint( +bool config_get_valint( + int* val, CONFIG_PARAMETER* param, const char* name, /*< if NULL examine current param only */ config_param_type_t ptype) -{ - int val = -1; /*< -1 indicates failure */ +{ + bool succp = false;; + + ss_dassert((ptype == COUNT_TYPE || ptype == PERCENT_TYPE) && param != NULL); while (param) { @@ -842,32 +948,95 @@ int config_get_valint( { switch (ptype) { case COUNT_TYPE: - val = param->qfd.valcount; - goto return_val; + *val = param->qfd.valcount; + succp = true; + goto return_succp; case PERCENT_TYPE: - val = param->qfd.valpercent; - goto return_val; - - case BOOL_TYPE: - val = param->qfd.valbool; - goto return_val; - - default: - goto return_val; + *val = param->qfd.valpercent; + succp =true; + goto return_succp; + + default: + goto return_succp; } } - else if (name == NULL) - { - goto return_val; - } param = param->next; } -return_val: - return val; +return_succp: + return succp; } +bool config_get_valbool( + bool* val, + CONFIG_PARAMETER* param, + const char* name, + config_param_type_t ptype) +{ + bool succp; + + ss_dassert(ptype == BOOL_TYPE); + ss_dassert(param != NULL); + + if (ptype != BOOL_TYPE || param == NULL) + { + succp = false; + goto return_succp; + } + + while (param) + { + if (name == NULL || !strncmp(param->name, name, MAX_PARAM_LEN)) + { + *val = param->qfd.valbool; + succp = true; + goto return_succp; + } + param = param->next; + } + succp = false; + +return_succp: + return succp; + +} + + +bool config_get_valtarget( + target_t* val, + CONFIG_PARAMETER* param, + const char* name, + config_param_type_t ptype) +{ + bool succp; + + ss_dassert(ptype == SQLVAR_TARGET_TYPE); + ss_dassert(param != NULL); + + if (ptype != SQLVAR_TARGET_TYPE || param == NULL) + { + succp = false; + goto return_succp; + } + + while (param) + { + if (name == NULL || !strncmp(param->name, name, MAX_PARAM_LEN)) + { + *val = param->qfd.valtarget; + succp = true; + goto return_succp; + } + param = param->next; + } + succp = false; + +return_succp: + return succp; + +} + CONFIG_PARAMETER* config_clone_param( CONFIG_PARAMETER* param) { @@ -932,6 +1101,15 @@ config_threadcount() return gateway.n_threads; } +static struct { + char *logname; + logfile_id_t logfile; +} lognames[] = { + { "log_messages", LOGFILE_MESSAGE }, + { "log_trace", LOGFILE_TRACE }, + { "log_debug", LOGFILE_DEBUG }, + { NULL, 0 } +}; /** * Configuration handler for items in the global [MaxScale] section * @@ -942,10 +1120,20 @@ config_threadcount() static int handle_global_item(const char *name, const char *value) { +int i; if (strcmp(name, "threads") == 0) { gateway.n_threads = atoi(value); } else { - return 0; + for (i = 0; lognames[i].logname; i++) + { + if (strcasecmp(name, lognames[i].logname) == 0) + { + if (atoi(value)) + skygw_log_enable(lognames[i].logfile); + else + skygw_log_disable(lognames[i].logfile); + } + } } return 1; } @@ -1053,13 +1241,20 @@ SERVER *server; param = config_get_param(obj->parameters, "max_slave_connections"); - succp = service_set_param_value( - service, - param, - max_slave_conn_str, - COUNT_ATMOST, - (PERCENT_TYPE|COUNT_TYPE)); - + if (param == NULL) + { + succp = false; + } + else + { + succp = service_set_param_value( + service, + param, + max_slave_conn_str, + COUNT_ATMOST, + (PERCENT_TYPE|COUNT_TYPE)); + } + if (!succp) { LOGIF(LM, (skygw_log_write( @@ -1089,13 +1284,20 @@ SERVER *server; obj->parameters, "max_slave_replication_lag"); - succp = service_set_param_value( - service, - param, - max_slave_rlag_str, - COUNT_ATMOST, - COUNT_TYPE); - + if (param == NULL) + { + succp = false; + } + else + { + succp = service_set_param_value( + service, + param, + max_slave_rlag_str, + COUNT_ATMOST, + COUNT_TYPE); + } + if (!succp) { LOGIF(LM, (skygw_log_write( @@ -1237,11 +1439,13 @@ SERVER *server; while (s) { CONFIG_CONTEXT *obj1 = context; + int found = 0; while (obj1) { if (strcmp(s, obj1->object) == 0 && obj->element && obj1->element) { + found = 1; if (!serviceHasBackend(obj->element, obj1->element)) { serviceAddBackend( @@ -1251,6 +1455,16 @@ SERVER *server; } obj1 = obj1->next; } + if (!found) + { + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Error: Unable to find " + "server '%s' that is " + "configured as part of " + "service '%s'.", + s, obj->object))); + } s = strtok(NULL, ","); } } @@ -1352,6 +1566,7 @@ static char *service_params[] = "localhost_match_any", "max_slave_connections", "max_slave_replication_lag", + "use_sql_variables_in", /*< rwsplit only */ "version_string", "filters", NULL @@ -1388,6 +1603,7 @@ static char *monitor_params[] = "passwd", "monitor_interval", "detect_replication_lag", + "detect_stale_master", NULL }; /** @@ -1474,7 +1690,11 @@ bool config_set_qualified_param( param->qfd.valbool = *(bool *)val; succp = true; break; - + + case SQLVAR_TARGET_TYPE: + param->qfd.valtarget = *(target_t *)val; + succp = true; + break; default: succp = false; break; diff --git a/server/core/dbusers.c b/server/core/dbusers.c index dd36d683c..d318fd5f2 100644 --- a/server/core/dbusers.c +++ b/server/core/dbusers.c @@ -187,14 +187,6 @@ getUsers(SERVICE *service, struct users *users) if (service_user == NULL || service_passwd == NULL) return -1; - /** multi-thread environment requires that thread init succeeds. */ - if (mysql_thread_init()) { - LOGIF(LE, (skygw_log_write_flush( - LOGFILE_ERROR, - "Error : mysql_thread_init failed."))); - return -1; - } - con = mysql_init(NULL); if (con == NULL) { @@ -388,10 +380,9 @@ getUsers(SERVICE *service, struct users *users) memcpy(users->cksum, hash, SHA_DIGEST_LENGTH); free(users_data); - + free(key.user); mysql_free_result(result); mysql_close(con); - mysql_thread_end(); return total_users; } diff --git a/server/core/dcb.c b/server/core/dcb.c index 382940626..001989c07 100644 --- a/server/core/dcb.c +++ b/server/core/dcb.c @@ -89,7 +89,13 @@ static int dcb_null_write(DCB *dcb, GWBUF *buf); static int dcb_null_close(DCB *dcb); static int dcb_null_auth(DCB *dcb, SERVER *server, SESSION *session, GWBUF *buf); -DCB* dcb_get_zombies(void) +/** + * Return the pointer to the lsit of zombie DCB's + * + * @return Zombies DCB list + */ +DCB * +dcb_get_zombies(void) { return zombies; } @@ -128,6 +134,12 @@ DCB *rval; spinlock_init(&rval->delayqlock); spinlock_init(&rval->authlock); spinlock_init(&rval->cb_lock); + spinlock_init(&rval->pollinlock); + spinlock_init(&rval->polloutlock); + rval->pollinbusy = 0; + rval->readcheck = 0; + rval->polloutbusy = 0; + rval->writecheck = 0; rval->fd = -1; memset(&rval->stats, 0, sizeof(DCBSTATS)); // Zero the statistics rval->state = DCB_STATE_ALLOC; @@ -376,11 +388,6 @@ DCB_CALLBACK *cb; } spinlock_release(&dcb->cb_lock); - if (dcb->dcb_readqueue) - { - GWBUF* queue = dcb->dcb_readqueue; - while ((queue = gwbuf_consume(queue, GWBUF_LENGTH(queue))) != NULL); - } bitmask_free(&dcb->memdata.bitmask); simple_mutex_done(&dcb->dcb_read_lock); simple_mutex_done(&dcb->dcb_write_lock); @@ -399,7 +406,7 @@ DCB_CALLBACK *cb; * * @param threadid The thread ID of the caller */ -DCB* +DCB * dcb_process_zombies(int threadid) { DCB *ptr, *lptr; @@ -815,8 +822,6 @@ int below_water; spinlock_acquire(&dcb->writeqlock); - ss_dassert(dcb->state != DCB_STATE_ZOMBIE); - if (dcb->writeq != NULL) { /* @@ -1187,7 +1192,7 @@ printDCB(DCB *dcb) if (dcb->remote) printf("\tConnected to: %s\n", dcb->remote); if (dcb->user) - printf("\tUsername to: %s\n", dcb->user); + printf("\tUsername to: %s\n", dcb->user); if (dcb->writeq) printf("\tQueued write data: %d\n",gwbuf_length(dcb->writeq)); printf("\tStatistics:\n"); @@ -1204,6 +1209,19 @@ printDCB(DCB *dcb) printf("\t\tNo. of Low Water Events: %d\n", dcb->stats.n_low_water); } +/** + * Display an entry from the spinlock statistics data + * + * @param dcb The DCB to print to + * @param desc Description of the statistic + * @param value The statistic value + */ +static void +spin_reporter(void *dcb, char *desc, int value) +{ + dcb_printf((DCB *)dcb, "\t\t%-35s %d\n", desc, value); +} + /** * Diagnostic to print all DCB allocated in the system @@ -1233,6 +1251,12 @@ void dprintAllDCBs(DCB *pdcb) DCB *dcb; spinlock_acquire(&dcbspin); +#if SPINLOCK_PROFILE + dcb_printf(pdcb, "DCB List Spinlock Statistics:\n"); + spinlock_stats(&dcbspin, spin_reporter, pdcb); + dcb_printf(pdcb, "Zombie Queue Lock Statistics:\n"); + spinlock_stats(&zombiespin, spin_reporter, pdcb); +#endif dcb = allDCBs; while (dcb) { @@ -1252,12 +1276,16 @@ DCB *dcb; dcb_printf(pdcb, "\tQueued write data: %d\n", gwbuf_length(dcb->writeq)); dcb_printf(pdcb, "\tStatistics:\n"); - dcb_printf(pdcb, "\t\tNo. of Reads: %d\n", dcb->stats.n_reads); - dcb_printf(pdcb, "\t\tNo. of Writes: %d\n", dcb->stats.n_writes); - dcb_printf(pdcb, "\t\tNo. of Buffered Writes: %d\n", dcb->stats.n_buffered); - dcb_printf(pdcb, "\t\tNo. of Accepts: %d\n", dcb->stats.n_accepts); - dcb_printf(pdcb, "\t\tNo. of High Water Events: %d\n", dcb->stats.n_high_water); - dcb_printf(pdcb, "\t\tNo. of Low Water Events: %d\n", dcb->stats.n_low_water); + dcb_printf(pdcb, "\t\tNo. of Reads: %d\n", dcb->stats.n_reads); + dcb_printf(pdcb, "\t\tNo. of Writes: %d\n", dcb->stats.n_writes); + dcb_printf(pdcb, "\t\tNo. of Buffered Writes: %d\n", dcb->stats.n_buffered); + dcb_printf(pdcb, "\t\tNo. of Accepts: %d\n", dcb->stats.n_accepts); + dcb_printf(pdcb, "\t\tNo. of busy polls: %d\n", dcb->stats.n_busypolls); + dcb_printf(pdcb, "\t\tNo. of read rechecks: %d\n", dcb->stats.n_readrechecks); + dcb_printf(pdcb, "\t\tNo. of busy write polls: %d\n", dcb->stats.n_busywrpolls); + dcb_printf(pdcb, "\t\tNo. of write rechecks: %d\n", dcb->stats.n_writerechecks); + dcb_printf(pdcb, "\t\tNo. of High Water Events: %d\n", dcb->stats.n_high_water); + dcb_printf(pdcb, "\t\tNo. of Low Water Events: %d\n", dcb->stats.n_low_water); if (dcb->flags & DCBF_CLONE) dcb_printf(pdcb, "\t\tDCB is a clone.\n"); dcb = dcb->next; @@ -1278,20 +1306,20 @@ DCB *dcb; spinlock_acquire(&dcbspin); dcb = allDCBs; dcb_printf(pdcb, "Descriptor Control Blocks\n"); - dcb_printf(pdcb, "------------+----------------------------+----------------------+----------\n"); - dcb_printf(pdcb, " %-10s | %-26s | %-20s | %s\n", + dcb_printf(pdcb, "------------------+----------------------------+--------------------+----------\n"); + dcb_printf(pdcb, " %-16s | %-26s | %-18s | %s\n", "DCB", "State", "Service", "Remote"); - dcb_printf(pdcb, "------------+----------------------------+----------------------+----------\n"); + dcb_printf(pdcb, "------------------+----------------------------+--------------------+----------\n"); while (dcb) { - dcb_printf(pdcb, " %10p | %-26s | %-20s | %s\n", + dcb_printf(pdcb, " %-16p | %-26s | %-18s | %s\n", dcb, gw_dcb_state2string(dcb->state), - (dcb->session->service ? - dcb->session->service->name : ""), + + ((dcb->session && dcb->session->service) ? dcb->session->service->name : ""), (dcb->remote ? dcb->remote : "")); dcb = dcb->next; } - dcb_printf(pdcb, "------------+----------------------------+----------------------+----------\n\n"); + dcb_printf(pdcb, "------------------+----------------------------+--------------------+----------\n\n"); spinlock_release(&dcbspin); } @@ -1308,16 +1336,16 @@ DCB *dcb; spinlock_acquire(&dcbspin); dcb = allDCBs; dcb_printf(pdcb, "Client Connections\n"); - dcb_printf(pdcb, "-----------------+------------+----------------------+------------\n"); - dcb_printf(pdcb, " %-15s | %-10s | %-20s | %s\n", + dcb_printf(pdcb, "-----------------+------------------+----------------------+------------\n"); + dcb_printf(pdcb, " %-15s | %-16s | %-20s | %s\n", "Client", "DCB", "Service", "Session"); - dcb_printf(pdcb, "-----------------+------------+----------------------+------------\n"); + dcb_printf(pdcb, "-----------------+------------------+----------------------+------------\n"); while (dcb) { if (dcb_isclient(dcb) && dcb->dcb_role == DCB_ROLE_REQUEST_HANDLER) { - dcb_printf(pdcb, " %-15s | %10p | %-20s | %10p\n", + dcb_printf(pdcb, " %-15s | %16p | %-20s | %10p\n", (dcb->remote ? dcb->remote : ""), dcb, (dcb->session->service ? dcb->session->service->name : ""), @@ -1325,7 +1353,7 @@ DCB *dcb; } dcb = dcb->next; } - dcb_printf(pdcb, "-----------------+------------+----------------------+------------\n\n"); + dcb_printf(pdcb, "-----------------+------------------+----------------------+------------\n\n"); spinlock_release(&dcbspin); } @@ -1342,16 +1370,18 @@ dprintDCB(DCB *pdcb, DCB *dcb) dcb_printf(pdcb, "DCB: %p\n", (void *)dcb); dcb_printf(pdcb, "\tDCB state: %s\n", gw_dcb_state2string(dcb->state)); if (dcb->session && dcb->session->service) - dcb_printf(pdcb, "\tService: %s\n", + dcb_printf(pdcb, "\tService: %s\n", dcb->session->service->name); if (dcb->remote) dcb_printf(pdcb, "\tConnected to: %s\n", dcb->remote); if (dcb->user) - dcb_printf(pdcb, "\tUsername: %s\n", + dcb_printf(pdcb, "\tUsername: %s\n", dcb->user); dcb_printf(pdcb, "\tOwning Session: %p\n", dcb->session); if (dcb->writeq) dcb_printf(pdcb, "\tQueued write data: %d\n", gwbuf_length(dcb->writeq)); + if (dcb->delayq) + dcb_printf(pdcb, "\tDelayed write data: %d\n", gwbuf_length(dcb->delayq)); dcb_printf(pdcb, "\tStatistics:\n"); dcb_printf(pdcb, "\t\tNo. of Reads: %d\n", dcb->stats.n_reads); @@ -1361,12 +1391,30 @@ dprintDCB(DCB *pdcb, DCB *dcb) dcb->stats.n_buffered); dcb_printf(pdcb, "\t\tNo. of Accepts: %d\n", dcb->stats.n_accepts); + dcb_printf(pdcb, "\t\tNo. of busy polls: %d\n", dcb->stats.n_busypolls); + dcb_printf(pdcb, "\t\tNo. of read rechecks: %d\n", dcb->stats.n_readrechecks); + dcb_printf(pdcb, "\t\tNo. of busy write polls: %d\n", dcb->stats.n_busywrpolls); + dcb_printf(pdcb, "\t\tNo. of write rechecks: %d\n", dcb->stats.n_writerechecks); dcb_printf(pdcb, "\t\tNo. of High Water Events: %d\n", dcb->stats.n_high_water); dcb_printf(pdcb, "\t\tNo. of Low Water Events: %d\n", dcb->stats.n_low_water); if (dcb->flags & DCBF_CLONE) dcb_printf(pdcb, "\t\tDCB is a clone.\n"); +#if SPINLOCK_PROFILE + dcb_printf(pdcb, "\tInitlock Statistics:\n"); + spinlock_stats(&dcb->dcb_initlock, spin_reporter, pdcb); + dcb_printf(pdcb, "\tWrite Queue Lock Statistics:\n"); + spinlock_stats(&dcb->writeqlock, spin_reporter, pdcb); + dcb_printf(pdcb, "\tDelay Queue Lock Statistics:\n"); + spinlock_stats(&dcb->delayqlock, spin_reporter, pdcb); + dcb_printf(pdcb, "\tPollin Lock Statistics:\n"); + spinlock_stats(&dcb->pollinlock, spin_reporter, pdcb); + dcb_printf(pdcb, "\tPollout Lock Statistics:\n"); + spinlock_stats(&dcb->polloutlock, spin_reporter, pdcb); + dcb_printf(pdcb, "\tCallback Lock Statistics:\n"); + spinlock_stats(&dcb->cb_lock, spin_reporter, pdcb); +#endif } /** @@ -1719,10 +1767,7 @@ int gw_write( * @return Non-zero (true) if the callback was added */ int -dcb_add_callback( - DCB *dcb, - DCB_REASON reason, - int (*callback)(struct dcb *, DCB_REASON, void *), void *userdata) +dcb_add_callback(DCB *dcb, DCB_REASON reason, int (*callback)(struct dcb *, DCB_REASON, void *), void *userdata) { DCB_CALLBACK *cb, *ptr; int rval = 1; @@ -1754,7 +1799,10 @@ int rval = 1; return 0; } if (cb->next == NULL) + { cb->next = ptr; + break; + } cb = cb->next; } spinlock_release(&dcb->cb_lock); @@ -1775,7 +1823,7 @@ int rval = 1; * @return Non-zero (true) if the callback was removed */ int -dcb_remove_callback(DCB *dcb, DCB_REASON reason, int (*callback)(struct dcb *, DCB_REASON), void *userdata) +dcb_remove_callback(DCB *dcb, DCB_REASON reason, int (*callback)(struct dcb *, DCB_REASON, void *), void *userdata) { DCB_CALLBACK *cb, *pcb = NULL; int rval = 0; @@ -1868,8 +1916,102 @@ int rval = 0; return rval; } -static DCB* dcb_get_next ( - DCB* dcb) +/** + * Called by the EPOLLIN event. Take care of calling the protocol + * read entry point and managing multiple threads competing for the DCB + * without blocking those threads. + * + * This mechanism does away with the need for a mutex on the EPOLLIN event + * and instead implements a queuing mechanism in which nested events are + * queued on the DCB such that when the thread processing the first event + * returns it will read the queued event and process it. This allows the + * thread that woudl otherwise have to wait to process the nested event + * to return immediately and and process other events. + * + * @param dcb The DCB that has data available + */ +void +dcb_pollin(DCB *dcb, int thread_id) +{ + + spinlock_acquire(&dcb->pollinlock); + if (dcb->pollinbusy == 0) + { + dcb->pollinbusy = 1; + do { + if (dcb->readcheck) + { + dcb->stats.n_readrechecks++; + dcb_process_zombies(thread_id); + } + dcb->readcheck = 0; + spinlock_release(&dcb->pollinlock); + dcb->func.read(dcb); + spinlock_acquire(&dcb->pollinlock); + } while (dcb->readcheck); + dcb->pollinbusy = 0; + } + else + { + dcb->stats.n_busypolls++; + dcb->readcheck = 1; + } + spinlock_release(&dcb->pollinlock); +} + + +/** + * Called by the EPOLLOUT event. Take care of calling the protocol + * write_ready entry point and managing multiple threads competing for the DCB + * without blocking those threads. + * + * This mechanism does away with the need for a mutex on the EPOLLOUT event + * and instead implements a queuing mechanism in which nested events are + * queued on the DCB such that when the thread processing the first event + * returns it will read the queued event and process it. This allows the + * thread that would otherwise have to wait to process the nested event + * to return immediately and and process other events. + * + * @param dcb The DCB thats available for writes + */ +void +dcb_pollout(DCB *dcb, int thread_id) +{ + + spinlock_acquire(&dcb->polloutlock); + if (dcb->polloutbusy == 0) + { + dcb->polloutbusy = 1; + do { + if (dcb->writecheck) + { + dcb_process_zombies(thread_id); + dcb->stats.n_writerechecks++; + } + dcb->writecheck = 0; + spinlock_release(&dcb->polloutlock); + dcb->func.write_ready(dcb); + spinlock_acquire(&dcb->polloutlock); + } while (dcb->writecheck); + dcb->polloutbusy = 0; + } + else + { + dcb->stats.n_busywrpolls++; + dcb->writecheck = 1; + } + spinlock_release(&dcb->polloutlock); +} + + +/** + * Get the next DCB in the list of all DCB's + * + * @param dcb The current DCB + * @return The pointer to the next DCB or NULL if this is the last + */ +static DCB * +dcb_get_next (DCB* dcb) { DCB* p; @@ -1903,8 +2045,13 @@ static DCB* dcb_get_next ( return dcb; } -void dcb_call_foreach ( - DCB_REASON reason) +/** + * Call all the callbacks on all DCB's that match the reason given + * + * @param reason The DCB_REASON that triggers the callback + */ +void +dcb_call_foreach(DCB_REASON reason) { switch (reason) { case DCB_REASON_CLOSE: diff --git a/server/core/gateway.c b/server/core/gateway.c index dc174577b..7c674ad34 100644 --- a/server/core/gateway.c +++ b/server/core/gateway.c @@ -51,6 +51,7 @@ #include #include #include +#include #include #include @@ -1492,11 +1493,16 @@ int main(int argc, char **argv) /* Init MaxScale poll system */ poll_init(); - /*< - * Start the services that were created above - */ + /** + * Init mysql thread context for main thread as well. Needed when users + * are queried from backends. + */ + mysql_thread_init(); + + /** Start the services that were created above */ n_services = serviceStartAll(); - if (n_services == 0) + + if (n_services == 0) { char* logerr = "Failed to start any MaxScale services. Exiting."; print_log_n_stderr(true, !daemon_mode, logerr, logerr, 0); @@ -1510,6 +1516,12 @@ int main(int argc, char **argv) log_flush_thr = thread_start( log_flush_cb, (void *)&log_flush_timeout_ms); + + /* + * Start the housekeeper thread + */ + hkinit(); + /*< * Start the polling threads, note this is one less than is * configured as the main thread will also poll. @@ -1549,9 +1561,13 @@ int main(int argc, char **argv) /*< Stop all the monitors */ monitorStopAll(); + LOGIF(LM, (skygw_log_write( LOGFILE_MESSAGE, "MaxScale is shutting down."))); + /** Release mysql thread context*/ + mysql_thread_end(); + datadir_cleanup(); LOGIF(LM, (skygw_log_write( LOGFILE_MESSAGE, diff --git a/server/core/hashtable.c b/server/core/hashtable.c index b945f3631..0f129e825 100644 --- a/server/core/hashtable.c +++ b/server/core/hashtable.c @@ -28,7 +28,7 @@ * and value and to free them. * * The hashtable is arrange as a set of linked lists, the number of linked - * lists beign the hashsize as requested by the user. Entries are hashed by + * lists being the hashsize as requested by the user. Entries are hashed by * calling the hash function that is passed in by the user, this is used as * an index into the array of linked lists, usign modulo hashsize. * @@ -63,6 +63,10 @@ static void hashtable_read_lock(HASHTABLE *table); static void hashtable_read_unlock(HASHTABLE *table); static void hashtable_write_lock(HASHTABLE *table); static void hashtable_write_unlock(HASHTABLE *table); +static HASHTABLE *hashtable_alloc_real(HASHTABLE* target, + int size, + int (*hashfn)(), + int (*cmpfn)()); /** * Special null function used as default memory allfunctions in the hashtable @@ -93,16 +97,44 @@ nullfn(void *data) HASHTABLE * hashtable_alloc(int size, int (*hashfn)(), int (*cmpfn)()) { -HASHTABLE *rval; + return hashtable_alloc_real(NULL, size, hashfn, cmpfn); +} - if ((rval = malloc(sizeof(HASHTABLE))) == NULL) - return NULL; +HASHTABLE* hashtable_alloc_flat( + HASHTABLE* target, + int size, + int (*hashfn)(), + int (*cmpfn)()) +{ + return hashtable_alloc_real(target, size, hashfn, cmpfn); +} +static HASHTABLE * +hashtable_alloc_real( + HASHTABLE* target, + int size, + int (*hashfn)(), + int (*cmpfn)()) +{ + HASHTABLE *rval; + + if (target == NULL) + { + if ((rval = malloc(sizeof(HASHTABLE))) == NULL) + return NULL; + rval->ht_isflat = false; + } + else + { + rval = target; + rval->ht_isflat = true; + } + #if defined(SS_DEBUG) - rval->ht_chk_top = CHK_NUM_HASHTABLE; - rval->ht_chk_tail = CHK_NUM_HASHTABLE; + rval->ht_chk_top = CHK_NUM_HASHTABLE; + rval->ht_chk_tail = CHK_NUM_HASHTABLE; #endif - rval->hashsize = size; + rval->hashsize = size > 0 ? size : 1; rval->hashfn = hashfn; rval->cmpfn = cmpfn; rval->kcopyfn = nullfn; @@ -112,12 +144,12 @@ HASHTABLE *rval; rval->n_readers = 0; rval->writelock = 0; spinlock_init(&rval->spin); - if ((rval->entries = (HASHENTRIES **)calloc(size, sizeof(HASHENTRIES *))) == NULL) + if ((rval->entries = (HASHENTRIES **)calloc(rval->hashsize, sizeof(HASHENTRIES *))) == NULL) { free(rval); return NULL; } - memset(rval->entries, 0, size * sizeof(HASHENTRIES *)); + memset(rval->entries, 0, rval->hashsize * sizeof(HASHENTRIES *)); return rval; } @@ -147,7 +179,11 @@ HASHENTRIES *entry, *ptr; } } free(table->entries); - free(table); + + if (!table->ht_isflat) + { + free(table); + } } /** diff --git a/server/core/hint.c b/server/core/hint.c new file mode 100644 index 000000000..2a463585e --- /dev/null +++ b/server/core/hint.c @@ -0,0 +1,153 @@ +/* + * This file is distributed as part of MaxScale. It is free + * software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, + * version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright SkySQL Ab 2014 + */ +#include +#include +#include + +/** + * @file hint.c generic support routines for hints. + * + * @verbatim + * Revision History + * + * Date Who Description + * 25/07/14 Mark Riddoch Initial implementation + * + * @endverbatim + */ + + +/** + * Duplicate a list of hints + * + * @param hint The hint list to duplicate + * @return A duplicate of the list + */ +HINT * +hint_dup(HINT *hint) +{ +HINT *nlhead = NULL, *nltail = NULL, *ptr1, *ptr2; + + ptr1 = hint; + while (ptr1) + { + if ((ptr2 = (HINT *)malloc(sizeof(HINT))) == NULL) + return nlhead; + ptr2->type = ptr1->type; + if (ptr1->data) + ptr2->data = strdup(ptr1->data); + else + ptr2->data = NULL; + if (ptr1->value) + ptr2->value = strdup(ptr1->value); + else + ptr2->value = NULL; + ptr2->next = NULL; + if (nltail) + { + nltail->next = ptr2; + nltail = ptr2; + } + else + { + nlhead = ptr2; + nltail = ptr2; + } + ptr1 = ptr1->next; + } + return nlhead; +} + +/** + * Create a ROUTE TO type hint + * + * @param head The current hint list + * @param type The HINT_TYPE + * @param data Data may be NULL or the name of a server to route to + * @return The result hint list + */ +HINT * +hint_create_route(HINT *head, HINT_TYPE type, char *data) +{ +HINT *hint; + + if ((hint = (HINT *)malloc(sizeof(HINT))) == NULL) + return head; + hint->next = head; + hint->type = type; + if (data) + hint->data = strdup(data); + else + hint->data = NULL; + hint->value = NULL; + return hint; +} + +/** + * Create name/value parameter hint + * + * @param head The current hint list + * @param pname The parameter name + * @param value The parameter value + * @return The result hint list + */ +HINT * +hint_create_parameter(HINT *head, char *pname, char *value) +{ +HINT *hint; + + if ((hint = (HINT *)malloc(sizeof(HINT))) == NULL) + return head; + hint->next = head; + hint->type = HINT_PARAMETER; + hint->data = pname; + hint->value = strdup(value); + return hint; +} + +/** + * free_hint - free a hint + * + * @param hint The hint to free + */ +void +hint_free(HINT *hint) +{ + if (hint->data) + free(hint->data); + if (hint->value) + free(hint->value); + free(hint); +} + +bool hint_exists( + HINT** p_hint, + HINT_TYPE type) +{ + bool succp = false; + + while (*p_hint != NULL) + { + if ((*p_hint)->type == type) + { + succp = true; + } + p_hint = &(*p_hint)->next; + } + return succp; +} \ No newline at end of file diff --git a/server/core/housekeeper.c b/server/core/housekeeper.c new file mode 100644 index 000000000..6180f24a5 --- /dev/null +++ b/server/core/housekeeper.c @@ -0,0 +1,195 @@ +/* + * This file is distributed as part of the SkySQL Gateway. It is free + * software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, + * version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright SkySQL Ab 2014 + */ +#include +#include +#include +#include + +/** + * @file housekeeper.c Provide a mechanism to run periodic tasks + * + * @verbatim + * Revision History + * + * Date Who Description + * 29/08/14 Mark Riddoch Initial implementation + * + * @endverbatim + */ + +/** + * List of all tasks that need to be run + */ +static HKTASK *tasks = NULL; +/** + * Spinlock to protect the tasks list + */ +static SPINLOCK tasklock = SPINLOCK_INIT; + +static void hkthread(void *); + +/** + * Initialise the housekeeper thread + */ +void +hkinit() +{ + thread_start(hkthread, NULL); +} + +/** + * Add a new task to the housekeepers lists of tasks that should be + * run periodically. + * + * The task will be first run frequency seconds after this call is + * made and will the be executed repeatedly every frequency seconds + * until the task is removed. + * + * Task names must be unique. + * + * @param name The unique name for this housekeeper task + * @param taskfn The function to call for the task + * @param data Data to pass to the task function + * @param frequency How often to run the task, expressed in seconds + * @return Return the tiem in seconds when the task will be first run if the task was added, otherwise 0 + */ +int +hktask_add(char *name, void (*taskfn)(void *), void *data, int frequency) +{ +HKTASK *task, *ptr; + + if ((task = (HKTASK *)malloc(sizeof(HKTASK))) == NULL) + { + return 0; + } + if ((task->name = strdup(name)) == NULL) + { + free(task); + return 0; + } + task->task = taskfn; + task->data = data; + task->frequency = frequency; + task->nextdue = time(0) + frequency; + task->next = NULL; + spinlock_acquire(&tasklock); + ptr = tasks; + while (ptr && ptr->next) + { + if (strcmp(ptr->name, name) == 0) + { + spinlock_release(&tasklock); + free(task->name); + free(task); + return 0; + } + ptr = ptr->next; + } + if (ptr) + ptr->next = task; + else + tasks = task; + spinlock_release(&tasklock); + + return task->nextdue; +} + +/** + * Remove a named task from the housekeepers task list + * + * @param name The task name to remove + * @return Returns 0 if the task could not be removed + */ +int +hktask_remove(char *name) +{ +HKTASK *ptr, *lptr = NULL; + + spinlock_acquire(&tasklock); + ptr = tasks; + while (ptr && strcmp(ptr->name, name) != 0) + { + lptr = ptr; + ptr = ptr->next; + } + if (ptr && lptr) + lptr->next = ptr->next; + else if (ptr) + tasks = ptr->next; + spinlock_release(&tasklock); + + if (ptr) + { + free(ptr->name); + free(ptr); + return 1; + } + else + { + return 0; + } +} + + +/** + * The housekeeper thread implementation. + * + * This function is responsible for executing the housekeeper tasks. + * + * The implementation of the callng of the task functions is such that + * the tasks are called without the tasklock spinlock being held. This + * allows manipulation of the housekeeper task list during execution of + * one of the tasks. The resutl is that upon completion of a task the + * search for tasks to run must restart from the start of the queue. + * It is vital that the task->nextdue tiem is updated before the task + * is run. + * + * @param data Unused, here to satisfy the thread system + */ +void +hkthread(void *data) +{ +HKTASK *ptr; +time_t now; +void (*taskfn)(void *); +void *taskdata; + + for (;;) + { + thread_millisleep(1000); + now = time(0); + spinlock_acquire(&tasklock); + ptr = tasks; + while (ptr) + { + if (ptr->nextdue <= now) + { + ptr->nextdue = now + ptr->frequency; + taskfn = ptr->task; + taskdata = ptr->data; + spinlock_release(&tasklock); + (*taskfn)(taskdata); + spinlock_acquire(&tasklock); + ptr = tasks; + } + else + ptr = ptr->next; + } + spinlock_release(&tasklock); + } +} diff --git a/server/core/modutil.c b/server/core/modutil.c index 78f389ebf..e2800b849 100644 --- a/server/core/modutil.c +++ b/server/core/modutil.c @@ -29,6 +29,7 @@ */ #include #include +#include /** * Check if a GWBUF structure is a MySQL COM_QUERY packet @@ -78,7 +79,7 @@ unsigned char *ptr; *length += (*ptr++ << 8); ptr += 2; // Skip sequence id and COM_QUERY byte *length = *length - 1; - *sql = (char *) ptr; + *sql = (char *)ptr; return 1; } @@ -171,3 +172,57 @@ GWBUF *addition; return orig; } + +/** + * Copy query string from GWBUF buffer to separate memory area. + * + * @param buf GWBUF buffer including the query + * + * @return Plaint text query if the packet type is COM_QUERY. Otherwise return + * a string including the packet type. + */ +char* modutil_get_query( + GWBUF* buf) +{ + uint8_t* packet; + mysql_server_cmd_t packet_type; + size_t len; + char* query_str; + + packet = GWBUF_DATA(buf); + packet_type = packet[4]; + + switch (packet_type) { + case MYSQL_COM_QUIT: + len = strlen("[Quit msg]")+1; + if ((query_str = (char *)malloc(len+1)) == NULL) + { + goto retblock; + } + memcpy(query_str, "[Quit msg]", len); + memset(&query_str[len], 0, 1); + break; + + case MYSQL_COM_QUERY: + len = MYSQL_GET_PACKET_LEN(packet)-1; /*< distract 1 for packet type byte */ + if ((query_str = (char *)malloc(len+1)) == NULL) + { + goto retblock; + } + memcpy(query_str, &packet[5], len); + memset(&query_str[len], 0, 1); + break; + + default: + len = strlen(STRPACKETTYPE(packet_type))+1; + if ((query_str = (char *)malloc(len+1)) == NULL) + { + goto retblock; + } + memcpy(query_str, STRPACKETTYPE(packet_type), len); + memset(&query_str[len], 0, 1); + break; + } /*< switch */ +retblock: + return query_str; +} \ No newline at end of file diff --git a/server/core/monitor.c b/server/core/monitor.c index 85ff878d7..6227afd5d 100644 --- a/server/core/monitor.c +++ b/server/core/monitor.c @@ -207,8 +207,7 @@ MONITOR *ptr; /** * Show a single monitor * - * @param dcb DCB for printing output - * @param monitor The monitor to print information regarding + * @param dcb DCB for printing output */ void monitorShow(DCB *dcb, MONITOR *monitor) @@ -304,12 +303,26 @@ monitorSetInterval (MONITOR *mon, unsigned long interval) * Enable Replication Heartbeat support in monitor. * * @param mon The monitor instance - * @param replication_heartbeat The replication heartbeat + * @param enable The enabling value is 1, 0 turns it off */ void -monitorSetReplicationHeartbeat(MONITOR *mon, int replication_heartbeat) +monitorSetReplicationHeartbeat(MONITOR *mon, int enable) { if (mon->module->replicationHeartbeat != NULL) { - mon->module->replicationHeartbeat(mon->handle, replication_heartbeat); + mon->module->replicationHeartbeat(mon->handle, enable); + } +} + +/** + * Enable Stale Master assignement. + * + * @param mon The monitor instance + * @param enable The enabling value is 1, 0 turns it off + */ +void +monitorDetectStaleMaster(MONITOR *mon, int enable) +{ + if (mon->module->detectStaleMaster != NULL) { + mon->module->detectStaleMaster(mon->handle, enable); } } diff --git a/server/core/poll.c b/server/core/poll.c index 87d3640f0..2df68fd60 100644 --- a/server/core/poll.c +++ b/server/core/poll.c @@ -28,6 +28,8 @@ #include #include #include +#include +#include extern int lm_enabled_logfiles_bitmask; @@ -41,14 +43,63 @@ extern int lm_enabled_logfiles_bitmask; * 19/06/13 Mark Riddoch Initial implementation * 28/06/13 Mark Riddoch Added poll mask support and DCB * zombie management + * 29/08/14 Mark Riddoch Addition of thread status data, load average + * etc. * * @endverbatim */ static int epoll_fd = -1; /*< The epoll file descriptor */ -static int do_shutdown = 0; /*< Flag the shutdown of the poll subsystem */ +static int do_shutdown = 0; /*< Flag the shutdown of the poll subsystem */ static GWBITMASK poll_mask; static simple_mutex_t epoll_wait_mutex; /*< serializes calls to epoll_wait */ +static int n_waiting = 0; /*< No. of threads in epoll_wait */ + +/** + * Thread load average, this is the average number of descriptors in each + * poll completion, a value of 1 or less is the ideal. + */ +static double load_average = 0.0; +static int load_samples = 0; +static int load_nfds = 0; +static double current_avg = 0.0; +static double *avg_samples = NULL; +static int next_sample = 0; +static int n_avg_samples; + +/* Thread statistics data */ +static int n_threads; /*< No. of threads */ + +/** + * Internal MaxScale thread states + */ +typedef enum { THREAD_STOPPED, THREAD_IDLE, + THREAD_POLLING, THREAD_PROCESSING, + THREAD_ZPROCESSING } THREAD_STATE; + +/** + * Thread data used to report the current state and activity related to + * a thread + */ +typedef struct { + THREAD_STATE state; /*< Current thread state */ + int n_fds; /*< No. of descriptors thread is processing */ + DCB *cur_dcb; /*< Current DCB being processed */ + uint32_t event; /*< Current event being processed */ +} THREAD_DATA; + +static THREAD_DATA *thread_data = NULL; /*< Status of each thread */ + +/** + * The number of buckets used to gather statistics about how many + * descriptors where processed on each epoll completion. + * + * An array of wakeup counts is created, with the number of descriptors used + * to index that array. Each time a completion occurs the n_fds - 1 value is + * used to index this array and increment the count held there. + * If n_fds - 1 >= MAXFDS then the count at MAXFDS -1 is incremented. + */ +#define MAXNFDS 10 /** * The polling statistics @@ -60,8 +111,20 @@ static struct { int n_hup; /*< Number of hangup events */ int n_accept; /*< Number of accept events */ int n_polls; /*< Number of poll cycles */ + int n_nothreads; /*< Number of times no threads are polling */ + int n_fds[MAXNFDS]; /*< Number of wakeups with particular + n_fds value */ } pollStats; +/** + * How frequently to call the poll_loadav function used to monitor the load + * average of the poll subsystem. + */ +#define POLL_LOAD_FREQ 10 +/** + * Periodic function to collect load data for average calculations + */ +static void poll_loadav(void *); /** * Initialise the polling system we are using for the gateway. @@ -71,6 +134,8 @@ static struct { void poll_init() { +int i; + if (epoll_fd != -1) return; if ((epoll_fd = epoll_create(MAX_EVENTS)) == -1) @@ -80,7 +145,23 @@ poll_init() } memset(&pollStats, 0, sizeof(pollStats)); bitmask_init(&poll_mask); + n_threads = config_threadcount(); + if ((thread_data = + (THREAD_DATA *)malloc(n_threads * sizeof(THREAD_DATA))) != NULL) + { + for (i = 0; i < n_threads; i++) + { + thread_data[i].state = THREAD_STOPPED; + } + } simple_mutex_init(&epoll_wait_mutex, "epoll_wait_mutex"); + + hktask_add("Load Average", poll_loadav, NULL, POLL_LOAD_FREQ); + n_avg_samples = 15 * 60 / POLL_LOAD_FREQ; + avg_samples = (double *)malloc(sizeof(double *) * n_avg_samples); + for (i = 0; i < n_avg_samples; i++) + avg_samples[i] = 0.0; + } /** @@ -100,7 +181,7 @@ poll_add_dcb(DCB *dcb) CHK_DCB(dcb); - ev.events = EPOLLIN | EPOLLOUT | EPOLLET; + ev.events = EPOLLIN | EPOLLOUT | EPOLLRDHUP | EPOLLHUP | EPOLLET; ev.data.ptr = dcb; /*< @@ -245,20 +326,29 @@ return_rc: void poll_waitevents(void *arg) { - struct epoll_event events[MAX_EVENTS]; - int i, nfds; - int thread_id = (int)arg; - bool no_op = false; - static bool process_zombies_only = false; /*< flag for all threads */ - DCB *zombies = NULL; +struct epoll_event events[MAX_EVENTS]; +int i, nfds; +int thread_id = (int)arg; +bool no_op = false; +static bool process_zombies_only = false; /*< flag for all threads */ +DCB *zombies = NULL; - /* Add this thread to the bitmask of running polling threads */ + /** Add this thread to the bitmask of running polling threads */ bitmask_set(&poll_mask, thread_id); + if (thread_data) + { + thread_data[thread_id].state = THREAD_IDLE; + } + /** Init mysql thread context for use with a mysql handle and a parser */ + mysql_thread_init(); + while (1) { + atomic_add(&n_waiting, 1); #if BLOCKINGPOLL nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); + atomic_add(&n_waiting, -1); #else /* BLOCKINGPOLL */ if (!no_op) { LOGIF(LD, (skygw_log_write( @@ -272,9 +362,14 @@ poll_waitevents(void *arg) #if 0 simple_mutex_lock(&epoll_wait_mutex, TRUE); #endif + if (thread_data) + { + thread_data[thread_id].state = THREAD_POLLING; + } if ((nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, 0)) == -1) { + atomic_add(&n_waiting, -1); int eno = errno; errno = 0; LOGIF(LD, (skygw_log_write( @@ -288,6 +383,7 @@ poll_waitevents(void *arg) } else if (nfds == 0) { + atomic_add(&n_waiting, -1); if (process_zombies_only) { #if 0 simple_mutex_unlock(&epoll_wait_mutex); @@ -310,6 +406,13 @@ poll_waitevents(void *arg) } } } + else + { + atomic_add(&n_waiting, -1); + } + + if (n_waiting == 0) + atomic_add(&pollStats.n_nothreads, 1); #if 0 simple_mutex_unlock(&epoll_wait_mutex); #endif @@ -322,6 +425,20 @@ poll_waitevents(void *arg) pthread_self(), nfds))); atomic_add(&pollStats.n_polls, 1); + if (thread_data) + { + thread_data[thread_id].n_fds = nfds; + thread_data[thread_id].cur_dcb = NULL; + thread_data[thread_id].event = 0; + thread_data[thread_id].state = THREAD_PROCESSING; + } + + pollStats.n_fds[(nfds < MAXNFDS ? (nfds - 1) : MAXNFDS - 1)]++; + + load_average = (load_average * load_samples + nfds) + / (load_samples + 1); + atomic_add(&load_samples, 1); + atomic_add(&load_nfds, nfds); for (i = 0; i < nfds; i++) { @@ -329,6 +446,11 @@ poll_waitevents(void *arg) __uint32_t ev = events[i].events; CHK_DCB(dcb); + if (thread_data) + { + thread_data[thread_id].cur_dcb = dcb; + thread_data[thread_id].event = ev; + } #if defined(SS_DEBUG) if (dcb_fake_write_ev[dcb->fd] != 0) { @@ -364,6 +486,7 @@ poll_waitevents(void *arg) eno = gw_getsockerrno(dcb->fd); if (eno == 0) { +#if MUTEX_BLOCK simple_mutex_lock( &dcb->dcb_write_lock, true); @@ -378,6 +501,11 @@ poll_waitevents(void *arg) dcb->dcb_write_active = FALSE; simple_mutex_unlock( &dcb->dcb_write_lock); +#else + atomic_add(&pollStats.n_write, + 1); + dcb_pollout(dcb, thread_id); +#endif } else { LOGIF(LD, (skygw_log_write( LOGFILE_DEBUG, @@ -393,11 +521,13 @@ poll_waitevents(void *arg) } if (ev & EPOLLIN) { +#if MUTEX_BLOCK simple_mutex_lock(&dcb->dcb_read_lock, true); ss_info_dassert(!dcb->dcb_read_active, "Read already active"); dcb->dcb_read_active = TRUE; +#endif if (dcb->state == DCB_STATE_LISTENING) { @@ -421,11 +551,17 @@ poll_waitevents(void *arg) dcb, dcb->fd))); atomic_add(&pollStats.n_read, 1); +#if MUTEX_BLOCK dcb->func.read(dcb); +#else + dcb_pollin(dcb, thread_id); +#endif } +#if MUTEX_BLOCK dcb->dcb_read_active = FALSE; simple_mutex_unlock( &dcb->dcb_read_lock); +#endif } if (ev & EPOLLERR) { @@ -475,10 +611,33 @@ poll_waitevents(void *arg) atomic_add(&pollStats.n_hup, 1); dcb->func.hangup(dcb); } + + if (ev & EPOLLRDHUP) + { + int eno = 0; + eno = gw_getsockerrno(dcb->fd); + + LOGIF(LD, (skygw_log_write( + LOGFILE_DEBUG, + "%lu [poll_waitevents] " + "EPOLLRDHUP on dcb %p, fd %d. " + "Errno %d, %s.", + pthread_self(), + dcb, + dcb->fd, + eno, + strerror(eno)))); + atomic_add(&pollStats.n_hup, 1); + dcb->func.hangup(dcb); + } } /*< for */ no_op = FALSE; } process_zombies: + if (thread_data) + { + thread_data[thread_id].state = THREAD_ZPROCESSING; + } zombies = dcb_process_zombies(thread_id); if (zombies == NULL) { @@ -491,10 +650,20 @@ poll_waitevents(void *arg) * Remove the thread from the bitmask of running * polling threads. */ + if (thread_data) + { + thread_data[thread_id].state = THREAD_STOPPED; + } bitmask_clear(&poll_mask, thread_id); return; } + if (thread_data) + { + thread_data[thread_id].state = THREAD_IDLE; + } } /*< while(1) */ + /** Release mysql thread context */ + mysql_thread_end(); } /** @@ -525,10 +694,194 @@ poll_bitmask() void dprintPollStats(DCB *dcb) { - dcb_printf(dcb, "Number of epoll cycles: %d\n", pollStats.n_polls); - dcb_printf(dcb, "Number of read events: %d\n", pollStats.n_read); - dcb_printf(dcb, "Number of write events: %d\n", pollStats.n_write); - dcb_printf(dcb, "Number of error events: %d\n", pollStats.n_error); - dcb_printf(dcb, "Number of hangup events: %d\n", pollStats.n_hup); - dcb_printf(dcb, "Number of accept events: %d\n", pollStats.n_accept); +int i; + + dcb_printf(dcb, "Number of epoll cycles: %d\n", + pollStats.n_polls); + dcb_printf(dcb, "Number of read events: %d\n", + pollStats.n_read); + dcb_printf(dcb, "Number of write events: %d\n", + pollStats.n_write); + dcb_printf(dcb, "Number of error events: %d\n", + pollStats.n_error); + dcb_printf(dcb, "Number of hangup events: %d\n", + pollStats.n_hup); + dcb_printf(dcb, "Number of accept events: %d\n", + pollStats.n_accept); + dcb_printf(dcb, "Number of times no threads polling: %d\n", + pollStats.n_nothreads); + + dcb_printf(dcb, "No of poll completions with descriptors\n"); + dcb_printf(dcb, "\tNo. of descriptors\tNo. of poll completions.\n"); + for (i = 0; i < MAXNFDS - 1; i++) + { + dcb_printf(dcb, "\t%2d\t\t\t%d\n", i + 1, pollStats.n_fds[i]); + } + dcb_printf(dcb, "\t>= %d\t\t\t%d\n", MAXNFDS, + pollStats.n_fds[MAXNFDS-1]); +} + +/** + * Convert an EPOLL event mask into a printable string + * + * @param event The event mask + * @return A string representation, the caller must free the string + */ +static char * +event_to_string(uint32_t event) +{ +char *str; + + str = malloc(22); // 22 is max returned string length + if (str == NULL) + return NULL; + *str = 0; + if (event & EPOLLIN) + { + strcat(str, "IN"); + } + if (event & EPOLLOUT) + { + if (*str) + strcat(str, "|"); + strcat(str, "OUT"); + } + if (event & EPOLLERR) + { + if (*str) + strcat(str, "|"); + strcat(str, "ERR"); + } + if (event & EPOLLHUP) + { + if (*str) + strcat(str, "|"); + strcat(str, "HUP"); + } + if (event & EPOLLRDHUP) + { + if (*str) + strcat(str, "|"); + strcat(str, "RDHUP"); + } + + return str; +} + +/** + * Print the thread status for all the polling threads + * + * @param dcb The DCB to send the thread status data + */ +void +dShowThreads(DCB *dcb) +{ +int i, j, n; +char *state; +double avg1 = 0.0, avg5 = 0.0, avg15 = 0.0; + + + dcb_printf(dcb, "Polling Threads.\n\n"); + dcb_printf(dcb, "Historic Thread Load Average: %.2f.\n", load_average); + dcb_printf(dcb, "Current Thread Load Average: %.2f.\n", current_avg); + + /* Average all the samples to get the 15 minute average */ + for (i = 0; i < n_avg_samples; i++) + avg15 += avg_samples[i]; + avg15 = avg15 / n_avg_samples; + + /* Average the last third of the samples to get the 5 minute average */ + n = 5 * 60 / POLL_LOAD_FREQ; + i = next_sample - (n + 1); + if (i < 0) + i += n_avg_samples; + for (j = i; j < i + n; j++) + avg5 += avg_samples[j % n_avg_samples]; + avg5 = (3 * avg5) / (n_avg_samples); + + /* Average the last 15th of the samples to get the 1 minute average */ + n = 60 / POLL_LOAD_FREQ; + i = next_sample - (n + 1); + if (i < 0) + i += n_avg_samples; + for (j = i; j < i + n; j++) + avg1 += avg_samples[j % n_avg_samples]; + avg1 = (15 * avg1) / (n_avg_samples); + + dcb_printf(dcb, "15 Minute Average: %.2f, 5 Minute Average: %.2f, " + "1 Minute Average: %.2f\n\n", avg15, avg5, avg1); + + if (thread_data == NULL) + return; + dcb_printf(dcb, " ID | State | # fds | Descriptor | Event\n"); + dcb_printf(dcb, "----+------------+--------+------------------+---------------\n"); + for (i = 0; i < n_threads; i++) + { + switch (thread_data[i].state) + { + case THREAD_STOPPED: + state = "Stopped"; + break; + case THREAD_IDLE: + state = "Idle"; + break; + case THREAD_POLLING: + state = "Polling"; + break; + case THREAD_PROCESSING: + state = "Processing"; + break; + case THREAD_ZPROCESSING: + state = "Collecting"; + break; + } + if (thread_data[i].state != THREAD_PROCESSING) + dcb_printf(dcb, + " %2d | %-10s | | |\n", + i, state); + else if (thread_data[i].cur_dcb == NULL) + dcb_printf(dcb, + " %2d | %-10s | %6d | |\n", + i, state, thread_data[i].n_fds); + else + { + char *event_string + = event_to_string(thread_data[i].event); + if (event_string == NULL) + event_string = "??"; + dcb_printf(dcb, + " %2d | %-10s | %6d | %-16p | %s\n", + i, state, thread_data[i].n_fds, + thread_data[i].cur_dcb, event_string); + free(event_string); + } + } +} + +/** + * The function used to calculate time based load data. This is called by the + * housekeeper every POLL_LOAD_FREQ seconds. + * + * @param data Argument required by the housekeeper but not used here + */ +static void +poll_loadav(void *data) +{ +static int last_samples = 0, last_nfds = 0; +int new_samples, new_nfds; + + new_samples = load_samples - last_samples; + new_nfds = load_nfds - last_nfds; + last_samples = load_samples; + last_nfds = load_nfds; + + /* POLL_LOAD_FREQ average is... */ + if (new_samples) + current_avg = new_nfds / new_samples; + else + current_avg = 0.0; + avg_samples[next_sample] = current_avg; + next_sample++; + if (next_sample >= n_avg_samples) + next_sample = 0; } diff --git a/server/core/server.c b/server/core/server.c index 5a75756ed..0fe018d7a 100644 --- a/server/core/server.c +++ b/server/core/server.c @@ -30,6 +30,7 @@ * 28/05/14 Massimiliano Pinto Addition of rlagd and node_ts fields * 20/06/14 Massimiliano Pinto Addition of master_id, depth, slaves fields * 26/06/14 Mark Riddoch Addition of server parameters + * 30/08/14 Massimiliano Pinto Addition of new service status description * * @endverbatim */ @@ -148,7 +149,8 @@ server_set_unique_name(SERVER *server, char *name) * Find an existing server using the unique section name in * configuration file * - * @param name The Server name defined in the header file + * @param servname The Server name or address + * @param port The server port * @return The server or NULL if not found */ SERVER * @@ -373,23 +375,23 @@ char *stat; if (ptr) { dcb_printf(dcb, "Servers.\n"); - dcb_printf(dcb, "-------------------+-----------------+-------+----------------------+------------\n"); - dcb_printf(dcb, "%-18s | %-15s | Port | %-20s | Connections\n", + dcb_printf(dcb, "-------------------+-----------------+-------+-------------+--------------------\n"); + dcb_printf(dcb, "%-18s | %-15s | Port | Connections | %-20s", "Server", "Address", "Status"); - dcb_printf(dcb, "-------------------+-----------------+-------+----------------------+------------\n"); + dcb_printf(dcb, "-------------------+-----------------+-------+-------------+--------------------\n"); } while (ptr) { stat = server_status(ptr); - dcb_printf(dcb, "%-18s | %-15s | %5d | %-20s | %4d\n", + dcb_printf(dcb, "%-18s | %-15s | %5d | %11d | %s\n", ptr->unique_name, ptr->name, - ptr->port, stat, - ptr->stats.n_current); + ptr->port, + ptr->stats.n_current, stat); free(stat); ptr = ptr->next; } if (allServers) - dcb_printf(dcb, "-------------------+-----------------+-------+----------------------+------------\n\n"); + dcb_printf(dcb, "-------------------+-----------------+-------+-------------+--------------------\n"); spinlock_release(&server_spin); } @@ -405,7 +407,7 @@ server_status(SERVER *server) { char *status = NULL; - if ((status = (char *)malloc(200)) == NULL) + if ((status = (char *)malloc(256)) == NULL) return NULL; status[0] = 0; if (server->status & SERVER_MAINT) @@ -416,6 +418,14 @@ char *status = NULL; strcat(status, "Slave, "); if (server->status & SERVER_JOINED) strcat(status, "Synced, "); + if (server->status & SERVER_NDB) + strcat(status, "NDB, "); + if (server->status & SERVER_SLAVE_OF_EXTERNAL_MASTER) + strcat(status, "Slave of External Server, "); + if (server->status & SERVER_STALE_STATUS) + strcat(status, "Stale Status, "); + if (server->status & SERVER_AUTH_ERROR) + strcat(status, "Auth Error, "); if (server->status & SERVER_RUNNING) strcat(status, "Running"); else diff --git a/server/core/service.c b/server/core/service.c index 4c98a5a5c..e64d25038 100644 --- a/server/core/service.c +++ b/server/core/service.c @@ -56,9 +56,29 @@ extern int lm_enabled_logfiles_bitmask; +/** To be used with configuration type checks */ +typedef struct typelib_st { + int tl_nelems; + const char* tl_name; + const char** tl_p_elems; +} typelib_t; +/** Set of subsequent false,true pairs */ +static const char* bool_strings[11] = {"FALSE", "TRUE", "OFF", "ON", "N", "Y", "0", "1", "NO", "YES", 0}; +typelib_t bool_type = {array_nelems(bool_strings)-1, "bool_type", bool_strings}; + +/** List of valid values */ +static const char* sqlvar_target_strings[4] = {"MASTER", "ALL", 0}; +typelib_t sqlvar_target_type = { + array_nelems(sqlvar_target_strings)-1, + "sqlvar_target_type", + sqlvar_target_strings +}; + static SPINLOCK service_spin = SPINLOCK_INIT; static SERVICE *allServices = NULL; +static int find_type(typelib_t* tl, const char* needle, int maxlen); + static void service_add_qualified_param( SERVICE* svc, CONFIG_PARAMETER* param); @@ -677,6 +697,7 @@ int n = 0; "Unable to find filter '%s' for service '%s'\n", trim(ptr), service->name ))); + n--; } flist[n] = NULL; ptr = strtok_r(NULL, "|", &brkt); @@ -1008,78 +1029,174 @@ bool service_set_param_value ( count_spec_t count_spec, config_param_type_t type) { - char* p; - int valint; - bool succp = true; - - /** - * Find out whether the value is numeric and ends with '%' or '\0' - */ - p = valstr; - - while(isdigit(*p)) p++; - - errno = 0; - - if (p == valstr || (*p != '%' && *p != '\0')) - { - succp = false; - } - else if (*p == '%') - { - if (*(p+1) == '\0') - { - *p = '\0'; - valint = (int) strtol(valstr, (char **)NULL, 10); - - if (valint == 0 && errno != 0) - { - succp = false; - } - else if (PARAM_IS_TYPE(type,PERCENT_TYPE)) - { - succp = true; - config_set_qualified_param(param, (void *)&valint, PERCENT_TYPE); - } - else - { - /** Log error */ - } - } - else - { - succp = false; - } - } - else if (*p == '\0') - { - valint = (int) strtol(valstr, (char **)NULL, 10); + char* p; + int valint; + bool valbool; + target_t valtarget; + bool succp = true; - if (valint == 0 && errno != 0) - { - succp = false; - } - else if (PARAM_IS_TYPE(type,COUNT_TYPE)) - { - succp = true; - config_set_qualified_param(param, (void *)&valint, COUNT_TYPE); - } - else - { - /** Log error */ - } - } - + if (PARAM_IS_TYPE(type,PERCENT_TYPE) ||PARAM_IS_TYPE(type,COUNT_TYPE)) + { + /** + * Find out whether the value is numeric and ends with '%' or '\0' + */ + p = valstr; + + while(isdigit(*p)) p++; + + errno = 0; + + if (p == valstr || (*p != '%' && *p != '\0')) + { + succp = false; + } + else if (*p == '%') + { + if (*(p+1) == '\0') + { + *p = '\0'; + valint = (int) strtol(valstr, (char **)NULL, 10); + + if (valint == 0 && errno != 0) + { + succp = false; + } + else if (PARAM_IS_TYPE(type,PERCENT_TYPE)) + { + succp = true; + config_set_qualified_param(param, (void *)&valint, PERCENT_TYPE); + } + else + { + /** Log error */ + } + } + else + { + succp = false; + } + } + else if (*p == '\0') + { + valint = (int) strtol(valstr, (char **)NULL, 10); + + if (valint == 0 && errno != 0) + { + succp = false; + } + else if (PARAM_IS_TYPE(type,COUNT_TYPE)) + { + succp = true; + config_set_qualified_param(param, (void *)&valint, COUNT_TYPE); + } + else + { + /** Log error */ + } + } + } + else if (type == BOOL_TYPE) + { + unsigned int rc; + + rc = find_type(&bool_type, valstr, strlen(valstr)+1); + + if (rc > 0) + { + succp = true; + if (rc%2 == 1) + { + valbool = false; + } + else if (rc%2 == 0) + { + valbool = true; + } + /** add param to config */ + config_set_qualified_param(param, + (void *)&valbool, + BOOL_TYPE); + } + else + { + succp = false; + } + } + else if (type == SQLVAR_TARGET_TYPE) + { + unsigned int rc; + + rc = find_type(&sqlvar_target_type, valstr, strlen(valstr)+1); + + if (rc > 0 && rc < 3) + { + succp = true; + if (rc == 1) + { + valtarget = TYPE_MASTER; + } + else if (rc == 2) + { + valtarget = TYPE_ALL; + } + /** add param to config */ + config_set_qualified_param(param, + (void *)&valtarget, + SQLVAR_TARGET_TYPE); + } + else + { + succp = false; + } + } + if (succp) { - service_add_qualified_param(service, param); /*< add param to svc */ + service_add_qualified_param(service, param); /*< add param to svc */ } return succp; } +/* + * Function to find a string in typelib_t + * (similar to find_type() of mysys/typelib.c) + * + * SYNOPSIS + * find_type() + * lib typelib_t + * find String to find + * length Length of string to find + * part_match Allow part matching of value + * + * RETURN + * 0 error + * > 0 position in TYPELIB->type_names +1 + */ + +static int find_type( + typelib_t* tl, + const char* needle, + int maxlen) +{ + int i; + + if (tl == NULL || needle == NULL || maxlen <= 0) + { + return -1; + } + + for (i=0; itl_nelems; i++) + { + if (strncasecmp(tl->tl_p_elems[i], needle, maxlen) == 0) + { + return i+1; + } + } + return 0; +} /** * Add qualified config parameter to SERVICE struct. - */ + */ static void service_add_qualified_param( SERVICE* svc, CONFIG_PARAMETER* param) diff --git a/server/core/session.c b/server/core/session.c index db59f9e6e..a67926b48 100644 --- a/server/core/session.c +++ b/server/core/session.c @@ -694,6 +694,7 @@ int i; return 0; } session->tail = *tail; + free(tail); } return 1; diff --git a/server/core/spinlock.c b/server/core/spinlock.c index c859f726e..7b35163f3 100644 --- a/server/core/spinlock.c +++ b/server/core/spinlock.c @@ -40,9 +40,12 @@ void spinlock_init(SPINLOCK *lock) { lock->lock = 0; -#ifdef DEBUG +#ifdef SPINLOCK_PROFILE lock->spins = 0; lock->acquired = 0; + lock->waiting = 0; + lock->max_waiting = 0; + lock->contended = 0; #endif } @@ -54,16 +57,29 @@ spinlock_init(SPINLOCK *lock) void spinlock_acquire(SPINLOCK *lock) { +#ifdef SPINLOCK_PROFILE +int spins = 0; + + atomic_add(&(lock->waiting), 1); +#endif while (atomic_add(&(lock->lock), 1) != 0) { atomic_add(&(lock->lock), -1); -#ifdef DEBUG +#ifdef SPINLOCK_PROFILE atomic_add(&(lock->spins), 1); + spins++; #endif } -#ifdef DEBUG +#ifdef SPINLOCK_PROFILE + if (spins) + { + lock->contended++; + if (lock->maxspins < spins) + lock->maxspins = spins; + } lock->acquired++; lock->owner = THREAD_SHELF(); + atomic_add(&(lock->waiting), -1); #endif } @@ -71,7 +87,7 @@ spinlock_acquire(SPINLOCK *lock) * Acquire a spinlock if it is not already locked. * * @param lock The spinlock to acquire - * @return True ifthe spinlock was acquired, otherwise false + * @return True if the spinlock was acquired, otherwise false */ int spinlock_acquire_nowait(SPINLOCK *lock) @@ -81,7 +97,7 @@ spinlock_acquire_nowait(SPINLOCK *lock) atomic_add(&(lock->lock), -1); return FALSE; } -#ifdef DEBUG +#ifdef SPINLOCK_PROFILE lock->acquired++; lock->owner = THREAD_SHELF(); #endif @@ -96,5 +112,45 @@ spinlock_acquire_nowait(SPINLOCK *lock) void spinlock_release(SPINLOCK *lock) { +#ifdef SPINLOCK_PROFILE + if (lock->waiting > lock->max_waiting) + lock->max_waiting = lock->waiting; +#endif atomic_add(&(lock->lock), -1); } + +/** + * Report statistics on a spinlock. This only has an effect if the + * spinlock code has been compiled with the SPINLOCK_PROFILE option set. + * + * NB A callback function is used to return the data rather than + * merely printing to a DCB in order to avoid a dependency on the DCB + * form the spinlock code and also to facilitate other uses of the + * statistics reporting. + * + * @param lock The spinlock to report on + * @param reporter The callback function to pass the statistics to + * @param hdl A handle that is passed to the reporter function + */ +void +spinlock_stats(SPINLOCK *lock, void (*reporter)(void *, char *, int), void *hdl) +{ +#ifdef SPINLOCK_PROFILE + reporter(hdl, "Spinlock acquired", lock->acquired); + if (lock->acquired) + { + reporter(hdl, "Total no. of spins", lock->spins); + reporter(hdl, "Average no. of spins (overall)", + lock->spins / lock->acquired); + if (lock->contended) + reporter(hdl, "Average no. of spins (when contended)", + lock->spins / lock->contended); + reporter(hdl, "Maximum no. of spins", lock->maxspins); + reporter(hdl, "Maximim no. of blocked threads", + lock->max_waiting); + reporter(hdl, "Contended locks", lock->contended); + reporter(hdl, "Contention percentage", + (lock->contended * 100) / lock->acquired); + } +#endif +} diff --git a/server/core/test/makefile b/server/core/test/makefile index 7f6831082..14f2828f2 100644 --- a/server/core/test/makefile +++ b/server/core/test/makefile @@ -13,13 +13,13 @@ TESTLOG := $(shell pwd)/testcore.log LOGPATH := $(ROOT_PATH)/log_manager UTILSPATH := $(ROOT_PATH)/utils -LDFLAGS=-rdynamic -L$(LOGPATH) \ +LDFLAGS=-rdynamic -L$(LOGPATH) -L$(EMBEDDED_LIB) \ -Wl,-rpath,$(DEST)/lib \ -Wl,-rpath,$(LOGPATH) -Wl,-rpath,$(UTILSPATH) \ -Wl,-rpath,$(EMBEDDED_LIB) LIBS= -lz -lm -lcrypt -lcrypto -ldl -laio -lrt -pthread -llog_manager \ - -L../../inih/extra -linih -lssl -lstdc++ + -L../../inih/extra -linih -lssl -lstdc++ -lmysqld TESTS=testhash testspinlock testfilter testadminusers diff --git a/server/core/test/testhash.c b/server/core/test/testhash.c index 6aac2d016..3fa9d7f0d 100644 --- a/server/core/test/testhash.c +++ b/server/core/test/testhash.c @@ -1,19 +1,73 @@ +/* + * This file is distributed as part of MaxScale. It is free + * software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, + * version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright SkySQL Ab 2014 + */ + +/** + * + * @verbatim + * Revision History + * + * Date Who Description + * 18/08-2014 Mark Riddoch Initial implementation + * + * @endverbatim + */ + #include #include #include +#include +#include #include "../../include/hashtable.h" +static void +read_lock(HASHTABLE *table) +{ + spinlock_acquire(&table->spin); + while (table->writelock) + { + spinlock_release(&table->spin); + while (table->writelock) + ; + spinlock_acquire(&table->spin); + } + table->n_readers++; + spinlock_release(&table->spin); +} + +static void +read_unlock(HASHTABLE *table) +{ + atomic_add(&table->n_readers, -1); +} + static int hfun(void* key); static int cmpfun (void *, void *); static int hfun( void* key) { - return *(int *)key; + int *i = (int *)key; + int j = (*i * 23) + 41; + return j; + /* return *(int *)key; */ } - static int cmpfun( void* v1, void* v2) @@ -27,7 +81,19 @@ static int cmpfun( return (i1 < i2 ? -1 : (i1 > i2 ? 1 : 0)); } +static double start; +/** + * test1 spinlock_acquire_nowait tests + * + * Test that spinlock_acquire_nowait returns false if the spinlock + * is already taken. + * + * Test that spinlock_acquire_nowait returns true if the spinlock + * is not taken. + * + * Test that spinlock_acquire_nowait does hold the spinlock. + */ static bool do_hashtest( int argelems, int argsize) @@ -39,12 +105,14 @@ static bool do_hashtest( int* val_arr; int hsize; int longest; + int* iter; ss_dfprintf(stderr, "testhash : creating hash table of size %d, including %d " - "elements in total.", + "elements in total, at time %g.", argsize, - argelems); + argelems, + (double)clock()-start); val_arr = (int *)malloc(sizeof(void *)*argelems); @@ -56,17 +124,33 @@ static bool do_hashtest( val_arr[i] = i; hashtable_add(h, (void *)&val_arr[i], (void *)&val_arr[i]); } - + if (argelems > 1000) ss_dfprintf(stderr, "\t..done\nOperation took %g", (double)clock()-start); + ss_dfprintf(stderr, "\t..done\nRead hash table statistics."); hashtable_get_stats((void *)h, &hsize, &nelems, &longest); ss_dfprintf(stderr, "\t..done\nValidate read values."); - ss_info_dassert(hsize == argsize, "Invalid hash size"); + ss_info_dassert(hsize == (argsize > 0 ? argsize: 1), "Invalid hash size"); ss_info_dassert((nelems == argelems) || (nelems == 0 && argsize == 0), "Invalid element count"); ss_info_dassert(longest <= nelems, "Too large longest list value"); + if (argelems > 1000) ss_dfprintf(stderr, "\t..done\nOperation took %g", (double)clock()-start); + + ss_dfprintf(stderr, "\t..done\nValidate iterator."); + + HASHITERATOR *iterator = hashtable_iterator(h); + read_lock(h); + for (i=0; i < (argelems+1); i++) { + iter = (int *)hashtable_next(iterator); + if (iter == NULL) break; + if (argelems < 100) ss_dfprintf(stderr, "\nNext item, iter = %d, i = %d", *iter, i); + } + read_unlock(h); + ss_info_dassert((i == argelems) || (i == 0 && argsize == 0), "\nIncorrect number of elements from iterator"); + hashtable_iterator_free(iterator); + if (argelems > 1000) ss_dfprintf(stderr, "\t..done\nOperation took %g", (double)clock()-start); ss_dfprintf(stderr, "\t\t..done\n\nTest completed successfully.\n\n"); @@ -91,11 +175,13 @@ return_succp: int main(void) { int rc = 1; + start = (double) clock(); if (!do_hashtest(0, 1)) goto return_rc; if (!do_hashtest(10, 1)) goto return_rc; if (!do_hashtest(1000, 10)) goto return_rc; if (!do_hashtest(10, 0)) goto return_rc; + if (!do_hashtest(10, -5)) goto return_rc; if (!do_hashtest(1500, 17)) goto return_rc; if (!do_hashtest(1, 1)) goto return_rc; if (!do_hashtest(10000, 133)) goto return_rc; diff --git a/server/core/test/testspinlock.c b/server/core/test/testspinlock.c index bcbfa2a3f..ecdbdf108 100644 --- a/server/core/test/testspinlock.c +++ b/server/core/test/testspinlock.c @@ -55,18 +55,18 @@ SPINLOCK lck; spinlock_acquire(&lck); if (spinlock_acquire_nowait(&lck)) { - fprintf(stderr, "spinlock_acquire_nowait: test 1 failed.\n"); + fprintf(stderr, "spinlock_acquire_nowait: test 1.1 failed.\n"); return 1; } spinlock_release(&lck); if (!spinlock_acquire_nowait(&lck)) { - fprintf(stderr, "spinlock_acquire_nowait: test 2 failed.\n"); + fprintf(stderr, "spinlock_acquire_nowait: test 1.2 failed.\n"); return 1; } if (spinlock_acquire_nowait(&lck)) { - fprintf(stderr, "spinlock_acquire_nowait: test 3 failed.\n"); + fprintf(stderr, "spinlock_acquire_nowait: test 1.3 failed.\n"); return 1; } spinlock_release(&lck); @@ -89,6 +89,8 @@ unsigned long t1 = time(0); } /** + * test2 spinlock_acquire tests + * * Check that spinlock correctly blocks another thread whilst the spinlock * is held. * @@ -114,7 +116,7 @@ void *handle; if (acquire_time < 8) { - fprintf(stderr, "spinlock: test 1 failed.\n"); + fprintf(stderr, "spinlock: test 2 failed.\n"); return 1; } return 0; diff --git a/server/include/buffer.h b/server/include/buffer.h index 9729c538c..eefdd431d 100644 --- a/server/include/buffer.h +++ b/server/include/buffer.h @@ -38,12 +38,30 @@ * 10/06/2013 Mark Riddoch Initial implementation * 11/07/2013 Mark Riddoch Addition of reference count in the gwbuf * 16/07/2013 Massimiliano Pinto Added command type for the queue + * 10/07/2014 Mark Riddoch Addition of hints + * 15/07/2014 Mark Riddoch Added buffer properties * * @endverbatim */ +#include #include +#include +#include +EXTERN_C_BLOCK_BEGIN + +/** + * Buffer properties - used to store properties related to the buffer + * contents. This may be added at any point during the processing of the + * data, especially in the protocol stage of the processing. + */ +typedef struct buf_property { + char *name; + char *value; + struct buf_property *next; +} BUF_PROPERTY; + typedef enum { GWBUF_TYPE_UNDEFINED = 0x00, @@ -52,7 +70,8 @@ typedef enum GWBUF_TYPE_SINGLE_STMT = 0x04, GWBUF_TYPE_SESCMD_RESPONSE = 0x08, GWBUF_TYPE_RESPONSE_END = 0x10, - GWBUF_TYPE_SESCMD = 0x20 + GWBUF_TYPE_SESCMD = 0x20, + GWBUF_TYPE_HTTP = 0x40 } gwbuf_type_t; #define GWBUF_IS_TYPE_UNDEFINED(b) (b->gwbuf_type == 0) @@ -73,6 +92,35 @@ typedef struct { int refcount; /*< Reference count on the buffer */ } SHARED_BUF; +typedef enum +{ + GWBUF_INFO_NONE = 0x0, + GWBUF_INFO_PARSED = 0x1 +} gwbuf_info_t; + +#define GWBUF_IS_PARSED(b) (b->gwbuf_info & GWBUF_INFO_PARSED) + +/** + * A structure for cleaning up memory allocations of structures which are + * referred to by GWBUF and deallocated in gwbuf_free but GWBUF doesn't + * know what they are. + * All functions on the list are executed before freeing memory of GWBUF struct. + */ +typedef enum +{ + GWBUF_PARSING_INFO +} bufobj_id_t; + +typedef struct buffer_object_st buffer_object_t; + +struct buffer_object_st { + bufobj_id_t bo_id; + void* bo_data; + void (*bo_donefun_fp)(void *); + buffer_object_t* bo_next; +}; + + /** * The buffer structure used by the descriptor control blocks. * @@ -82,12 +130,17 @@ typedef struct { * be copied within the gateway. */ typedef struct gwbuf { + SPINLOCK gwbuf_lock; struct gwbuf *next; /*< Next buffer in a linked chain of buffers */ + struct gwbuf *tail; /*< Last buffer in a linked chain of buffers */ void *start; /*< Start of the valid data */ void *end; /*< First byte after the valid data */ SHARED_BUF *sbuf; /*< The shared buffer with the real data */ - int command;/*< The command type for the queue */ + buffer_object_t *gwbuf_bufobj; /*< List of objects referred to by GWBUF */ + gwbuf_info_t gwbuf_info; /*< Info bits */ gwbuf_type_t gwbuf_type; /*< buffer's data type information */ + HINT *hint; /*< Hint data for this buffer */ + BUF_PROPERTY *properties; /*< Buffer properties */ } GWBUF; /*< @@ -121,4 +174,18 @@ extern unsigned int gwbuf_length(GWBUF *head); extern GWBUF *gwbuf_clone_portion(GWBUF *head, size_t offset, size_t len); extern GWBUF *gwbuf_clone_transform(GWBUF *head, gwbuf_type_t type); extern void gwbuf_set_type(GWBUF *head, gwbuf_type_t type); +extern int gwbuf_add_property(GWBUF *buf, char *name, char *value); +extern char *gwbuf_get_property(GWBUF *buf, char *name); +extern GWBUF *gwbuf_make_contiguous(GWBUF *); +extern int gwbuf_add_hint(GWBUF *, HINT *); + +void gwbuf_add_buffer_object(GWBUF* buf, + bufobj_id_t id, + void* data, + void (*donefun_fp)(void *)); +void* gwbuf_get_buffer_object_data(GWBUF* buf, bufobj_id_t id); + +EXTERN_C_BLOCK_END + + #endif diff --git a/server/include/config.h b/server/include/config.h index 59cb096e8..659fb6378 100644 --- a/server/include/config.h +++ b/server/include/config.h @@ -39,13 +39,22 @@ enum {MAX_PARAM_LEN=256}; typedef enum { - UNDEFINED_TYPE = 0x00, - STRING_TYPE = 0x01, - COUNT_TYPE = 0x02, - PERCENT_TYPE = 0x04, - BOOL_TYPE = 0x08 + UNDEFINED_TYPE = 0x00, + STRING_TYPE = 0x01, + COUNT_TYPE = 0x02, + PERCENT_TYPE = 0x04, + BOOL_TYPE = 0x08, + SQLVAR_TARGET_TYPE = 0x10 } config_param_type_t; +typedef enum { + TYPE_UNDEFINED = 0, + TYPE_MASTER, + TYPE_ALL +} target_t; + +enum {MAX_RLAG_NOT_AVAILABLE=-1, MAX_RLAG_UNDEFINED=-2}; + #define PARAM_IS_TYPE(p,t) ((p) & (t)) /** @@ -59,6 +68,7 @@ typedef struct config_parameter { int valcount; /*< int */ int valpercent; /*< int */ bool valbool; /*< bool */ + target_t valtarget; /*< sql variable route target */ } qfd; config_param_type_t qfd_param_type; struct config_parameter *next; /**< Next pointer in the linked list */ @@ -97,9 +107,21 @@ bool config_set_qualified_param( config_param_type_t type); -int config_get_valint( +bool config_get_valint( + int* val, CONFIG_PARAMETER* param, const char* name, /*< if NULL examine current param only */ config_param_type_t ptype); +bool config_get_valbool( + bool* val, + CONFIG_PARAMETER* param, + const char* name, /*< if NULL examine current param only */ + config_param_type_t ptype); + +bool config_get_valtarget( + target_t* val, + CONFIG_PARAMETER* param, + const char* name, /*< if NULL examine current param only */ + config_param_type_t ptype); #endif diff --git a/server/include/dcb.h b/server/include/dcb.h index 4f8be99d4..723acec5d 100644 --- a/server/include/dcb.h +++ b/server/include/dcb.h @@ -53,6 +53,7 @@ struct service; * 07/02/2014 Massimiliano Pinto Added ipv4 data struct into for dcb * 07/05/2014 Mark Riddoch Addition of callback mechanism * 08/05/2014 Mark Riddoch Addition of writeq high and low watermarks + * 27/08/2014 Mark Ridddoch Addition of write event queuing * * @endverbatim */ @@ -107,12 +108,16 @@ typedef struct gw_protocol { * The statitics gathered on a descriptor control block */ typedef struct dcbstats { - int n_reads; /*< Number of reads on this descriptor */ - int n_writes; /*< Number of writes on this descriptor */ - int n_accepts; /*< Number of accepts on this descriptor */ - int n_buffered; /*< Number of buffered writes */ - int n_high_water; /*< Number of crosses of high water mark */ - int n_low_water; /*< Number of crosses of low water mark */ + int n_reads; /*< Number of reads on this descriptor */ + int n_writes; /*< Number of writes on this descriptor */ + int n_accepts; /*< Number of accepts on this descriptor */ + int n_buffered; /*< Number of buffered writes */ + int n_high_water; /*< Number of crosses of high water mark */ + int n_low_water; /*< Number of crosses of low water mark */ + int n_busypolls; /*< Number of read polls whiel reading */ + int n_readrechecks; /*< Number of rechecks for reads */ + int n_busywrpolls; /*< Number of write polls while writing */ + int n_writerechecks;/*< Number of rechecks for writes */ } DCBSTATS; /** @@ -231,6 +236,13 @@ typedef struct dcb { DCBMM memdata; /**< The data related to DCB memory management */ SPINLOCK cb_lock; /**< The lock for the callbacks linked list */ DCB_CALLBACK *callbacks; /**< The list of callbacks for the DCB */ + SPINLOCK pollinlock; + int pollinbusy; + int readcheck; + + SPINLOCK polloutlock; + int polloutbusy; + int writecheck; unsigned int high_water; /**< High water mark */ unsigned int low_water; /**< Low water mark */ @@ -259,6 +271,8 @@ int fail_accept_errno; #define DCB_BELOW_LOW_WATER(x) ((x)->low_water && (x)->writeqlen < (x)->low_water) #define DCB_ABOVE_HIGH_WATER(x) ((x)->high_water && (x)->writeqlen > (x)->high_water) +void dcb_pollin(DCB *, int); +void dcb_pollout(DCB *, int); DCB *dcb_get_zombies(void); int gw_write( #if defined(SS_DEBUG) @@ -289,7 +303,7 @@ void dcb_hashtable_stats(DCB *, void *); /**< Print statisitics */ void dcb_add_to_zombieslist(DCB* dcb); int dcb_add_callback(DCB *, DCB_REASON, int (*)(struct dcb *, DCB_REASON, void *), void *); -int dcb_remove_callback(DCB *, DCB_REASON, int (*)(struct dcb *, DCB_REASON), +int dcb_remove_callback(DCB *, DCB_REASON, int (*)(struct dcb *, DCB_REASON, void *), void *); int dcb_isvalid(DCB *); /* Check the DCB is in the linked list */ diff --git a/server/include/hashtable.h b/server/include/hashtable.h index 92bc71b8e..175bd5d32 100644 --- a/server/include/hashtable.h +++ b/server/include/hashtable.h @@ -84,12 +84,17 @@ typedef struct hashtable { SPINLOCK spin; /**< Internal spinlock for the hashtable */ int n_readers; /**< Number of clients reading the table */ int writelock; /**< The table is locked by a writer */ + bool ht_isflat; /**< Indicates whether hashtable is in stack or heap */ #if defined(SS_DEBUG) skygw_chk_t ht_chk_tail; #endif } HASHTABLE; extern HASHTABLE *hashtable_alloc(int, int (*hashfn)(), int (*cmpfn)()); +HASHTABLE *hashtable_alloc_flat(HASHTABLE* target, + int size, + int (*hashfn)(), + int (*cmpfn)()); /**< Allocate a hashtable */ extern void hashtable_memory_fns(HASHTABLE *, HASHMEMORYFN, HASHMEMORYFN, HASHMEMORYFN, HASHMEMORYFN); /**< Provide an interface to control key/value memory diff --git a/server/include/hint.h b/server/include/hint.h new file mode 100644 index 000000000..03c319142 --- /dev/null +++ b/server/include/hint.h @@ -0,0 +1,69 @@ +#ifndef _HINT_H +#define _HINT_H +/* + * This file is distributed as part of MaxScale. It is free + * software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, + * version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright SkySQL Ab 2014 + */ + +/** + * @file hint.h The generic hint data that may be attached to buffers + * + * @verbatim + * Revision History + * + * Date Who Description + * 10/07/14 Mark Riddoch Initial implementation + * + * @endverbatim + */ + +#include + + +/** + * The types of hint that are supported by the generic hinting mechanism. + */ +typedef enum { + HINT_ROUTE_TO_MASTER = 1, + HINT_ROUTE_TO_SLAVE, + HINT_ROUTE_TO_NAMED_SERVER, + HINT_ROUTE_TO_UPTODATE_SERVER, + HINT_ROUTE_TO_ALL, /*< not implemented yet */ + HINT_PARAMETER +} HINT_TYPE; + +/** + * A generic hint. + * + * A hint has a type associated with it and may optionally have hint + * specific data. + * Multiple hints may be attached to a single buffer. + */ +typedef struct hint { + HINT_TYPE type; /*< The Type of hint */ + void *data; /*< Type specific data */ + void *value; /*< Parameter value for hint */ + unsigned int dsize; /*< Size of the hint data */ + struct hint *next; /*< Another hint for this buffer */ +} HINT; + +extern HINT *hint_alloc(HINT_TYPE, void *, unsigned int); +extern HINT *hint_create_parameter(HINT *, char *, char *); +extern HINT *hint_create_route(HINT *, HINT_TYPE, char *); +extern void hint_free(HINT *); +extern HINT *hint_dup(HINT *); +bool hint_exists(HINT **, HINT_TYPE); +#endif diff --git a/server/include/housekeeper.h b/server/include/housekeeper.h new file mode 100644 index 000000000..597f19a91 --- /dev/null +++ b/server/include/housekeeper.h @@ -0,0 +1,50 @@ +#ifndef _HOUSEKEEPER_H +#define _HOUSEKEEPER_H +/* + * This file is distributed as part of the SkySQL Gateway. It is free + * software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, + * version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright SkySQL Ab 2014 + */ +#include + +/** + * @file housekeeper.h A mechanism to have task run periodically + * + * @verbatim + * Revision History + * + * Date Who Description + * 29/08/14 Mark Riddoch Initial implementation + * + * @endverbatim + */ + +/** + * The housekeeper task list + */ +typedef struct hktask { + char *name; /*< A simple task name */ + void (*task)(void *data); /*< The task to call */ + void *data; /*< Data to pass the task */ + int frequency; /*< How often to call the tasks (seconds) */ + time_t nextdue; /*< When the task should be next run */ + struct hktask + *next; /*< Next task in the list */ +} HKTASK; + +extern void hkinit(); +extern int hktask_add(char *name, void (*task)(void *), void *data, int frequency); +extern int hktask_remove(char *name); +#endif diff --git a/server/include/modutil.h b/server/include/modutil.h index 00336f937..a0624752a 100644 --- a/server/include/modutil.h +++ b/server/include/modutil.h @@ -36,4 +36,6 @@ extern int modutil_is_SQL(GWBUF *); extern int modutil_extract_SQL(GWBUF *, char **, int *); extern int modutil_MySQL_Query(GWBUF *, char **, int *, int *); extern GWBUF *modutil_replace_SQL(GWBUF *, char *); +char* modutil_get_query(GWBUF* buf); + #endif diff --git a/server/include/monitor.h b/server/include/monitor.h index d65fd075f..5d018c16f 100644 --- a/server/include/monitor.h +++ b/server/include/monitor.h @@ -31,6 +31,8 @@ * 25/07/13 Mark Riddoch Addition of diagnotics * 23/05/14 Mark Riddoch Addition of routine to find monitors by name * 23/05/14 Massimiliano Pinto Addition of defaultId and setInterval + * 23/06/14 Massimiliano Pinto Addition of replicationHeartbeat + * 28/08/14 Massimiliano Pinto Addition of detectStaleMaster * * @endverbatim */ @@ -70,6 +72,7 @@ typedef struct { void (*setInterval)(void *, unsigned long); void (*defaultId)(void *, unsigned long); void (*replicationHeartbeat)(void *, int); + void (*detectStaleMaster)(void *, int); } MONITOR_OBJECT; /** @@ -110,4 +113,5 @@ extern void monitorList(DCB *); extern void monitorSetId(MONITOR *, unsigned long); extern void monitorSetInterval (MONITOR *, unsigned long); extern void monitorSetReplicationHeartbeat(MONITOR *, int); +extern void monitorDetectStaleMaster(MONITOR *, int); #endif diff --git a/server/include/poll.h b/server/include/poll.h index e19be9c94..6524f1bbb 100644 --- a/server/include/poll.h +++ b/server/include/poll.h @@ -41,4 +41,5 @@ extern void poll_waitevents(void *); extern void poll_shutdown(); extern GWBITMASK *poll_bitmask(); extern void dprintPollStats(DCB *); +extern void dShowThreads(DCB *dcb); #endif diff --git a/server/include/server.h b/server/include/server.h index e747c298d..81392243c 100644 --- a/server/include/server.h +++ b/server/include/server.h @@ -38,6 +38,8 @@ * 03/06/14 Mark Riddoch Addition of maintainance mode * 20/06/14 Massimiliano Pinto Addition of master_id, depth, slaves fields * 26/06/14 Mark Riddoch Adidtion of server parameters + * 30/07/14 Massimiliano Pinto Addition of NDB status for MySQL Cluster + * 30/08/14 Massimiliano Pinto Addition of SERVER_STALE_STATUS * * @endverbatim */ @@ -95,12 +97,15 @@ typedef struct server { * * These are a bitmap of attributes that may be applied to a server */ -#define SERVER_RUNNING 0x0001 /**<< The server is up and running */ -#define SERVER_MASTER 0x0002 /**<< The server is a master, i.e. can handle writes */ -#define SERVER_SLAVE 0x0004 /**<< The server is a slave, i.e. can handle reads */ -#define SERVER_JOINED 0x0008 /**<< The server is joined in a Galera cluster */ -#define SERVER_MAINT 0x1000 /**<< Server is in maintenance mode */ -#define SERVER_SLAVE_OF_EXTERNAL_MASTER 0x0080 /**<< Server is slave of a Master outside the provided replication topology */ +#define SERVER_RUNNING 0x0001 /**<< The server is up and running */ +#define SERVER_MASTER 0x0002 /**<< The server is a master, i.e. can handle writes */ +#define SERVER_SLAVE 0x0004 /**<< The server is a slave, i.e. can handle reads */ +#define SERVER_JOINED 0x0008 /**<< The server is joined in a Galera cluster */ +#define SERVER_NDB 0x0010 /**<< The server is part of a MySQL cluster setup */ +#define SERVER_MAINT 0x0020 /**<< Server is in maintenance mode */ +#define SERVER_SLAVE_OF_EXTERNAL_MASTER 0x0040 /**<< Server is slave of a Master outside the provided replication topology */ +#define SERVER_STALE_STATUS 0x0080 /**<< Server stale status, monitor didn't update it */ +#define SERVER_AUTH_ERROR 0x1000 /**<< Authentication erorr from monitor */ /** * Is the server running - the macro returns true if the server is marked as running @@ -131,15 +136,21 @@ typedef struct server { #define SERVER_IS_JOINED(server) \ (((server)->status & (SERVER_RUNNING|SERVER_JOINED|SERVER_MAINT)) == (SERVER_RUNNING|SERVER_JOINED)) +/** + * Is the server a SQL node in MySQL Cluster? The server must be running and with NDB status + */ +#define SERVER_IS_NDB(server) \ + (((server)->status & (SERVER_RUNNING|SERVER_NDB|SERVER_MAINT)) == (SERVER_RUNNING|SERVER_NDB)) + /** * Is the server in maintenance mode. */ #define SERVER_IN_MAINT(server) ((server)->status & SERVER_MAINT) /** server is not master, slave or joined */ -#define SERVER_NOT_IN_CLUSTER(s) (((s)->status & (SERVER_MASTER|SERVER_SLAVE|SERVER_JOINED)) == 0) +#define SERVER_NOT_IN_CLUSTER(s) (((s)->status & (SERVER_MASTER|SERVER_SLAVE|SERVER_JOINED|SERVER_NDB)) == 0) -#define SERVER_IS_IN_CLUSTER(s) (((s)->status & (SERVER_MASTER|SERVER_SLAVE|SERVER_JOINED)) != 0) +#define SERVER_IS_IN_CLUSTER(s) (((s)->status & (SERVER_MASTER|SERVER_SLAVE|SERVER_JOINED|SERVER_NDB)) != 0) #define SERVER_IS_RELAY_SERVER(server) \ (((server)->status & (SERVER_RUNNING|SERVER_MASTER|SERVER_SLAVE|SERVER_MAINT)) == (SERVER_RUNNING|SERVER_MASTER|SERVER_SLAVE)) diff --git a/server/include/service.h b/server/include/service.h index 43077c511..9f5bef7b4 100644 --- a/server/include/service.h +++ b/server/include/service.h @@ -137,7 +137,7 @@ typedef struct service { struct service *next; /**< The next service in the linked list */ } SERVICE; -typedef enum count_spec_t {COUNT_ATLEAST=0, COUNT_EXACT, COUNT_ATMOST} count_spec_t; +typedef enum count_spec_t {COUNT_NONE=0, COUNT_ATLEAST, COUNT_EXACT, COUNT_ATMOST} count_spec_t; #define SERVICE_STATE_ALLOC 1 /**< The service has been allocated */ #define SERVICE_STATE_STARTED 2 /**< The service has been started */ diff --git a/server/include/spinlock.h b/server/include/spinlock.h index 42f7b5c2e..43192da3f 100644 --- a/server/include/spinlock.h +++ b/server/include/spinlock.h @@ -21,7 +21,7 @@ /** * @file spinlock.h * - * Spinlock implementation for ther gateway. + * Spinlock implementation for MaxScale. * * Spinlocks are cheap locks that can be used to protect short code blocks, they are * generally wasteful as any blocked threads will spin, consuming CPU cycles, waiting @@ -31,12 +31,28 @@ #include #include +#define SPINLOCK_PROFILE 1 + +/** + * The spinlock structure. + * + * In normal builds the structure merely contains a lock value which + * is 0 if the spinlock is not taken and greater than zero if it is held. + * + * In builds with the SPINLOCK_PROFILE option set this structure also holds + * a number of profile related fields that count the number of spins, number + * of waiting threads and the number of times the lock has been acquired. + */ typedef struct spinlock { - int lock; -#if DEBUG - int spins; - int acquired; - THREAD owner; + int lock; /*< Is the lock held? */ +#if SPINLOCK_PROFILE + int spins; /*< Number of spins on this lock */ + int maxspins; /*< Max no of spins to acquire lock */ + int acquired; /*< No. of times lock was acquired */ + int waiting; /*< No. of threads acquiring this lock */ + int max_waiting; /*< Max no of threads waiting for lock */ + int contended; /*< No. of times acquire was contended */ + THREAD owner; /*< Last owner of this lock */ #endif } SPINLOCK; @@ -47,8 +63,8 @@ typedef struct spinlock { #define FALSE false #endif -#if DEBUG -#define SPINLOCK_INIT { 0, 0, 0, NULL } +#if SPINLOCK_PROFILE +#define SPINLOCK_INIT { 0, 0, 0, 0, 0, 0, 0, 0 } #else #define SPINLOCK_INIT { 0 } #endif @@ -59,4 +75,6 @@ extern void spinlock_init(SPINLOCK *lock); extern void spinlock_acquire(SPINLOCK *lock); extern int spinlock_acquire_nowait(SPINLOCK *lock); extern void spinlock_release(SPINLOCK *lock); +extern void spinlock_stats(SPINLOCK *lock, + void (*reporter)(void *, char *, int), void *hdl); #endif diff --git a/server/modules/filter/Makefile b/server/modules/filter/Makefile index 8e6036e91..c52c8a8fa 100644 --- a/server/modules/filter/Makefile +++ b/server/modules/filter/Makefile @@ -21,15 +21,16 @@ include ../../../build_gateway.inc LOGPATH := $(ROOT_PATH)/log_manager +QCLASSPATH := $(ROOT_PATH)/query_classifier UTILSPATH := $(ROOT_PATH)/utils CC=cc -CFLAGS=-c -fPIC -I/usr/include -I../include -I../../include -I$(LOGPATH) \ - -I$(UTILSPATH) -Wall -g +CFLAGS=-c -fPIC -I/usr/include -I../include -I../../include -I$(LOGPATH) -I$(QCLASSPATH) \ + -I$(UTILSPATH) -I$(MYSQL_ROOT) -Wall -g include ../../../makefile.inc -LDFLAGS=-shared -L$(LOGPATH) -Wl,-rpath,$(DEST)/lib \ +LDFLAGS=-shared -L$(LOGPATH) -L$(EMBEDDED_LIB) -L$(QCLASSPATH) -Wl,-rpath,$(DEST)/lib \ -Wl,-rpath,$(LOGPATH) -Wl,-rpath,$(UTILSPATH) TESTSRCS=testfilter.c @@ -42,17 +43,31 @@ TOPNSRCS=topfilter.c TOPNOBJ=$(TOPNSRCS:.c=.o) TEESRCS=tee.c TEEOBJ=$(TEESRCS:.c=.o) +MQSRCS=mqfilter.c +MQOBJ=$(MQSRCS:.c=.o) SRCS=$(TESTSRCS) $(QLASRCS) $(REGEXSRCS) $(TOPNSRCS) $(TEESRCS) OBJ=$(SRCS:.c=.o) LIBS=$(UTILSPATH)/skygw_utils.o -lssl -llog_manager -MODULES= libtestfilter.so libqlafilter.so libregexfilter.so libtopfilter.so libtee.so +MODULES= libtestfilter.so libqlafilter.so libregexfilter.so libtopfilter.so libhintfilter.so libtee.so +ifndef BUILD_RABBITMQ +BUILD_RABBITMQ=N +endif + +ifeq ($(BUILD_RABBITMQ),Y) +SRCS += $(MQSRCS) +MODULES += libmqfilter.so +LIBS += -lrabbitmq -lquery_classifier +endif all: $(MODULES) libtestfilter.so: $(TESTOBJ) $(CC) $(LDFLAGS) $(TESTOBJ) $(LIBS) -o $@ +libmqfilter.so: $(MQOBJ) + $(CC) $(LDFLAGS) $(MQOBJ) $(LIBS) -o $@ + libqlafilter.so: $(QLAOBJ) $(CC) $(LDFLAGS) $(QLAOBJ) $(LIBS) -o $@ @@ -65,32 +80,38 @@ libtopfilter.so: $(TOPNOBJ) libtee.so: $(TEEOBJ) $(CC) $(LDFLAGS) $(TEEOBJ) $(LIBS) -o $@ +libhintfilter.so: + (cd hint; touch depend.mk ; make; cp $@ ..) + .c.o: $(CC) $(CFLAGS) $< -o $@ clean: - $(DEL) $(OBJ) $(MODULES) + rm -f $(OBJ) $(MODULES) + (cd hint; touch depend.mk; make clean) tags: ctags $(SRCS) $(HDRS) + (cd hint; touch depend.mk; make tags) depend: - @$(DEL) depend.mk + @rm -f depend.mk cc -M $(CFLAGS) $(SRCS) > depend.mk + (cd hint; touch depend.mk; make depend) install: $(MODULES) install -D $(MODULES) $(DEST)/modules cleantests: $(MAKE) -C test cleantests - + buildtests: $(MAKE) -C test DEBUG=Y buildtests - + runtests: $(MAKE) -C test runtests - + testall: $(MAKE) -C test testall - + include depend.mk diff --git a/server/modules/filter/hint/Makefile b/server/modules/filter/hint/Makefile new file mode 100644 index 000000000..4f2194739 --- /dev/null +++ b/server/modules/filter/hint/Makefile @@ -0,0 +1,70 @@ +# This file is distributed as part of MaxScale form SkySQL. It is free +# software: you can redistribute it and/or modify it under the terms of the +# GNU General Public License as published by the Free Software Foundation, +# version 2. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Copyright SkySQL Ab 2014 +# +# Revision History +# Date Who Description +# 21/07/14 Mark Riddoch Initial module development + +include ../../../../build_gateway.inc + +LOGPATH := $(ROOT_PATH)/log_manager +UTILSPATH := $(ROOT_PATH)/utils + +CC=cc +CFLAGS=-c -fPIC -I/usr/include -I../../include -I../../../include -I$(LOGPATH) \ + -I$(UTILSPATH) -Wall -g + +include ../../../../makefile.inc + +LDFLAGS=-shared -L$(LOGPATH) -Wl,-rpath,$(DEST)/lib \ + -Wl,-rpath,$(LOGPATH) -Wl,-rpath,$(UTILSPATH) + +SRCS= hintfilter.c hintparser.c +OBJ=$(SRCS:.c=.o) +LIBS=$(UTILSPATH)/skygw_utils.o -lssl -llog_manager + +libhintfilter.so: $(OBJ) + $(CC) $(LDFLAGS) $(OBJ) $(LIBS) -o $@ + +.c.o: + $(CC) $(CFLAGS) $< -o $@ + +clean: + rm -f $(OBJ) libhintfilter.so + +tags: + ctags $(SRCS) $(HDRS) + +depend: + @rm -f depend.mk + cc -M $(CFLAGS) $(SRCS) > depend.mk + +install: $(MODULES) + install -D $(MODULES) $(DEST)/modules + +cleantests: + $(MAKE) -C test cleantests + +buildtests: + $(MAKE) -C test DEBUG=Y buildtests + +runtests: + $(MAKE) -C test runtests + +testall: + $(MAKE) -C test testall + +include depend.mk diff --git a/server/modules/filter/hint/hintfilter.c b/server/modules/filter/hint/hintfilter.c new file mode 100644 index 000000000..e54319d5d --- /dev/null +++ b/server/modules/filter/hint/hintfilter.c @@ -0,0 +1,272 @@ +/* + * This file is distributed as part of MaxScale by SkySQL. It is free + * software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, + * version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright SkySQL Ab 2014 + */ +#include +#include +#include +#include +#include + +/** + * hintfilter.c - a filter to parse the MaxScale hint syntax and attach those + * hints to the buffers that carry the requests. + * + */ + +MODULE_INFO info = { + MODULE_API_FILTER, + MODULE_ALPHA_RELEASE, + FILTER_VERSION, + "A hint parsing filter" +}; + +static char *version_str = "V1.0.0"; + +static FILTER *createInstance(char **options, FILTER_PARAMETER **params); +static void *newSession(FILTER *instance, SESSION *session); +static void closeSession(FILTER *instance, void *session); +static void freeSession(FILTER *instance, void *session); +static void setDownstream(FILTER *instance, void *fsession, DOWNSTREAM *downstream); +static int routeQuery(FILTER *instance, void *fsession, GWBUF *queue); +static void diagnostic(FILTER *instance, void *fsession, DCB *dcb); + + +static FILTER_OBJECT MyObject = { + createInstance, + newSession, + closeSession, + freeSession, + setDownstream, + NULL, // No upstream requirement + routeQuery, + NULL, + diagnostic, +}; + +/** + * Implementation of the mandatory version entry point + * + * @return version string of the module + */ +char * +version() +{ + return version_str; +} + +/** + * The module initialisation routine, called when the module + * is first loaded. + */ +void +ModuleInit() +{ +} + +/** + * The module entry point routine. It is this routine that + * must populate the structure that is referred to as the + * "module object", this is a structure with the set of + * external entry points for this module. + * + * @return The module object + */ +FILTER_OBJECT * +GetModuleObject() +{ + return &MyObject; +} + +/** + * Create an instance of the filter for a particular service + * within MaxScale. + * + * @param options The options for this filter + * + * @return The instance data for this new instance + */ +static FILTER * +createInstance(char **options, FILTER_PARAMETER **params) +{ +HINT_INSTANCE *my_instance; + + if ((my_instance = calloc(1, sizeof(HINT_INSTANCE))) != NULL) + my_instance->sessions = 0; + return (FILTER *)my_instance; +} + +/** + * Associate a new session with this instance of the filter. + * + * @param instance The filter instance data + * @param session The session itself + * @return Session specific data for this session + */ +static void * +newSession(FILTER *instance, SESSION *session) +{ +HINT_INSTANCE *my_instance = (HINT_INSTANCE *)instance; +HINT_SESSION *my_session; + + if ((my_session = calloc(1, sizeof(HINT_SESSION))) != NULL) + { + my_session->query_len = 0; + my_session->request = NULL; + my_session->stack = NULL; + my_session->named_hints = NULL; + } + + return my_session; +} + +/** + * Close a session with the filter, this is the mechanism + * by which a filter may cleanup data structure etc. + * + * @param instance The filter instance data + * @param session The session being closed + */ +static void +closeSession(FILTER *instance, void *session) +{ +HINT_SESSION *my_session = (HINT_SESSION *)session; +NAMEDHINTS* named_hints; +HINTSTACK* hint_stack; + + if (my_session->request) + gwbuf_free(my_session->request); + + + /** Free named hints */ + named_hints = my_session->named_hints; + + while ((named_hints = free_named_hint(named_hints)) != NULL) + ; + /** Free stacked hints */ + hint_stack = my_session->stack; + + while ((hint_stack = free_hint_stack(hint_stack)) != NULL) + ; +} + +/** + * Free the memory associated with this filter session. + * + * @param instance The filter instance data + * @param session The session being closed + */ +static void +freeSession(FILTER *instance, void *session) +{ + free(session); + return; +} + +/** + * Set the downstream component for this filter. + * + * @param instance The filter instance data + * @param session The session being closed + * @param downstream The downstream filter or router + */ +static void +setDownstream(FILTER *instance, void *session, DOWNSTREAM *downstream) +{ +HINT_SESSION *my_session = (HINT_SESSION *)session; + + my_session->down = *downstream; +} + +/** + * The routeQuery entry point. This is passed the query buffer + * to which the filter should be applied. Once applied the + * query shoudl normally be passed to the downstream component + * (filter or router) in the filter chain. + * + * @param instance The filter instance data + * @param session The filter session + * @param queue The query data + */ +static int +routeQuery(FILTER *instance, void *session, GWBUF *queue) +{ +HINT_SESSION *my_session = (HINT_SESSION *)session; +char *ptr; +int rval, len, residual; +HINT *hint; + + if (my_session->request == NULL) + { + /* + * No stored buffer, so this must be the first + * buffer of a new request. + */ + if (modutil_MySQL_Query(queue, &ptr, &len, &residual) == 0) + { + return my_session->down.routeQuery( + my_session->down.instance, + my_session->down.session, queue); + } + my_session->request = queue; + my_session->query_len = len; + } + else + { + gwbuf_append(my_session->request, queue); + } + + if (gwbuf_length(my_session->request) < my_session->query_len) + { + /* + * We have not got the entire SQL text, buffer and wait for + * the remainder. + */ + return 1; + } + /* We have the entire SQL text, parse for hints and attach to the + * buffer at the head of the queue. + */ + queue = my_session->request; + my_session->request = NULL; + my_session->query_len = 0; + hint = hint_parser(my_session, queue); + queue->hint = hint; + + /* Now process the request */ + rval = my_session->down.routeQuery(my_session->down.instance, + my_session->down.session, queue); + return rval; +} + +/** + * Diagnostics routine + * + * If fsession is NULL then print diagnostics on the filter + * instance as a whole, otherwise print diagnostics for the + * particular session. + * + * @param instance The filter instance + * @param fsession Filter session, may be NULL + * @param dcb The DCB for diagnostic output + */ +static void +diagnostic(FILTER *instance, void *fsession, DCB *dcb) +{ +HINT_INSTANCE *my_instance = (HINT_INSTANCE *)instance; +HINT_SESSION *my_session = (HINT_SESSION *)fsession; + +} diff --git a/server/modules/filter/hint/hintparser.c b/server/modules/filter/hint/hintparser.c new file mode 100644 index 000000000..77d9c98ec --- /dev/null +++ b/server/modules/filter/hint/hintparser.c @@ -0,0 +1,770 @@ +/* + * This file is distributed as part of MaxScale by SkySQL. It is free + * software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, + * version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright SkySQL Ab 2014 + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern int lm_enabled_logfiles_bitmask; + +/** + * hintparser.c - Find any comment in the SQL packet and look for MAXSCALE + * hints in that comment. + */ + +/** + * The keywords in the hint syntax + */ +struct { + char *keyword; + TOKEN_VALUE token; +} keywords[] = { + { "maxscale", TOK_MAXSCALE }, + { "prepare", TOK_PREPARE }, + { "start", TOK_START }, + { "begin", TOK_START }, + { "stop", TOK_STOP }, + { "end", TOK_STOP }, + { "=", TOK_EQUAL }, + { "route", TOK_ROUTE }, + { "to", TOK_TO }, + { "master", TOK_MASTER }, + { "slave", TOK_SLAVE }, + { "server", TOK_SERVER }, + { NULL, 0 } +}; +/** +HINT_TOKEN kwords[] = { + { TOK_MAXSCALE, "maxscale" }, + { TOK_PREPARE, "prepare" }, + { TOK_START, "start" }, + { TOK_START, "begin" }, + { TOK_STOP, "stop" }, + { TOK_STOP, "end" }, + { TOK_EQUAL, "=" }, + { TOK_ROUTE, "route" }, + { TOK_TO, "to" }, + { TOK_MASTER, "master" }, + { TOK_SLAVE, "slave" }, + { TOK_SERVER, "server" }, + { 0, NULL} +}; +*/ + +static HINT_TOKEN *hint_next_token(GWBUF **buf, char **ptr); +static void hint_pop(HINT_SESSION *); +static HINT *lookup_named_hint(HINT_SESSION *, char *); +static void create_named_hint(HINT_SESSION *, char *, HINT *); +static void hint_push(HINT_SESSION *, HINT *); +static const char* token_get_keyword (HINT_TOKEN* token); +static void token_free(HINT_TOKEN* token); + +typedef enum { HM_EXECUTE, HM_START, HM_PREPARE } HINT_MODE; + +void token_free(HINT_TOKEN* token) +{ + if (token->value != NULL) + { + free(token->value); + } + free(token); +} + + +static const char* token_get_keyword ( + HINT_TOKEN* token) +{ + switch (token->token) { + case TOK_EOL: + return "End of line"; + break; + + case TOK_STRING: + return token->value; + break; + + default: + { + int i = 0; + while (i < TOK_EOL && keywords[i].token != token->token) + i++; + + ss_dassert(i != TOK_EOL); + + if (i == TOK_EOL) + { + return "Unknown token"; + } + else + { + return keywords[i].keyword; + } + } + break; + } +} +/** + * Parse the hint comments in the MySQL statement passed in request. + * Add any hints to the buffer for later processing. + * + * @param session The filter session + * @param request The MySQL request buffer + * @return The hints parsed in this statement or active on the + * stack + */ +HINT * +hint_parser(HINT_SESSION *session, GWBUF *request) +{ +char *ptr, lastch = ' '; +int len, residual, state; +int found, escape, quoted, squoted; +HINT *rval = NULL; +char *pname, *lvalue, *hintname = NULL; +GWBUF *buf; +HINT_TOKEN *tok; +HINT_MODE mode = HM_EXECUTE; + + /* First look for any comment in the SQL */ + modutil_MySQL_Query(request, &ptr, &len, &residual); + buf = request; + found = 0; + escape = 0; + quoted = 0; + squoted = 0; + do { + while (len--) + { + if (*ptr == '\\') + escape = 1; + else if (*ptr == '\"' && quoted) + quoted = 0; + else if (*ptr == '\"' && quoted == 0) + quoted = 0; + else if (*ptr == '\'' && squoted) + squoted = 0; + else if (*ptr == '\"' && squoted == 0) + squoted = 0; + else if (quoted || squoted) + ; + else if (escape) + escape = 0; + else if (*ptr == '#') + { + found = 1; + break; + } + else if (*ptr == '/') + lastch = '/'; + else if (*ptr == '*' && lastch == '/') + { + found = 1; + break; + } + else if (*ptr == '-' && lastch == '-') + { + found = 1; + break; + } + else if (*ptr == '-') + lastch = '-'; + else + lastch = *ptr; + ptr++; + } + if (found) + break; + + buf = buf->next; + if (buf) + { + len = GWBUF_LENGTH(buf); + ptr = GWBUF_DATA(buf); + } + } while (buf); + + if (!found) /* No comment so we need do no more */ + { + goto retblock; + } + + /* + * If we have got here then we have a comment, ptr point to + * the comment character if it is a '#' comment or the second + * character of the comment if it is a -- or /* comment + * + * Move to the next character in the SQL. + */ + ptr++; + if (ptr > (char *)(buf->end)) + { + buf = buf->next; + if (buf) + ptr = GWBUF_DATA(buf); + else + goto retblock; + } + + tok = hint_next_token(&buf, &ptr); + + if (tok == NULL) + { + goto retblock; + } + + /** This is not MaxScale hint because it doesn't start with 'maxscale' */ + if (tok->token != TOK_MAXSCALE) + { + token_free(tok); + goto retblock; + } + token_free(tok); + + state = HS_INIT; + + while ((tok = hint_next_token(&buf, &ptr)) != NULL + && tok->token != TOK_EOL) + { + switch (state) + { + case HS_INIT: + switch (tok->token) + { + case TOK_ROUTE: + state = HS_ROUTE; + break; + case TOK_STRING: + state = HS_NAME; + lvalue = strdup(tok->value); + break; + case TOK_STOP: + /* Action: pop active hint */ + hint_pop(session); + state = HS_INIT; + break; + case TOK_START: + hintname = NULL; + mode = HM_START; + state = HS_INIT; + break; + default: + /* Error: expected hint, name or STOP */ + LOGIF(LT, (skygw_log_write( + LOGFILE_TRACE, + "Error : Syntax error in hint. Expected " + "'route', 'stop' or hint name instead of " + "'%s'. Hint ignored.", + token_get_keyword(tok)))); + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Error : Syntax error in hint. Expected " + "'route', 'stop' or hint name instead of " + "'%s'. Hint ignored.", + token_get_keyword(tok)))); + token_free(tok); + goto retblock; + } + break; + case HS_ROUTE: + if (tok->token != TOK_TO) + { + /* Error, expect TO */; + LOGIF(LT, (skygw_log_write( + LOGFILE_TRACE, + "Error : Syntax error in hint. Expected " + "'to' instead of '%s'. Hint ignored.", + token_get_keyword(tok)))); + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Error : Syntax error in hint. Expected " + "'to' instead of '%s'. Hint ignored.", + token_get_keyword(tok)))); + token_free(tok); + goto retblock; + } + state = HS_ROUTE1; + break; + case HS_ROUTE1: + switch (tok->token) + { + case TOK_MASTER: + rval = hint_create_route(rval, + HINT_ROUTE_TO_MASTER, NULL); + break; + case TOK_SLAVE: + rval = hint_create_route(rval, + HINT_ROUTE_TO_SLAVE, NULL); + break; + case TOK_SERVER: + state = HS_ROUTE_SERVER; + break; + default: + /* Error expected MASTER, SLAVE or SERVER */ + LOGIF(LT, (skygw_log_write( + LOGFILE_TRACE, + "Error : Syntax error in hint. Expected " + "'master', 'slave', or 'server' instead " + "of '%s'. Hint ignored.", + token_get_keyword(tok)))); + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Error : Syntax error in hint. Expected " + "'master', 'slave', or 'server' instead " + "of '%s'. Hint ignored.", + token_get_keyword(tok)))); + + token_free(tok); + goto retblock; + } + break; + case HS_ROUTE_SERVER: + if (tok->token == TOK_STRING) + { + rval = hint_create_route(rval, + HINT_ROUTE_TO_NAMED_SERVER, tok->value); + } + else + { + /* Error: Expected server name */ + LOGIF(LT, (skygw_log_write( + LOGFILE_TRACE, + "Error : Syntax error in hint. Expected " + "server name instead of '%s'. Hint " + "ignored.", + token_get_keyword(tok)))); + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Error : Syntax error in hint. Expected " + "server name instead of '%s'. Hint " + "ignored.", + token_get_keyword(tok)))); + token_free(tok); + goto retblock; + } + break; + case HS_NAME: + switch (tok->token) + { + case TOK_EQUAL: + pname = lvalue; + state = HS_PVALUE; + break; + case TOK_PREPARE: + pname = lvalue; + state = HS_PREPARE; + break; + case TOK_START: + /* Action start(lvalue) */ + hintname = lvalue; + mode = HM_START; + state = HS_INIT; + break; + default: + /* Error, token tok->value not expected */ + LOGIF(LT, (skygw_log_write( + LOGFILE_TRACE, + "Error : Syntax error in hint. Expected " + "'=', 'prepare', or 'start' instead of " + "'%s'. Hint ignored.", + token_get_keyword(tok)))); + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Error : Syntax error in hint. Expected " + "'=', 'prepare', or 'start' instead of " + "'%s'. Hint ignored.", + token_get_keyword(tok)))); + token_free(tok); + goto retblock; + } + break; + case HS_PVALUE: + /* Action: pname = tok->value */ + rval = hint_create_parameter(rval, pname, tok->value); + state = HS_INIT; + break; + case HS_PREPARE: + mode = HM_PREPARE; + hintname = lvalue; + switch (tok->token) + { + case TOK_ROUTE: + state = HS_ROUTE; + break; + case TOK_STRING: + state = HS_NAME; + lvalue = tok->value; + break; + default: + /* Error, token tok->value not expected */ + LOGIF(LT, (skygw_log_write( + LOGFILE_TRACE, + "Error : Syntax error in hint. Expected " + "'route' or hint name instead of " + "'%s'. Hint ignored.", + token_get_keyword(tok)))); + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Error : Syntax error in hint. Expected " + "'route' or hint name instead of " + "'%s'. Hint ignored.", + token_get_keyword(tok)))); + token_free(tok); + goto retblock; + } + break; + } + token_free(tok); + } /*< while */ + + if (tok->token == TOK_EOL) + { + token_free(tok); + } + + switch (mode) + { + case HM_START: + /* + * We are starting either a predefined set of hints, + * creating a new set of hints and starting in a single + * operation or starting an anonymous block of hints. + */ + if (hintname == NULL && rval != NULL) + { + /* We are starting an anonymous block of hints */ + hint_push(session, rval); + rval = NULL; + } else if (hintname && rval) + { + /* We are creating and starting a block of hints */ + if (lookup_named_hint(session, hintname) != NULL) + { + /* Error hint with this name already exists */ + } + else + { + create_named_hint(session, hintname, rval); + hint_push(session, hint_dup(rval)); + } + } else if (hintname && rval == NULL) + { + /* We starting an already define set of named hints */ + rval = lookup_named_hint(session, hintname); + hint_push(session, hint_dup(rval)); + free(hintname); + rval = NULL; + } else if (hintname == NULL && rval == NULL) + { + /* Error case */ + } + break; + case HM_PREPARE: + /* + * We are preparing a named set of hints. Note this does + * not trigger the usage of these hints currently. + */ + if (hintname == NULL || rval == NULL) + { + /* Error case, name and hints must be defined */ + } + else + { + create_named_hint(session, hintname, rval); + } + /* We are not starting the hints now, so return an empty + * hint set. + */ + rval = NULL; + break; + case HM_EXECUTE: + /* + * We have a one-off hint for the statement we are + * currently forwarding. + */ + break; + } + +retblock: + if (rval == NULL) + { + /* No new hint parsed in this statement, apply the current + * top of stack if there is one. + */ + if (session->stack) + rval = hint_dup(session->stack->hint); + } + return rval; +} + +/** + * Return the next token in the inout stream + * + * @param buf A pointer to the buffer point, will be updated if a + * new buffer is used. + * @param ptr The pointer within the buffer we are processing + * @return A HINT token + */ +static HINT_TOKEN * +hint_next_token(GWBUF **buf, char **ptr) +{ +char word[100], *dest; +int inword = 0; + int endtag = 0; +char inquote = '\0'; +int i, found; +HINT_TOKEN *tok; + + if ((tok = (HINT_TOKEN *)malloc(sizeof(HINT_TOKEN))) == NULL) + return NULL; + tok->value = NULL; + dest = word; + while (*ptr < (char *)((*buf)->end) || (*buf)->next) + { + /** word ends, don't move ptr but return with read word */ + if (inword && inquote == '\0' && + (isspace(**ptr) || **ptr == '=')) + { + inword = 0; + break; + } + /** found '=', move ptr and return with '=' */ + else if (!inword && inquote == '\0' && **ptr == '=') + { + *dest = **ptr; + *dest++; + (*ptr)++; + break; + } + else if (**ptr == '\'' && inquote == '\'') + inquote = '\0'; + else if (**ptr == '\'') + inquote = **ptr; + /** Any other character which belongs to the word, move ahead */ + + else if(**ptr == '/' && endtag) + { + /** Comment end tag found, rewind the pointer back and return the token */ + inword = 0; + (*ptr)--; + break; + } + else if(**ptr == '*' && !endtag) + { + endtag = 1; + } + else if (inword || (isspace(**ptr) == 0)) + { + *dest++ = **ptr; + inword = 1; + } + (*ptr)++; + + if (*ptr > (char *)((*buf)->end) && (*buf)->next) + { + *buf = (*buf)->next; + *ptr = (*buf)->start; + } + + if (dest - word > 98) + break; + } /*< while */ + *dest = 0; + + /* We now have a word in the local word, check to see if it is a + * token we recognise. + */ + if (word[0] == '\0' || (word[0] == '*' && word[1] == '/')) + { + tok->token = TOK_EOL; + return tok; + } + found = 0; + for (i = 0; keywords[i].keyword; i++) + { + if (strcasecmp(word, keywords[i].keyword) == 0) + { + tok->token = keywords[i].token; + found = 1; + break; + } + } + if (found == 0) + { + tok->token = TOK_STRING; + tok->value = strdup(word); + } + + return tok; +} + + +/** + * hint_pop - pop the hint off the top of the stack if it is not empty + * + * @param session The filter session. + */ +void +hint_pop(HINT_SESSION *session) +{ +HINTSTACK *ptr; +HINT *hint; + + if ((ptr = session->stack) != NULL) + { + session->stack = ptr->next; + while (ptr->hint) + { + hint = ptr->hint; + ptr->hint = hint->next; + hint_free(hint); + } + free(ptr); + } +} + +/** + * Push a hint onto the stack of actie hints + * + * @param session The filter session + * @param hint The hint to push, the hint ownership is retained + * by the stack and should not be freed by the caller + */ +static void +hint_push(HINT_SESSION *session, HINT *hint) +{ +HINTSTACK *item; + + if ((item = (HINTSTACK *)malloc(sizeof(HINTSTACK))) == NULL) + return; + item->hint = hint; + item->next = session->stack; + session->stack = item; +} + +/** + * Search for a hint block that already exists with this name + * + * @param session The filter session + * @param name The name to lookup + * @return the HINT or NULL if the name was not found. + */ +static HINT * +lookup_named_hint(HINT_SESSION *session, char *name) +{ +NAMEDHINTS *ptr = session->named_hints; + + while (ptr) + { + if (strcmp(ptr->name, name) == 0) + return ptr->hints; + ptr = ptr->next; + } + return NULL; +} + +/** + * Create a named hint block + * + * @param session The filter session + * @param name The name of the block to ceate + * @param hint The hints themselves + */ +static void +create_named_hint(HINT_SESSION *session, char *name, HINT *hint) +{ +NAMEDHINTS *block; + + if ((block = (NAMEDHINTS *)malloc(sizeof(NAMEDHINTS))) == NULL) + return; + + block->name = name; + block->hints = hint_dup(hint); + block->next = session->named_hints; + session->named_hints = block; +} + +/** + * Release the given NAMEDHINTS struct and all included hints. + * + * @param named_hint NAMEDHINTS struct to be released + * + * @return pointer to next NAMEDHINTS struct. + */ +NAMEDHINTS* free_named_hint( + NAMEDHINTS* named_hint) +{ + NAMEDHINTS* next; + + if (named_hint != NULL) + { + HINT* hint; + + next = named_hint->next; + + while (named_hint->hints != NULL) + { + hint = named_hint->hints->next; + hint_free(named_hint->hints); + named_hint->hints = hint; + } + free(named_hint->name); + free(named_hint); + return next; + } + else + { + return NULL; + } +} + +/** + * Release the given HINTSTACK struct and all included hints. + * + * @param hint_stack HINTSTACK struct to be released + * + * @return pointer to next HINTSTACK struct. + */ +HINTSTACK* free_hint_stack( + HINTSTACK* hint_stack) +{ + HINTSTACK* next; + + if (hint_stack != NULL) + { + HINT* hint; + + next = hint_stack->next; + + while (hint_stack->hint != NULL) + { + hint = hint_stack->hint->next; + hint_free(hint_stack->hint); + hint_stack->hint = hint; + } + free(hint_stack); + return next; + } + else + { + return NULL; + } +} diff --git a/server/modules/filter/mqfilter.c b/server/modules/filter/mqfilter.c new file mode 100644 index 000000000..9761fa777 --- /dev/null +++ b/server/modules/filter/mqfilter.c @@ -0,0 +1,1462 @@ +/* + * This file is distributed as part of MaxScale by SkySQL. It is free + * software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, + * version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright SkySQL Ab 2014 + */ + +/** + * @file mqfilter.c + * MQ Filter - AMQP Filter. + * A filter that logs and publishes canonized queries on to a RabbitMQ server. + * + * The filter reads the routed query, forms a canonized version of it and publishes the + * message on the RabbitMQ server. The messages are timestamped with a pure unix-timestamp that + * is meant to be easily transformable in various environments. Replies to the queries are also logged + * and published on the RabbitMQ server. + * + * The filter makes no attempt to deal with queries that do not fit + * in a single GWBUF or result sets that span multiple GWBUFs. + * + * To use a SSL connection the CA certificate, the client certificate and the client public key must be provided. + * By default this filter uses a TCP connection. + *@verbatim + * The options for this filter are: + * + * logging_trigger Set the logging level + * logging_strict Sets whether to trigger when any of the parameters match or only if all parameters match + * logging_log_all Log only SELECT, UPDATE, DELETE and INSERT or all posddible queries + * hostname The server hostname where the messages are sent + * port Port to send the messages to + * username Server login username + * password Server login password + * vhost The virtual host location on the server, where the messages are sent + * exchange The name of the exchange + * exchange_type The type of the exchange, defaults to direct + * key The routing key used when sending messages to the exchange + * queue The queue that will be bound to the used exchange + * ssl_CA_cert Path to the CA certificate in PEM format + * ssl_client_cert Path to the client cerificate in PEM format + * ssl_client_key Path to the client public key in PEM format + * + * The logging trigger levels are: + * all Log everything + * source Trigger on statements originating from a particular source (database user and host combination) + * schema Trigger on a certain schema + * object Trigger on a particular database object (table or view) + *@endverbatim + * See the individual struct documentations for logging trigger parameters + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +MODULE_INFO info = { + MODULE_API_FILTER, + MODULE_ALPHA_RELEASE, + FILTER_VERSION, + "A RabbitMQ query logging filter" +}; + +static char *version_str = "V1.0.2"; +static int uid_gen; + +/* + * The filter entry points + */ +static FILTER *createInstance(char **options, FILTER_PARAMETER **); +static void *newSession(FILTER *instance, SESSION *session); +static void closeSession(FILTER *instance, void *session); +static void freeSession(FILTER *instance, void *session); +static void setDownstream(FILTER *instance, void *fsession, DOWNSTREAM *downstream); +static void setUpstream(FILTER *instance, void *fsession, UPSTREAM *upstream); +static int routeQuery(FILTER *instance, void *fsession, GWBUF *queue); +static int clientReply(FILTER *instance, void *fsession, GWBUF *queue); +static void diagnostic(FILTER *instance, void *fsession, DCB *dcb); + + +static FILTER_OBJECT MyObject = { + createInstance, + newSession, + closeSession, + freeSession, + setDownstream, + setUpstream, + routeQuery, + clientReply, + diagnostic, +}; + +/** + *Structure used to store messages and their properties. + */ +typedef struct mqmessage_t { + amqp_basic_properties_t *prop; + char *msg; + struct mqmessage_t *next; +}mqmessage; + +/** + *Logging trigger levels + */ +enum log_trigger_t{ + TRG_ALL = 0x00, + TRG_SOURCE = 0x01, + TRG_SCHEMA = 0x02, + TRG_OBJECT = 0x04 +}; + +/** + * Source logging trigger + * + * Log only those queries that come from a valid pair of username and hostname combinations. + * Both options allow multiple values separated by a ','. + * @verbatim + * Trigger options: + * logging_source_user Comma-separated list of usernames to log + * logging_source_host Comma-separated list of hostnames to log + * @endverbatim + */ +typedef struct source_trigger_t{ + char** user; + int usize; + char** host; + int hsize; +}SRC_TRIG; + +/** + * Schema logging trigger + * + * Log only those queries that target a specific database. + * + * Trigger options: + * logging_schema Comma-separated list of databases + */ +typedef struct schema_trigger_t{ + char** objects; + int size; +}SHM_TRIG; + + +/** + * Database object logging trigger + * + * Log only those queries that target specific database objects. + *@verbatim + * Trigger options: + * logging_object Comma-separated list of database objects + *@endverbatim + */ +typedef struct object_trigger_t{ + char** objects; + int size; +}OBJ_TRIG; + + +/** + * A instance structure, containing the hostname, login credentials, + * virtual host location and the names of the exchange and the key. + * Also contains the paths to the CA certificate and client certificate and key. + * + * Default values assume that a local RabbitMQ server is running on port 5672 with the default + * user 'guest' and the password 'guest' using a default exchange named 'default_exchange' with a + * routing key named 'key'. Type of the exchange is 'direct' by default and all queries are logged. + * + */ + +typedef struct { + int port; + char *hostname; + char *username; + char *password; + char *vhost; + char *exchange; + char *exchange_type; + char *key; + char *queue; + bool use_ssl; + bool log_all; + bool strict_logging; + char *ssl_CA_cert; + char *ssl_client_cert; + char *ssl_client_key; + amqp_connection_state_t conn; /**The connection object*/ + amqp_socket_t* sock; /**The currently active socket*/ + amqp_channel_t channel; /**The current channel in use*/ + int conn_stat; /**state of the connection to the server*/ + int rconn_intv; /**delay for reconnects, in seconds*/ + time_t last_rconn; /**last reconnect attempt*/ + SPINLOCK* rconn_lock; + mqmessage* messages; + enum log_trigger_t trgtype; + SRC_TRIG* src_trg; + SHM_TRIG* shm_trg; + OBJ_TRIG* obj_trg; +} MQ_INSTANCE; + +/** + * The session structure for this MQ filter. + * This stores the downstream filter information, such that the + * filter is able to pass the query on to the next filter (or router) + * in the chain. + * + * Also holds the necessary session connection information. + * + */ +typedef struct { + char* uid; /**Unique identifier used to tag messages*/ + char* db; /**The currently active database*/ + DOWNSTREAM down; + UPSTREAM up; + SESSION* session; + bool was_query; /**True if the previous routeQuery call had valid content*/ +} MQ_SESSION; + +/** + * Implementation of the mandatory version entry point + * + * @return version string of the module + */ +char * +version() +{ + return version_str; +} + +/** + * The module initialisation routine, called when the module + * is first loaded. + */ +void +ModuleInit() +{ +} + +/** + * The module entry point routine. It is this routine that + * must populate the structure that is referred to as the + * "module object", this is a structure with the set of + * external entry points for this module. + * + * @return The module object + */ +FILTER_OBJECT * +GetModuleObject() +{ + return &MyObject; +} + +/** + * Internal function used to initialize the connection to + * the RabbitMQ server. Also used to reconnect to the server + * in case the connection fails and to redeclare exchanges + * and queues if they are lost + * + */ +static int +init_conn(MQ_INSTANCE *my_instance) +{ + int rval = 0; + int amqp_ok = AMQP_STATUS_OK; + + if(my_instance->use_ssl){ + + if((my_instance->sock = amqp_ssl_socket_new(my_instance->conn)) != NULL){ + + if((amqp_ok = amqp_ssl_socket_set_cacert(my_instance->sock,my_instance->ssl_CA_cert)) != AMQP_STATUS_OK){ + skygw_log_write(LOGFILE_ERROR, + "Error : Failed to set CA certificate: %s", amqp_error_string2(amqp_ok)); + goto cleanup; + } + if((amqp_ok = amqp_ssl_socket_set_key(my_instance->sock, + my_instance->ssl_client_cert, + my_instance->ssl_client_key)) != AMQP_STATUS_OK){ + skygw_log_write(LOGFILE_ERROR, + "Error : Failed to set client certificate and key: %s", amqp_error_string2(amqp_ok)); + goto cleanup; + } + }else{ + + amqp_ok = AMQP_STATUS_SSL_CONNECTION_FAILED; + skygw_log_write(LOGFILE_ERROR, + "Error : SSL socket creation failed."); + goto cleanup; + } + + /**SSL is not used, falling back to TCP*/ + }else if((my_instance->sock = amqp_tcp_socket_new(my_instance->conn)) == NULL){ + skygw_log_write(LOGFILE_ERROR, + "Error : TCP socket creation failed."); + goto cleanup; + + } + + /**Socket creation was successful, trying to open the socket*/ + if((amqp_ok = amqp_socket_open(my_instance->sock,my_instance->hostname,my_instance->port)) != AMQP_STATUS_OK){ + skygw_log_write(LOGFILE_ERROR, + "Error : Failed to open socket: %s", amqp_error_string2(amqp_ok)); + goto cleanup; + } + amqp_rpc_reply_t reply; + reply = amqp_login(my_instance->conn,my_instance->vhost,0,AMQP_DEFAULT_FRAME_SIZE,0,AMQP_SASL_METHOD_PLAIN,my_instance->username,my_instance->password); + if(reply.reply_type != AMQP_RESPONSE_NORMAL){ + skygw_log_write(LOGFILE_ERROR, + "Error : Login to RabbitMQ server failed."); + + goto cleanup; + } + amqp_channel_open(my_instance->conn,my_instance->channel); + reply = amqp_get_rpc_reply(my_instance->conn); + if(reply.reply_type != AMQP_RESPONSE_NORMAL){ + skygw_log_write(LOGFILE_ERROR, + "Error : Channel creation failed."); + goto cleanup; + } + + amqp_exchange_declare(my_instance->conn,my_instance->channel, + amqp_cstring_bytes(my_instance->exchange), + amqp_cstring_bytes(my_instance->exchange_type), + 0, 1, + amqp_empty_table); + + reply = amqp_get_rpc_reply(my_instance->conn); + + if(reply.reply_type != AMQP_RESPONSE_NORMAL){ + skygw_log_write(LOGFILE_ERROR, + "Error : Exchange declaration failed,trying to redeclare the exchange."); + if(reply.reply_type == AMQP_RESPONSE_SERVER_EXCEPTION){ + if(reply.reply.id == AMQP_CHANNEL_CLOSE_METHOD){ + amqp_send_method(my_instance->conn,my_instance->channel,AMQP_CHANNEL_CLOSE_OK_METHOD,NULL); + }else if(reply.reply.id == AMQP_CONNECTION_CLOSE_METHOD){ + amqp_send_method(my_instance->conn,my_instance->channel,AMQP_CONNECTION_CLOSE_OK_METHOD,NULL); + } + + my_instance->channel++; + amqp_channel_open(my_instance->conn,my_instance->channel); + + amqp_exchange_delete(my_instance->conn,my_instance->channel,amqp_cstring_bytes(my_instance->exchange),0); + amqp_exchange_declare(my_instance->conn,my_instance->channel, + amqp_cstring_bytes(my_instance->exchange), + amqp_cstring_bytes(my_instance->exchange_type), + 0, 1, + amqp_empty_table); + reply = amqp_get_rpc_reply(my_instance->conn); + } + if(reply.reply_type != AMQP_RESPONSE_NORMAL){ + skygw_log_write(LOGFILE_ERROR, + "Error : Exchange redeclaration failed."); + goto cleanup; + } + } + + if(my_instance->queue){ + + + + amqp_queue_declare(my_instance->conn,my_instance->channel, + amqp_cstring_bytes(my_instance->queue), + 0, 1, 0, 0, + amqp_empty_table); + reply = amqp_get_rpc_reply(my_instance->conn); + if(reply.reply_type != AMQP_RESPONSE_NORMAL){ + skygw_log_write(LOGFILE_ERROR, + "Error : Queue declaration failed."); + goto cleanup; + } + + + amqp_queue_bind(my_instance->conn,my_instance->channel, + amqp_cstring_bytes(my_instance->queue), + amqp_cstring_bytes(my_instance->exchange), + amqp_cstring_bytes(my_instance->key), + amqp_empty_table); + reply = amqp_get_rpc_reply(my_instance->conn); + if(reply.reply_type != AMQP_RESPONSE_NORMAL){ + skygw_log_write(LOGFILE_ERROR, + "Error : Failed to bind queue to exchange."); + goto cleanup; + } + } + rval = 1; + + cleanup: + + return rval; + +} + +/** + * Parse the provided string into an array of strings. + * The caller is responsible for freeing all the allocated memory. + * If an error occurred no memory is allocated and the size of the array is set to zero. + * @param str String to parse + * @param tok Token string containing delimiting characters + * @param szstore Address where to store the size of the array after parsing + * @return The array containing the parsed string + */ +char** parse_optstr(char* str, char* tok, int* szstore) +{ + char* tk = str; + char** arr; + int i = 0, size = 1; + while((tk = strpbrk(tk + 1,tok))){ + size++; + } + + arr = malloc(sizeof(char*)*size); + + if(arr == NULL){ + skygw_log_write(LOGFILE_ERROR, + "Error : Cannot allocate enough memory."); + *szstore = 0; + return NULL; + } + + *szstore = size; + tk = strtok(str,tok); + while(tk && i < size){ + arr[i++] = strdup(tk); + tk = strtok(NULL,tok); + } + return arr; +} + +/** + * Create an instance of the filter for a particular service + * within MaxScale. + * + * @param options The options for this filter + * + * @return The instance data for this new instance + */ +static FILTER * +createInstance(char **options, FILTER_PARAMETER **params) +{ + MQ_INSTANCE *my_instance; + int paramcount = 0, parammax = 64, i = 0, x = 0, arrsize = 0; + FILTER_PARAMETER** paramlist; + char** arr; + + if ((my_instance = calloc(1, sizeof(MQ_INSTANCE)))&& + (my_instance->rconn_lock = malloc(sizeof(SPINLOCK)))) + { + spinlock_init(my_instance->rconn_lock); + uid_gen = 0; + paramlist = malloc(sizeof(FILTER_PARAMETER*)*64); + + if((my_instance->conn = amqp_new_connection()) == NULL){ + + + return NULL; + } + my_instance->channel = 1; + my_instance->last_rconn = time(NULL); + my_instance->conn_stat = AMQP_STATUS_OK; + my_instance->rconn_intv = 1; + my_instance->port = 5672; + my_instance->trgtype = TRG_ALL; + my_instance->log_all = false; + my_instance->strict_logging = true; + + for(i = 0;params[i];i++){ + if(!strcmp(params[i]->name,"hostname")){ + my_instance->hostname = strdup(params[i]->value); + }else if(!strcmp(params[i]->name,"username")){ + my_instance->username = strdup(params[i]->value); + }else if(!strcmp(params[i]->name,"password")){ + my_instance->password = strdup(params[i]->value); + }else if(!strcmp(params[i]->name,"vhost")){ + my_instance->vhost = strdup(params[i]->value); + }else if(!strcmp(params[i]->name,"port")){ + my_instance->port = atoi(params[i]->value); + }else if(!strcmp(params[i]->name,"exchange")){ + my_instance->exchange = strdup(params[i]->value); + }else if(!strcmp(params[i]->name,"key")){ + my_instance->key = strdup(params[i]->value); + }else if(!strcmp(params[i]->name,"queue")){ + my_instance->queue = strdup(params[i]->value); + } + else if(!strcmp(params[i]->name,"ssl_client_certificate")){ + + my_instance->ssl_client_cert = strdup(params[i]->value); + + }else if(!strcmp(params[i]->name,"ssl_client_key")){ + + my_instance->ssl_client_key = strdup(params[i]->value); + }else if(!strcmp(params[i]->name,"ssl_CA_cert")){ + + my_instance->ssl_CA_cert = strdup(params[i]->value); + + }else if(!strcmp(params[i]->name,"exchange_type")){ + + my_instance->exchange_type = strdup(params[i]->value); + + }else if(!strcmp(params[i]->name,"logging_trigger")){ + + arr = parse_optstr(params[i]->value,",",&arrsize); + + for(x = 0;xtrgtype |= TRG_SOURCE; + }else if(!strcmp(arr[x],"schema")){ + my_instance->trgtype |= TRG_SCHEMA; + }else if(!strcmp(arr[x],"object")){ + my_instance->trgtype |= TRG_OBJECT; + }else if(!strcmp(arr[x],"all")){ + my_instance->trgtype = TRG_ALL; + }else{ + skygw_log_write(LOGFILE_ERROR,"Error: Unknown option for 'logging_trigger':%s.",arr[x]); + } + } + + if(arrsize > 0){ + free(arr); + } + arrsize = 0; + + + + }else if(strstr(params[i]->name,"logging_")){ + + if(paramcount < parammax){ + paramlist[paramcount] = malloc(sizeof(FILTER_PARAMETER)); + paramlist[paramcount]->name = strdup(params[i]->name); + paramlist[paramcount]->value = strdup(params[i]->value); + paramcount++; + } + + } + + } + + if(my_instance->trgtype & TRG_SOURCE){ + + my_instance->src_trg = (SRC_TRIG*)malloc(sizeof(SRC_TRIG)); + my_instance->src_trg->user = NULL; + my_instance->src_trg->host = NULL; + my_instance->src_trg->usize = 0; + my_instance->src_trg->hsize = 0; + + } + + if(my_instance->trgtype & TRG_SCHEMA){ + + my_instance->shm_trg = (SHM_TRIG*)malloc(sizeof(SHM_TRIG)); + my_instance->shm_trg->objects = NULL; + my_instance->shm_trg->size = 0; + + } + + if(my_instance->trgtype & TRG_OBJECT){ + + my_instance->obj_trg = (OBJ_TRIG*)malloc(sizeof(OBJ_TRIG)); + my_instance->obj_trg->objects = NULL; + my_instance->obj_trg->size = 0; + + } + + for(i = 0;iname,"logging_source_user")){ + + if(my_instance->src_trg){ + my_instance->src_trg->user = parse_optstr(paramlist[i]->value,",",&arrsize); + my_instance->src_trg->usize = arrsize; + arrsize = 0; + } + + }else if(!strcmp(paramlist[i]->name,"logging_source_host")){ + + if(my_instance->src_trg){ + my_instance->src_trg->host = parse_optstr(paramlist[i]->value,",",&arrsize); + my_instance->src_trg->hsize = arrsize; + arrsize = 0; + } + + }else if(!strcmp(paramlist[i]->name,"logging_schema")){ + + if(my_instance->shm_trg){ + my_instance->shm_trg->objects = parse_optstr(paramlist[i]->value,",",&arrsize); + my_instance->shm_trg->size = arrsize; + arrsize = 0; + } + + }else if(!strcmp(paramlist[i]->name,"logging_object")){ + + if(my_instance->obj_trg){ + my_instance->obj_trg->objects = parse_optstr(paramlist[i]->value,",",&arrsize); + my_instance->obj_trg->size = arrsize; + arrsize = 0; + } + + }else if(!strcmp(paramlist[i]->name,"logging_log_all")){ + if(!strcmp(paramlist[i]->value,"true")){ + my_instance->log_all = true; + } + }else if(!strcmp(paramlist[i]->name,"logging_strict")){ + if(strcmp(paramlist[i]->value,"false") == 0){ + my_instance->strict_logging = false; + } + } + free(paramlist[i]->name); + free(paramlist[i]->value); + free(paramlist[i]); + } + + free(paramlist); + + if(my_instance->hostname == NULL){ + my_instance->hostname = strdup("localhost"); + } + if(my_instance->username == NULL){ + my_instance->username = strdup("guest"); + } + if(my_instance->password == NULL){ + my_instance->password = strdup("guest"); + } + if(my_instance->vhost == NULL){ + my_instance->vhost = strdup("/"); + } + if(my_instance->exchange == NULL){ + my_instance->exchange = strdup("default_exchange"); + } + if(my_instance->key == NULL){ + my_instance->key = strdup("key"); + } + if(my_instance->exchange_type == NULL){ + my_instance->exchange_type = strdup("direct"); + } + + if(my_instance->ssl_client_cert != NULL && + my_instance->ssl_client_key != NULL && + my_instance->ssl_CA_cert != NULL){ + my_instance->use_ssl = true; + }else{ + my_instance->use_ssl = false; + } + + if(my_instance->use_ssl){ + amqp_set_initialize_ssl_library(0);/**Assume the underlying SSL library is already initialized*/ + } + + /**Connect to the server*/ + init_conn(my_instance); + + + + } + return (FILTER *)my_instance; +} + + + +/** + * Declares a persistent, non-exclusive and non-passive queue that + * auto-deletes after all the messages have been consumed. + * @param my_session MQ_SESSION instance used to declare the queue + * @param qname Name of the queue to be declared + * @return Returns 0 if an error occurred, 1 if successful + */ +int declareQueue(MQ_INSTANCE *my_instance, MQ_SESSION* my_session, char* qname) +{ + int success = 1; + amqp_rpc_reply_t reply; + + spinlock_acquire(my_instance->rconn_lock); + + amqp_queue_declare(my_instance->conn,my_instance->channel, + amqp_cstring_bytes(qname), + 0, 1, 0, 1, + amqp_empty_table); + reply = amqp_get_rpc_reply(my_instance->conn); + if(reply.reply_type != AMQP_RESPONSE_NORMAL){ + success = 0; + skygw_log_write(LOGFILE_ERROR, + "Error : Queue declaration failed."); + + } + + + amqp_queue_bind(my_instance->conn,my_instance->channel, + amqp_cstring_bytes(qname), + amqp_cstring_bytes(my_instance->exchange), + amqp_cstring_bytes(my_session->uid), + amqp_empty_table); + reply = amqp_get_rpc_reply(my_instance->conn); + if(reply.reply_type != AMQP_RESPONSE_NORMAL){ + success = 0; + skygw_log_write(LOGFILE_ERROR, + "Error : Failed to bind queue to exchange."); + + } + spinlock_release(my_instance->rconn_lock); + return success; +} + +/** + * Broadcasts a message on the message stack to the RabbitMQ server + * and frees the allocated memory if successful. + * @return AMQP_STATUS_OK if the broadcasting was successful + */ +int sendMessage(MQ_INSTANCE *instance) +{ + int err_code; + mqmessage *tmp; + + if(instance->conn_stat != AMQP_STATUS_OK){ + + if(difftime(time(NULL),instance->last_rconn) > instance->rconn_intv){ + + instance->last_rconn = time(NULL); + + if(init_conn(instance)){ + instance->rconn_intv = 1.0; + instance->conn_stat = AMQP_STATUS_OK; + + }else{ + instance->rconn_intv += 5.0; + skygw_log_write(LOGFILE_ERROR, + "Error : Failed to reconnect to the MQRabbit server "); + } + } + } + + if(instance->messages){ + instance->conn_stat = amqp_basic_publish(instance->conn,instance->channel, + amqp_cstring_bytes(instance->exchange), + amqp_cstring_bytes(instance->key), + 0,0,instance->messages->prop,amqp_cstring_bytes(instance->messages->msg)); + + + /**Message was sent successfully*/ + if(instance->conn_stat == AMQP_STATUS_OK){ + tmp = instance->messages; + instance->messages = instance->messages->next; + free(tmp->prop); + free(tmp->msg); + free(tmp); + } + + } + + err_code = instance->conn_stat; + + return err_code; +} + + +/** + * Push a new message on the stack to be broadcasted later. + * The message assumes ownership of the memory allocated to the message content and properties. + * @param prop Message properties + * @param msg Message content + */ +void pushMessage(MQ_INSTANCE *instance, amqp_basic_properties_t* prop, char* msg) +{ + spinlock_acquire(instance->rconn_lock); + + mqmessage* newmsg = malloc(sizeof(mqmessage)); + if(newmsg){ + newmsg->msg = msg; + newmsg->prop = prop; + newmsg->next = NULL; + + if(instance->messages){ + newmsg->next = instance->messages; + } + + instance->messages = newmsg; + + }else{ + skygw_log_write(LOGFILE_ERROR, + "Error : Cannot allocate enough memory."); + free(prop); + free(msg); + } + + while(instance->messages){ + if(sendMessage(instance) != AMQP_STATUS_OK){ + break; + } + } + + spinlock_release(instance->rconn_lock); +} + + + +/** + * Associate a new session with this instance of the filter and opens + * a connection to the server and prepares the exchange and the queue for use. + * + * + * @param instance The filter instance data + * @param session The session itself + * @return Session specific data for this session + */ +static void * +newSession(FILTER *instance, SESSION *session) +{ + MQ_SESSION *my_session; + MYSQL_session* sessauth; + + if ((my_session = calloc(1, sizeof(MQ_SESSION))) != NULL){ + + my_session->was_query = false; + my_session->uid = NULL; + my_session->session = session; + sessauth = my_session->session->data; + if(sessauth->db && strnlen(sessauth->db,128)>0){ + my_session->db = strdup(sessauth->db); + }else{ + my_session->db = NULL; + } + + } + + return my_session; +} + + + +/** + * Close a session with the filter, this is the mechanism + * by which a filter may cleanup data structure etc. + * In the case of the MQ filter we do nothing. + * + * @param instance The filter instance data + * @param session The session being closed + */ +static void +closeSession(FILTER *instance, void *session) +{ +} + +/** + * Free the memory associated with the session + * + * @param instance The filter instance + * @param session The filter session + */ +static void +freeSession(FILTER *instance, void *session) +{ + MQ_SESSION *my_session = (MQ_SESSION *)session; + free(my_session->uid); + free(my_session->db); + free(my_session); + return; +} + +/** + * Set the downstream filter or router to which queries will be + * passed from this filter. + * + * @param instance The filter instance data + * @param session The filter session + * @param downstream The downstream filter or router. + */ +static void +setDownstream(FILTER *instance, void *session, DOWNSTREAM *downstream) +{ + MQ_SESSION *my_session = (MQ_SESSION *)session; + my_session->down = *downstream; +} + +static void setUpstream(FILTER *instance, void *session, UPSTREAM *upstream) +{ + MQ_SESSION *my_session = (MQ_SESSION *)session; + my_session->up = *upstream; +} + + +/** + * Generates a unique key using a number of unique unsigned integers. + * @param array The array that is used + * @param size Size of the array + */ +void genkey(char* array, int size) +{ + int i = 0; + for(i = 0;istart + 4)) == 0x02){ + if(my_session->db){ + free(my_session->db); + } + plen = pktlen(queue->start); + my_session->db = calloc(plen,sizeof(char)); + memcpy(my_session->db,queue->start + 5,plen - 1); + } + + if(modutil_is_SQL(queue)){ + + /**Parse the query*/ + + if (!query_is_parsed(queue)){ + success = parse_query(queue); + } + + if(!success){ + skygw_log_write(LOGFILE_ERROR,"Error: Parsing query failed."); + goto send_downstream; + } + + if(!my_instance->log_all){ + if(!skygw_is_real_query(queue)){ + goto send_downstream; + } + } + + if(my_instance->trgtype == TRG_ALL){ + skygw_log_write_flush(LOGFILE_TRACE,"Trigger is TRG_ALL"); + schema_ok = true; + src_ok = true; + obj_ok = true; + goto validate_triggers; + } + + if(my_instance->trgtype & TRG_SOURCE && my_instance->src_trg){ + + if(session_isvalid(my_session->session)){ + + sessusr = session_getUser(my_session->session); + sesshost = session_get_remote(my_session->session); + + /**Username was configured*/ + if(my_instance->src_trg->usize > 0){ + for(i = 0;isrc_trg->usize;i++){ + + if(strcmp(my_instance->src_trg->user[i],sessusr) == 0) + { + skygw_log_write_flush(LOGFILE_TRACE,"Trigger is TRG_SOURCE: user: %s = %s",my_instance->src_trg->user[i],sessusr); + src_ok = true; + break; + } + + } + + + } + + /**If username was not matched, try to match hostname*/ + + if(!src_ok && my_instance->src_trg->hsize > 0){ + + for(i = 0;isrc_trg->hsize;i++){ + + if(strcmp(my_instance->src_trg->host[i],sesshost) == 0) + { + skygw_log_write_flush(LOGFILE_TRACE,"Trigger is TRG_SOURCE: host: %s = %s",my_instance->src_trg->host[i],sesshost); + src_ok = true; + break; + } + + } + + } + + } + + if(src_ok && !my_instance->strict_logging){ + schema_ok = true; + obj_ok = true; + goto validate_triggers; + } + + }else{ + src_ok = true; + } + + + + if(my_instance->trgtype & TRG_SCHEMA && my_instance->shm_trg){ + int tbsz = 0,z; + char** tblnames = skygw_get_table_names(queue,&tbsz,true); + char* tmp; + bool all_remotes = true; + + for(z = 0;zshm_trg->size; i++){ + + if(strcmp(tmp,my_instance->shm_trg->objects[i]) == 0){ + + skygw_log_write_flush(LOGFILE_TRACE,"Trigger is TRG_SCHEMA: %s = %s",tmp,my_instance->shm_trg->objects[i]); + + schema_ok = true; + break; + } + } + }else{ + all_remotes = false; + } + free(tblnames[z]); + } + free(tblnames); + + if(!schema_ok && !all_remotes && my_session->db && strlen(my_session->db)>0){ + + for(i = 0; ishm_trg->size; i++){ + + if(strcmp(my_session->db,my_instance->shm_trg->objects[i]) == 0){ + + skygw_log_write_flush(LOGFILE_TRACE,"Trigger is TRG_SCHEMA: %s = %s",my_session->db,my_instance->shm_trg->objects[i]); + + schema_ok = true; + break; + } + } + } + + if(schema_ok && !my_instance->strict_logging){ + src_ok = true; + obj_ok = true; + goto validate_triggers; + } + + }else{ + schema_ok = true; + } + + + if(my_instance->trgtype & TRG_OBJECT && my_instance->obj_trg){ + + sesstbls = skygw_get_table_names(queue,&dbcount,false); + + for(j = 0; jobj_trg->size; i++){ + + + if(!strcmp(tbnm,my_instance->obj_trg->objects[i])){ + obj_ok = true; + skygw_log_write_flush(LOGFILE_TRACE,"Trigger is TRG_OBJECT: %s = %s",my_instance->obj_trg->objects[i],sesstbls[j]); + break; + } + + } + + } + if(dbcount > 0){ + for(j = 0; jstrict_logging){ + src_ok = true; + schema_ok = true; + goto validate_triggers; + } + + }else{ + obj_ok = true; + } + + + validate_triggers: + + if(src_ok&&schema_ok&&obj_ok){ + + /** + * Something matched the trigger, log the query + */ + + skygw_log_write_flush(LOGFILE_TRACE,"Routing message to: %s:%d %s as %s/%s, exchange: %s<%s> key:%s queue:%s", + my_instance->hostname,my_instance->port, + my_instance->vhost,my_instance->username, + my_instance->password,my_instance->exchange, + my_instance->exchange_type,my_instance->key, + my_instance->queue); + + if(my_session->uid == NULL){ + + my_session->uid = calloc(33,sizeof(char)); + + if(!my_session->uid){ + skygw_log_write(LOGFILE_ERROR,"Error : Out of memory."); + }else{ + genkey(my_session->uid,32); + } + + } + + + if(modutil_extract_SQL(queue, &ptr, &length)){ + + my_session->was_query = true; + + if((prop = malloc(sizeof(amqp_basic_properties_t)))){ + prop->_flags = AMQP_BASIC_CONTENT_TYPE_FLAG | + AMQP_BASIC_DELIVERY_MODE_FLAG | + AMQP_BASIC_MESSAGE_ID_FLAG | + AMQP_BASIC_CORRELATION_ID_FLAG; + prop->content_type = amqp_cstring_bytes("text/plain"); + prop->delivery_mode = AMQP_DELIVERY_PERSISTENT; + prop->correlation_id = amqp_cstring_bytes(my_session->uid); + prop->message_id = amqp_cstring_bytes("query"); + } + + + + if(success){ + + /**Try to convert to a canonical form and use the plain query if unsuccessful*/ + if((canon_q = skygw_get_canonical(queue)) == NULL){ + skygw_log_write_flush(LOGFILE_ERROR, + "Error: Cannot form canonical query."); + } + + } + + memset(t_buf,0,128); + sprintf(t_buf, "%lu|",(unsigned long)time(NULL)); + + int qlen = strnlen(canon_q,length) + strnlen(t_buf,128); + if((combined = malloc((qlen+1)*sizeof(char))) == NULL){ + skygw_log_write_flush(LOGFILE_ERROR, + "Error: Out of memory"); + } + strcpy(combined,t_buf); + strncat(combined,canon_q,length); + + pushMessage(my_instance,prop,combined); + free(canon_q); + } + + } + + /** Pass the query downstream */ + } + send_downstream: + return my_session->down.routeQuery(my_session->down.instance, + my_session->down.session, queue); +} + +/** + * Converts a length-encoded integer to an unsigned integer as defined by the + * MySQL manual. + * @param c Pointer to the first byte of a length-encoded integer + * @return The value converted to a standard unsigned integer + */ +unsigned int leitoi(unsigned char* c) +{ + unsigned char* ptr = c; + unsigned int sz = *ptr; + if(*ptr < 0xfb) return sz; + if(*ptr == 0xfc){ + sz = *++ptr; + sz += (*++ptr << 8); + }else if(*ptr == 0xfd){ + sz = *++ptr; + sz += (*++ptr << 8); + sz += (*++ptr << 8); + }else{ + sz = *++ptr; + sz += (*++ptr << 8); + sz += (*++ptr << 8); + sz += (*++ptr << 8); + sz += (*++ptr << 8); + sz += (*++ptr << 8); + sz += (*++ptr << 8); + sz += (*++ptr << 8); + sz += (*++ptr << 8); + } + return sz; +} + +/** + * Converts a length-encoded integer into a standard unsigned integer + * and advances the pointer to the next unrelated byte. + * + * @param c Pointer to the first byte of a length-encoded integer + */ +unsigned int consume_leitoi(unsigned char** c) +{ + unsigned int rval = leitoi(*c); + if(**c == 0xfc){ + *c += 3; + }else if(**c == 0xfd){ + *c += 4; + }else if(**c == 0xfe){ + *c += 9; + }else{ + *c += 1; + } + return rval; +} + +/** + * Converts length-encoded strings to character strings and advanced the pointer to the next unrelated byte. + * The caller is responsible for freeing the allocated memory. + * @param c Pointer to the first byte of a valid packet. + * @return The newly allocated string or NULL of an error occurred + */ +char* consume_lestr(unsigned char** c) +{ + unsigned int slen = consume_leitoi(c); + char *str = calloc((slen + 1), sizeof(char)); + if(str){ + memcpy(str,*c,slen); + *c += slen; + } + return str; +} + +/** + *Checks whether the packet is an EOF packet. + * @param p Pointer to the first byte of a packet + * @return 1 if the packet is an EOF packet and 0 if it is not + */ +unsigned int is_eof(void* p) +{ + unsigned char* ptr = (unsigned char*) p; + return *(ptr) == 0x05 && *(ptr + 1) == 0x00 && *(ptr + 2) == 0x00 && *(ptr + 4) == 0xfe; +} + + +/** + * The clientReply entry point. This is passed the response buffer + * to which the filter should be applied. Once processed the + * query is passed to the upstream component + * (filter or router) in the filter chain. + * + * The function tries to extract a SQL query response out of the response buffer, + * adds a timestamp to it and publishes the resulting string on the exchange. + * The message is tagged with the same identifier that the query was. + * + * @param instance The filter instance data + * @param session The filter session + * @param reply The response data + */ +static int clientReply(FILTER* instance, void *session, GWBUF *reply) +{ + MQ_SESSION *my_session = (MQ_SESSION *)session; + MQ_INSTANCE *my_instance = (MQ_INSTANCE *)instance; + char t_buf[128],*combined; + unsigned int pkt_len = pktlen(reply->sbuf->data), offset = 0; + amqp_basic_properties_t *prop; + + if (my_session->was_query){ + + int packet_ok = 0, was_last = 0; + + my_session->was_query = false; + + if(pkt_len > 0){ + if((prop = malloc(sizeof(amqp_basic_properties_t)))){ + prop->_flags = AMQP_BASIC_CONTENT_TYPE_FLAG | + AMQP_BASIC_DELIVERY_MODE_FLAG | + AMQP_BASIC_MESSAGE_ID_FLAG | + AMQP_BASIC_CORRELATION_ID_FLAG; + prop->content_type = amqp_cstring_bytes("text/plain"); + prop->delivery_mode = AMQP_DELIVERY_PERSISTENT; + prop->correlation_id = amqp_cstring_bytes(my_session->uid); + prop->message_id = amqp_cstring_bytes("reply"); + } + if(!(combined = calloc(GWBUF_LENGTH(reply) + 256,sizeof(char)))){ + skygw_log_write_flush(LOGFILE_ERROR, + "Error : Out of memory"); + } + + memset(t_buf,0,128); + sprintf(t_buf,"%lu|",(unsigned long)time(NULL)); + + + memcpy(combined + offset,t_buf,strnlen(t_buf,40)); + offset += strnlen(t_buf,40); + + if(*(reply->sbuf->data + 4) == 0x00){ /**OK packet*/ + unsigned int aff_rows = 0, l_id = 0, s_flg = 0, wrn = 0; + unsigned char *ptr = (unsigned char*)(reply->sbuf->data + 5); + pkt_len = pktlen(reply->sbuf->data); + aff_rows = consume_leitoi(&ptr); + l_id = consume_leitoi(&ptr); + s_flg |= *ptr++; + s_flg |= (*ptr++ << 8); + wrn |= *ptr++; + wrn |= (*ptr++ << 8); + sprintf(combined + offset,"OK - affected_rows: %d " + " last_insert_id: %d " + " status_flags: %#0x " + " warnings: %d ", + aff_rows,l_id,s_flg,wrn); + offset += strnlen(combined,GWBUF_LENGTH(reply) + 256) - offset; + + if(pkt_len > 7){ + int plen = consume_leitoi(&ptr); + if(plen > 0){ + sprintf(combined + offset," message: %.*s\n",plen,ptr); + } + } + + packet_ok = 1; + was_last = 1; + + }else if(*(reply->sbuf->data + 4) == 0xff){ /**ERR packet*/ + + sprintf(combined + offset,"ERROR - message: %.*s", + (int)(reply->end - ((void*)(reply->sbuf->data + 13))), + (char *)reply->sbuf->data + 13); + packet_ok = 1; + was_last = 1; + + }else if(*(reply->sbuf->data + 4) == 0xfb){ /**LOCAL_INFILE request packet*/ + + unsigned char *rset = (unsigned char*)reply->sbuf->data; + strcpy(combined + offset,"LOCAL_INFILE: "); + strncat(combined + offset,(const char*)rset+5,pktlen(rset)); + packet_ok = 1; + was_last = 1; + + }else{ /**Result set*/ + + unsigned char *rset = (unsigned char*)(reply->sbuf->data + 4); + char *tmp; + unsigned int col_cnt = consume_leitoi(&rset); + + tmp = calloc(256,sizeof(char)); + sprintf(tmp,"Columns: %d",col_cnt); + memcpy(combined + offset,tmp,strnlen(tmp,256)); + offset += strnlen(tmp,256); + memcpy(combined + offset,"\n",1); + offset++; + free(tmp); + + packet_ok = 1; + was_last = 1; + + } + if(packet_ok){ + + pushMessage(my_instance,prop,combined); + + if(was_last){ + + /**Successful reply received and sent, releasing uid*/ + + free(my_session->uid); + my_session->uid = NULL; + + } + } + } + + } + + return my_session->up.clientReply(my_session->up.instance, + my_session->up.session, reply); +} + +/** + * Diagnostics routine + * + * Prints the connection details and the names of the exchange, + * queue and the routing key. + * + * @param instance The filter instance + * @param fsession Filter session, may be NULL + * @param dcb The DCB for diagnostic output + */ +static void +diagnostic(FILTER *instance, void *fsession, DCB *dcb) +{ + MQ_INSTANCE *my_instance = (MQ_INSTANCE *)instance; + + if (my_instance) + { + dcb_printf(dcb, "\t\tConnecting to %s:%d as %s/%s.\nVhost: %s\tExchange: %s\tKey: %s\tQueue: %s\n", + my_instance->hostname,my_instance->port, + my_instance->username,my_instance->password, + my_instance->vhost, my_instance->exchange, + my_instance->key, my_instance->queue + ); + } +} + diff --git a/server/modules/include/blr.h b/server/modules/include/blr.h new file mode 100644 index 000000000..f493ec715 --- /dev/null +++ b/server/modules/include/blr.h @@ -0,0 +1,367 @@ +#ifndef _BLR_H +#define _BLR_H +/* + * This file is distributed as part of MaxScale. It is free + * software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, + * version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright SkySQL Ab 2014 + */ + +/** + * @file blr.h - The binlog router header file + * + * @verbatim + * Revision History + * + * Date Who Description + * 02/04/14 Mark Riddoch Initial implementation + * + * @endverbatim + */ +#include +#include +#include + +#define BINLOG_FNAMELEN 16 +#define BLR_PROTOCOL "MySQLBackend" +#define BINLOG_MAGIC { 0xfe, 0x62, 0x69, 0x6e } +#define BINLOG_NAMEFMT "%s.%06d" +#define BINLOG_NAME_ROOT "mysql-bin" + +/** + * High and Low water marks for the slave dcb. These values can be overriden + * by the router options highwater and lowwater. + */ +#define DEF_LOW_WATER 20000 +#define DEF_HIGH_WATER 300000 + +/** + * Some useful macros for examining the MySQL Response packets + */ +#define MYSQL_RESPONSE_OK(buf) (*((uint8_t *)GWBUF_DATA(buf) + 4) == 0x00) +#define MYSQL_RESPONSE_EOF(buf) (*((uint8_t *)GWBUF_DATA(buf) + 4) == 0xfe) +#define MYSQL_RESPONSE_ERR(buf) (*((uint8_t *)GWBUF_DATA(buf) + 4) == 0xff) +#define MYSQL_ERROR_CODE(buf) (*((uint8_t *)GWBUF_DATA(buf) + 5)) +#define MYSQL_ERROR_MSG(buf) ((uint8_t *)GWBUF_DATA(buf) + 6) +#define MYSQL_COMMAND(buf) (*((uint8_t *)GWBUF_DATA(buf) + 4)) + +/** + * Slave statistics + */ +typedef struct { + int n_events; /*< Number of events sent */ + int n_bursts; /*< Number of bursts sent */ + int n_requests; /*< Number of requests received */ + int n_flows; /*< Number of flow control restarts */ + int n_catchupnr; /*< No. of times catchup resulted in not entering loop */ + int n_alreadyupd; + int n_upd; + int n_cb; + int n_cbna; + int n_dcb; + int n_above; + int n_failed_read; + int n_overrun; + int n_actions[3]; +} SLAVE_STATS; + +/** + * The client session structure used within this router. This represents + * the slaves that are replicating binlogs from MaxScale. + */ +typedef struct router_slave { +#if defined(SS_DEBUG) + skygw_chk_t rses_chk_top; +#endif + DCB *dcb; /*< The slave server DCB */ + int state; /*< The state of this slave */ + int binlog_pos; /*< Binlog position for this slave */ + char binlogfile[BINLOG_FNAMELEN+1]; + /*< Current binlog file for this slave */ + int serverid; /*< Server-id of the slave */ + char *hostname; /*< Hostname of the slave, if known */ + char *user; /*< Username if given */ + char *passwd; /*< Password if given */ + short port; /*< MySQL port */ + int nocrc; /*< Disable CRC */ + int overrun; + uint32_t rank; /*< Replication rank */ + uint8_t seqno; /*< Replication dump sequence no */ + SPINLOCK catch_lock; /*< Event catchup lock */ + unsigned int cstate; /*< Catch up state */ + SPINLOCK rses_lock; /*< Protects rses_deleted */ + pthread_t pthread; + struct router_instance + *router; /*< Pointer to the owning router */ + struct router_slave *next; + SLAVE_STATS stats; /*< Slave statistics */ +#if defined(SS_DEBUG) + skygw_chk_t rses_chk_tail; +#endif +} ROUTER_SLAVE; + + +/** + * The statistics for this router instance + */ +typedef struct { + int n_slaves; /*< Number slave sessions created */ + int n_reads; /*< Number of record reads */ + uint64_t n_binlogs; /*< Number of binlog records from master */ + uint64_t n_binlog_errors;/*< Number of binlog records from master */ + uint64_t n_rotates; /*< Number of binlog rotate events */ + uint64_t n_cachehits; /*< Number of hits on the binlog cache */ + uint64_t n_cachemisses; /*< Number of misses on the binlog cache */ + int n_registered; /*< Number of registered slaves */ + int n_masterstarts; /*< Number of times connection restarted */ + int n_delayedreconnects; + int n_residuals; /*< Number of times residual data was buffered */ + int n_heartbeats; /*< Number of heartbeat messages */ + time_t lastReply; + uint64_t n_fakeevents; /*< Fake events not written to disk */ + uint64_t n_artificial; /*< Artificial events not written to disk */ + uint64_t events[0x24]; /*< Per event counters */ +} ROUTER_STATS; + +/** + * Saved responses from the master that will be forwarded to slaves + */ +typedef struct { + GWBUF *server_id; /*< Master server id */ + GWBUF *heartbeat; /*< Heartbeat period */ + GWBUF *chksum1; /*< Binlog checksum 1st response */ + GWBUF *chksum2; /*< Binlog checksum 2nd response */ + GWBUF *gtid_mode; /*< GTID Mode response */ + GWBUF *uuid; /*< Master UUID */ + GWBUF *setslaveuuid; /*< Set Slave UUID */ + GWBUF *setnames; /*< Set NAMES latin1 */ + GWBUF *utf8; /*< Set NAMES utf8 */ + GWBUF *select1; /*< select 1 */ + GWBUF *selectver; /*< select version() */ + uint8_t *fde_event; /*< Format Description Event */ + int fde_len; /*< Length of fde_event */ +} MASTER_RESPONSES; + +/** + * The binlog record structure. This contains the actual packet received from the + * master, the binlog position of the data in the packet, a point to the data and + * the length of the binlog record. + * + * This allows requests for binlog records in the cache to be serviced by simply + * sending the exact same packet as was received by MaxScale from the master. + * Items are written to the backing file as soon as they are received. The binlog + * cache is flushed of old records periodically, releasing the GWBUF's back to the + * free memory pool. + */ +typedef struct { + unsigned long position; /*< binlog record position for this cache entry */ + GWBUF *pkt; /*< The packet received from the master */ + unsigned char *data; /*< Pointer to the data within the packet */ + unsigned int record_len; /*< Binlog record length */ +} BLCACHE_RECORD; + +/** + * The binlog cache. A cache exists for each file that hold cached bin log records. + * Typically the router will hold two binlog caches, one for the current file and one + * for the previous file. + */ +typedef struct { + char filename[BINLOG_FNAMELEN+1]; + BLCACHE_RECORD *first; + BLCACHE_RECORD *current; + int cnt; +} BLCACHE; + + +/** + * The per instance data for the router. + */ +typedef struct router_instance { + SERVICE *service; /*< Pointer to the service using this router */ + ROUTER_SLAVE *slaves; /*< Link list of all the slave connections */ + SPINLOCK lock; /*< Spinlock for the instance data */ + char *uuid; /*< UUID for the router to use w/master */ + int masterid; /*< Server ID of the master */ + int serverid; /*< Server ID to use with master */ + char *user; /*< User name to use with master */ + char *password; /*< Password to use with master */ + char *fileroot; /*< Root of binlog filename */ + DCB *master; /*< DCB for master connection */ + DCB *client; /*< DCB for dummy client */ + SESSION *session; /*< Fake session for master connection */ + unsigned int master_state; /*< State of the master FSM */ + uint8_t lastEventReceived; + GWBUF *residual; /*< Any residual binlog event */ + MASTER_RESPONSES saved_master; /*< Saved master responses */ + char binlog_name[BINLOG_FNAMELEN+1]; + /*< Name of the current binlog file */ + uint64_t binlog_position; + /*< Current binlog position */ + int binlog_fd; /*< File descriptor of the binlog + * file being written + */ + unsigned int low_water; /*< Low water mark for client DCB */ + unsigned int high_water; /*< High water mark for client DCB */ + BLCACHE *cache[2]; + ROUTER_STATS stats; /*< Statistics for this router */ + int active_logs; + int reconnect_pending; + int handling_threads; + struct router_instance + *next; +} ROUTER_INSTANCE; + +/** + * Packet header for replication messages + */ +typedef struct rep_header { + int payload_len; /*< Payload length (24 bits) */ + uint8_t seqno; /*< Response sequence number */ + uint8_t ok; /*< OK Byte from packet */ + uint32_t timestamp; /*< Timestamp - start of binlog record */ + uint8_t event_type; /*< Binlog event type */ + uint32_t serverid; /*< Server id of master */ + uint32_t event_size; /*< Size of header, post-header and body */ + uint32_t next_pos; /*< Position of next event */ + uint16_t flags; /*< Event flags */ +} REP_HEADER; + +/** + * State machine for the master to MaxScale replication + */ +#define BLRM_UNCONNECTED 0x0000 +#define BLRM_AUTHENTICATED 0x0001 +#define BLRM_TIMESTAMP 0x0002 +#define BLRM_SERVERID 0x0003 +#define BLRM_HBPERIOD 0x0004 +#define BLRM_CHKSUM1 0x0005 +#define BLRM_CHKSUM2 0x0006 +#define BLRM_GTIDMODE 0x0007 +#define BLRM_MUUID 0x0008 +#define BLRM_SUUID 0x0009 +#define BLRM_LATIN1 0x000A +#define BLRM_UTF8 0x000B +#define BLRM_SELECT1 0x000C +#define BLRM_SELECTVER 0x000D +#define BLRM_REGISTER 0x000E +#define BLRM_BINLOGDUMP 0x000F + +#define BLRM_MAXSTATE 0x000F + +static char *blrm_states[] = { "Unconnected", "Authenticated", "Timestamp retrieval", + "Server ID retrieval", "HeartBeat Period setup", "binlog checksum config", + "binlog checksum rerieval", "GTID Mode retrieval", "Master UUID retrieval", + "Set Slave UUID", "Set Names latin1", "Set Names utf8", "select 1", + "select version()", "Register slave", "Binlog Dump" }; + +#define BLRS_CREATED 0x0000 +#define BLRS_UNREGISTERED 0x0001 +#define BLRS_REGISTERED 0x0002 +#define BLRS_DUMPING 0x0003 + +#define BLRS_MAXSTATE 0x0003 + +static char *blrs_states[] = { "Created", "Unregistered", "Registered", + "Sending binlogs" }; + +/** + * Slave catch-up status + */ +#define CS_READING 0x0001 +#define CS_INNERLOOP 0x0002 +#define CS_UPTODATE 0x0004 +#define CS_EXPECTCB 0x0008 +#define CS_DIST 0x0010 +#define CS_DISTLATCH 0x0020 + +/** + * MySQL protocol OpCodes needed for replication + */ +#define COM_QUIT 0x01 +#define COM_QUERY 0x03 +#define COM_REGISTER_SLAVE 0x15 +#define COM_BINLOG_DUMP 0x12 + +/** + * Binlog event types + */ +#define START_EVENT_V3 0x01 +#define QUERY_EVENT 0x02 +#define STOP_EVENT 0x03 +#define ROTATE_EVENT 0x04 +#define INTVAR_EVENT 0x05 +#define LOAD_EVENT 0x06 +#define SLAVE_EVENT 0x07 +#define CREATE_FILE_EVENT 0x08 +#define APPEND_BLOCK_EVENT 0x09 +#define EXEC_LOAD_EVENT 0x0A +#define DELETE_FILE_EVENT 0x0B +#define NEW_LOAD_EVENT 0x0C +#define RAND_EVENT 0x0D +#define USER_VAR_EVENT 0x0E +#define FORMAT_DESCRIPTION_EVENT 0x0F +#define XID_EVENT 0x10 +#define BEGIN_LOAD_QUERY_EVENT 0x11 +#define EXECUTE_LOAD_QUERY_EVENT 0x12 +#define TABLE_MAP_EVENT 0x13 +#define WRITE_ROWS_EVENTv0 0x14 +#define UPDATE_ROWS_EVENTv0 0x15 +#define DELETE_ROWS_EVENTv0 0x16 +#define WRITE_ROWS_EVENTv1 0x17 +#define UPDATE_ROWS_EVENTv1 0x18 +#define DELETE_ROWS_EVENTv1 0x19 +#define INCIDENT_EVENT 0x1A +#define HEARTBEAT_EVENT 0x1B +#define IGNORABLE_EVENT 0x1C +#define ROWS_QUERY_EVENT 0x1D +#define WRITE_ROWS_EVENTv2 0x1E +#define UPDATE_ROWS_EVENTv2 0x1F +#define DELETE_ROWS_EVENTv2 0x20 +#define GTID_EVENT 0x21 +#define ANONYMOUS_GTID_EVENT 0x22 +#define PREVIOUS_GTIDS_EVENT 0x23 + +/** + * Binlog event flags + */ +#define LOG_EVENT_BINLOG_IN_USE_F 0x0001 +#define LOG_EVENT_FORCED_ROTATE_F 0x0002 +#define LOG_EVENT_THREAD_SPECIFIC_F 0x0004 +#define LOG_EVENT_SUPPRESS_USE_F 0x0008 +#define LOG_EVENT_UPDATE_TABLE_MAP_VERSION_F 0x0010 +#define LOG_EVENT_ARTIFICIAL_F 0x0020 +#define LOG_EVENT_RELAY_LOG_F 0x0040 +#define LOG_EVENT_IGNORABLE_F 0x0080 +#define LOG_EVENT_NO_FILTER_F 0x0100 +#define LOG_EVENT_MTS_ISOLATE_F 0x0200 + +/* + * Externals within the router + */ +extern void blr_start_master(ROUTER_INSTANCE *); +extern void blr_master_response(ROUTER_INSTANCE *, GWBUF *); +extern void blr_master_reconnect(ROUTER_INSTANCE *); + +extern int blr_slave_request(ROUTER_INSTANCE *, ROUTER_SLAVE *, GWBUF *); +extern void blr_slave_rotate(ROUTER_SLAVE *slave, uint8_t *ptr); +extern int blr_slave_catchup(ROUTER_INSTANCE *router, ROUTER_SLAVE *slave); +extern void blr_init_cache(ROUTER_INSTANCE *); + +extern void blr_file_init(ROUTER_INSTANCE *); +extern int blr_open_binlog(ROUTER_INSTANCE *, char *); +extern void blr_write_binlog_record(ROUTER_INSTANCE *, REP_HEADER *,uint8_t *); +extern void blr_file_rotate(ROUTER_INSTANCE *, char *, uint64_t); +extern void blr_file_flush(ROUTER_INSTANCE *); +extern GWBUF *blr_read_binlog(int, unsigned int, REP_HEADER *); +#endif diff --git a/server/modules/include/mysql_client_server_protocol.h b/server/modules/include/mysql_client_server_protocol.h index c875a1232..cb2e41cd7 100644 --- a/server/modules/include/mysql_client_server_protocol.h +++ b/server/modules/include/mysql_client_server_protocol.h @@ -219,7 +219,7 @@ typedef enum mysql_server_cmd { MYSQL_COM_QUERY, MYSQL_COM_FIELD_LIST, MYSQL_COM_CREATE_DB, - MYSQL_COM_DROP_DB, + MYSQL_COM_DROP_DB, MYSQL_COM_REFRESH, MYSQL_COM_SHUTDOWN, MYSQL_COM_STATISTICS, diff --git a/server/modules/include/mysqlhint.h b/server/modules/include/mysqlhint.h new file mode 100644 index 000000000..3e936b847 --- /dev/null +++ b/server/modules/include/mysqlhint.h @@ -0,0 +1,114 @@ +#ifndef _MYSQLHINT_H +#define _MYSQLHINT_H +/* + * This file is distributed as part of the SkySQL Gateway. It is free + * software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, + * version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright SkySQL Ab 2013 + */ + +/* + * Revision History + * + * Date Who Description + * 17-07-2014 Mark Riddoch Initial implementation + */ +#include + +/* Parser tokens for the hint parser */ +typedef enum { + TOK_MAXSCALE = 1, + TOK_PREPARE, + TOK_START, + TOK_STOP, + TOK_EQUAL, + TOK_STRING, + TOK_ROUTE, + TOK_TO, + TOK_MASTER, + TOK_SLAVE, + TOK_SERVER, + TOK_EOL +} TOKEN_VALUE; + +/* The tokenising return type */ +typedef struct { + TOKEN_VALUE token; // The token itself + char *value; // The string version of the token +} HINT_TOKEN; + +/** + * A named hint set. + * + * The hint "MaxScale name PREPARE ..." can be used to defined a named set + * of hints that can be later applied. + */ +typedef struct namedhints { + char *name; /*< Hintsets name */ + HINT *hints; + struct namedhints + *next; /*< Next named hint */ +} NAMEDHINTS; + +/** + * A session meaintains a stack of hints, the hints BEGIN and STOP are used + * push hints on and off the stack. The current top of the stack is added to + * any statement that does not explicitly define a hint for that signle + * statement. + */ +typedef struct hintstack { + HINT *hint; + struct hintstack + *next; +} HINTSTACK; + +/** + * The hint instance structure + */ +typedef struct { + int sessions; +} HINT_INSTANCE; + +/** + * A hint parser session structure + */ +typedef struct { + DOWNSTREAM down; + GWBUF *request; + int query_len; + HINTSTACK *stack; + NAMEDHINTS *named_hints; /* The named hints defined in this session */ +} HINT_SESSION; + +/* Some useful macros */ +#define CURRENT_HINT(session) ((session)->stack ? \ + (session)->stack->hints : NULL) + +/* Hint Parser State Machine */ +#define HS_INIT 0 +#define HS_ROUTE 1 +#define HS_ROUTE1 2 +#define HS_ROUTE_SERVER 3 +#define HS_NAME 4 +#define HS_PVALUE 5 +#define HS_PREPARE 6 + + +extern HINT *hint_parser(HINT_SESSION *session, GWBUF *request); +NAMEDHINTS* free_named_hint(NAMEDHINTS* named_hint); +HINTSTACK* free_hint_stack(HINTSTACK* hint_stack); + + + +#endif diff --git a/server/modules/include/readwritesplit.h b/server/modules/include/readwritesplit.h index 6fc639005..5b17dbc7d 100644 --- a/server/modules/include/readwritesplit.h +++ b/server/modules/include/readwritesplit.h @@ -67,10 +67,26 @@ typedef enum backend_type_t { BE_UNDEFINED=-1, BE_MASTER, BE_JOINED = BE_MASTER, - BE_SLAVE, + BE_SLAVE, BE_COUNT } backend_type_t; +struct router_instance; + +typedef enum { + TARGET_MASTER = 0x01, + TARGET_SLAVE = 0x02, + TARGET_NAMED_SERVER = 0x04, + TARGET_ALL = 0x08, + TARGET_RLAG_MAX = 0x10 +} route_target_t; + +#define TARGET_IS_MASTER(t) (t & TARGET_MASTER) +#define TARGET_IS_SLAVE(t) (t & TARGET_SLAVE) +#define TARGET_IS_NAMED_SERVER(t) (t & TARGET_NAMED_SERVER) +#define TARGET_IS_ALL(t) (t & TARGET_ALL) +#define TARGET_IS_RLAG_MAX(t) (t & TARGET_RLAG_MAX) + typedef struct rses_property_st rses_property_t; typedef struct router_client_session ROUTER_CLIENT_SES; @@ -78,7 +94,8 @@ typedef enum rses_property_type_t { RSES_PROP_TYPE_UNDEFINED=-1, RSES_PROP_TYPE_SESCMD=0, RSES_PROP_TYPE_FIRST = RSES_PROP_TYPE_SESCMD, - RSES_PROP_TYPE_LAST=RSES_PROP_TYPE_SESCMD, + RSES_PROP_TYPE_TMPTABLES, + RSES_PROP_TYPE_LAST=RSES_PROP_TYPE_TMPTABLES, RSES_PROP_TYPE_COUNT=RSES_PROP_TYPE_LAST+1 } rses_property_type_t; @@ -103,6 +120,7 @@ typedef enum select_criteria { /** default values for rwsplit configuration parameters */ #define CONFIG_MAX_SLAVE_CONN 1 #define CONFIG_MAX_SLAVE_RLAG -1 /*< not used */ +#define CONFIG_SQL_VARIABLES_IN TYPE_ALL #define GET_SELECT_CRITERIA(s) \ (strncmp(s,"LEAST_GLOBAL_CONNECTIONS", strlen("LEAST_GLOBAL_CONNECTIONS")) == 0 ? \ @@ -143,7 +161,7 @@ struct rses_property_st { rses_property_type_t rses_prop_type; union rses_prop_data { mysql_sescmd_t sescmd; - void* placeholder; /*< to be removed due new type */ + HASHTABLE* temp_tables; } rses_prop_data; rses_property_t* rses_prop_next; /*< next property of same type */ #if defined(SS_DEBUG) @@ -217,6 +235,7 @@ typedef struct rwsplit_config_st { int rw_max_slave_conn_count; select_criteria_t rw_slave_select_criteria; int rw_max_slave_replication_lag; + target_t rw_use_sql_variables_in; } rwsplit_config_t; @@ -261,6 +280,7 @@ struct router_client_session { #if defined(PREP_STMT_CACHING) HASHTABLE* rses_prep_stmt[2]; #endif + struct router_instance *router; /*< The router instance */ struct router_client_session* next; #if defined(SS_DEBUG) skygw_chk_t rses_chk_tail; @@ -294,6 +314,8 @@ typedef struct router_instance { unsigned int bitvalue; /*< Required value of server->status */ ROUTER_STATS stats; /*< Statistics for this router */ struct router_instance* next; /*< Next router on the list */ + bool available_slaves; + /*< The router has some slaves avialable */ } ROUTER_INSTANCE; #define BACKEND_TYPE(b) (SERVER_IS_MASTER((b)->backend_server) ? BE_MASTER : \ diff --git a/server/modules/monitor/Makefile b/server/modules/monitor/Makefile index 8b60824ea..0f3bc5867 100644 --- a/server/modules/monitor/Makefile +++ b/server/modules/monitor/Makefile @@ -17,6 +17,7 @@ # Revision History # Date Who Description # 08/07/13 Mark Riddoch Initial implementation +# 28/07/14 Massimiliano Pinto new monitor ndbcluster added include ../../../build_gateway.inc include ../../../makefile.inc @@ -38,11 +39,13 @@ MYSQLSRCS=mysql_mon.c MYSQLOBJ=$(MYSQLSRCS:.c=.o) GALERASRCS=galera_mon.c GALERAOBJ=$(GALERASRCS:.c=.o) -SRCS=$(MYSQLSRCS) +NDBCLUSTERSRCS=ndbcluster_mon.c +NDBCLUSTEROBJ=$(NDBCLUSTERSRCS:.c=.o) +SRCS=$(MYSQLSRCS) $(GALERASRCS) $(NDBCLUSTERSRCS) OBJ=$(SRCS:.c=.o) LIBS=$(UTILSPATH)/skygw_utils.o -llog_manager \ -L$(EMBEDDED_LIB) -lmysqld -MODULES=libmysqlmon.so libgaleramon.so +MODULES=libmysqlmon.so libgaleramon.so libndbclustermon.so all: $(MODULES) @@ -53,6 +56,9 @@ libmysqlmon.so: $(MYSQLOBJ) libgaleramon.so: $(GALERAOBJ) $(CC) $(LDFLAGS) $(GALERAOBJ) $(LIBS) -o $@ +libndbclustermon.so: $(NDBCLUSTEROBJ) + $(CC) $(LDFLAGS) $(NDBCLUSTEROBJ) $(LIBS) -o $@ + .c.o: $(CC) $(CFLAGS) $< -o $@ diff --git a/server/modules/monitor/galera_mon.c b/server/modules/monitor/galera_mon.c index 3cba61325..211407f86 100644 --- a/server/modules/monitor/galera_mon.c +++ b/server/modules/monitor/galera_mon.c @@ -69,7 +69,7 @@ static void defaultUsers(void *, char *, char *); static void diagnostics(DCB *, void *); static void setInterval(void *, unsigned long); -static MONITOR_OBJECT MyObject = { startMonitor, stopMonitor, registerServer, unregisterServer, defaultUsers, diagnostics, setInterval, NULL, NULL }; +static MONITOR_OBJECT MyObject = { startMonitor, stopMonitor, registerServer, unregisterServer, defaultUsers, diagnostics, setInterval, NULL, NULL, NULL }; /** * Implementation of the mandatory version entry point @@ -335,10 +335,18 @@ char *server_string; database->server->port, mysql_error(database->con)))); server_clear_status(database->server, SERVER_RUNNING); + if (mysql_errno(database->con) == ER_ACCESS_DENIED_ERROR) + { + server_set_status(database->server, SERVER_AUTH_ERROR); + } database->server->node_id = -1; free(dpwd); return; } + else + { + server_clear_status(database->server, SERVER_AUTH_ERROR); + } free(dpwd); } @@ -351,7 +359,9 @@ char *server_string; /* get server version string */ server_string = (char *)mysql_get_server_info(database->con); if (server_string) { - database->server->server_string = strdup(server_string); + database->server->server_string = realloc(database->server->server_string, strlen(server_string)+1); + if (database->server->server_string) + strcpy(database->server->server_string, server_string); } /* Check if the the Galera FSM shows this node is joined to the cluster */ diff --git a/server/modules/monitor/mysql_mon.c b/server/modules/monitor/mysql_mon.c index ee5b4eeb7..976aa315d 100644 --- a/server/modules/monitor/mysql_mon.c +++ b/server/modules/monitor/mysql_mon.c @@ -40,6 +40,9 @@ * the status to update in server status field before * starting the replication consistency check. * This will also give routers a consistent "status" of all servers + * 28/08/14 Massimiliano Pinto Added detectStaleMaster feature: previous detected master will be used again, even if the replication is stopped. + * This means both IO and SQL threads are not working on slaves. + * This option is not enabled by default. * * @endverbatim */ @@ -62,7 +65,7 @@ extern int lm_enabled_logfiles_bitmask; static void monitorMain(void *); -static char *version_str = "V1.2.0"; +static char *version_str = "V1.3.0"; MODULE_INFO info = { MODULE_API_MONITOR, @@ -80,6 +83,7 @@ static void diagnostics(DCB *, void *); static void setInterval(void *, unsigned long); static void defaultId(void *, unsigned long); static void replicationHeartbeat(void *, int); +static void detectStaleMaster(void *, int); static bool mon_status_changed(MONITOR_SERVERS* mon_srv); static bool mon_print_fail_status(MONITOR_SERVERS* mon_srv); static MONITOR_SERVERS *getServerByNodeId(MONITOR_SERVERS *, long); @@ -91,7 +95,7 @@ static int add_slave_to_master(long *, int, long); static void monitor_set_pending_status(MONITOR_SERVERS *, int); static void monitor_clear_pending_status(MONITOR_SERVERS *, int); -static MONITOR_OBJECT MyObject = { startMonitor, stopMonitor, registerServer, unregisterServer, defaultUser, diagnostics, setInterval, defaultId, replicationHeartbeat }; +static MONITOR_OBJECT MyObject = { startMonitor, stopMonitor, registerServer, unregisterServer, defaultUser, diagnostics, setInterval, defaultId, replicationHeartbeat, detectStaleMaster }; /** * Implementation of the mandatory version entry point @@ -160,6 +164,7 @@ MYSQL_MONITOR *handle; handle->id = MONITOR_DEFAULT_ID; handle->interval = MONITOR_INTERVAL; handle->replicationHeartbeat = 0; + handle->detectStaleMaster = 0; handle->master = NULL; spinlock_init(&handle->lock); } @@ -306,6 +311,7 @@ char *sep; dcb_printf(dcb,"\tSampling interval:\t%lu milliseconds\n", handle->interval); dcb_printf(dcb,"\tMaxScale MonitorId:\t%lu\n", handle->id); dcb_printf(dcb,"\tReplication lag:\t%s\n", (handle->replicationHeartbeat == 1) ? "enabled" : "disabled"); + dcb_printf(dcb,"\tDetect Stale Master:\t%s\n", (handle->detectStaleMaster == 1) ? "enabled" : "disabled"); dcb_printf(dcb, "\tMonitored servers: "); db = handle->databases; @@ -394,6 +400,11 @@ char *server_string; * Store server NOT running in server and monitor server pending struct * */ + if (mysql_errno(database->con) == ER_ACCESS_DENIED_ERROR) + { + server_set_status(database->server, SERVER_AUTH_ERROR); + monitor_set_pending_status(database, SERVER_AUTH_ERROR); + } server_clear_status(database->server, SERVER_RUNNING); monitor_clear_pending_status(database, SERVER_RUNNING); @@ -403,8 +414,19 @@ char *server_string; monitor_clear_pending_status(database, SERVER_SLAVE); monitor_clear_pending_status(database, SERVER_MASTER); + /* Clean addition status too */ + server_clear_status(database->server, SERVER_SLAVE_OF_EXTERNAL_MASTER); + server_clear_status(database->server, SERVER_STALE_STATUS); + monitor_clear_pending_status(database, SERVER_SLAVE_OF_EXTERNAL_MASTER); + monitor_clear_pending_status(database, SERVER_STALE_STATUS); + return; } + else + { + server_clear_status(database->server, SERVER_AUTH_ERROR); + monitor_clear_pending_status(database, SERVER_AUTH_ERROR); + } free(dpwd); } /* Store current status in both server and monitor server pending struct */ @@ -417,7 +439,9 @@ char *server_string; /* get server version string */ server_string = (char *)mysql_get_server_info(database->con); if (server_string) { - database->server->server_string = strdup(server_string); + database->server->server_string = realloc(database->server->server_string, strlen(server_string)+1); + if (database->server->server_string) + strcpy(database->server->server_string, server_string); } /* get server_id form current node */ @@ -458,12 +482,20 @@ char *server_string; if (strncmp(row[12], "Yes", 3) == 0 && strncmp(row[13], "Yes", 3) == 0) { isslave += 1; - + } + + /* If Slave_IO_Running = Yes, assign the master_id to current server: this allows building + * the replication tree, slaves ids will be added to master(s) and we will have at least the + * root master server. + * Please note, there could be no slaves at all if Slave_SQL_Running == 'No' + */ + if (strncmp(row[12], "Yes", 3) == 0) { /* get Master_Server_Id values */ master_id = atol(row[41]); if (master_id == 0) master_id = -1; } + i++; } /* store master_id of current node */ @@ -489,7 +521,14 @@ char *server_string; if (strncmp(row[10], "Yes", 3) == 0 && strncmp(row[11], "Yes", 3) == 0) { isslave = 1; + } + /* If Slave_IO_Running = Yes, assign the master_id to current server: this allows building + * the replication tree, slaves ids will be added to master(s) and we will have at least the + * root master server. + * Please note, there could be no slaves at all if Slave_SQL_Running == 'No' + */ + if (strncmp(row[10], "Yes", 3) == 0) { /* get Master_Server_Id values */ master_id = atol(row[39]); if (master_id == 0) @@ -505,6 +544,7 @@ char *server_string; /* Remove addition info */ monitor_clear_pending_status(database, SERVER_SLAVE_OF_EXTERNAL_MASTER); + monitor_clear_pending_status(database, SERVER_STALE_STATUS); /* Please note, the MASTER status and SERVER_SLAVE_OF_EXTERNAL_MASTER * will be assigned in the monitorMain() via get_replication_tree() routine @@ -534,6 +574,7 @@ monitorMain(void *arg) MYSQL_MONITOR *handle = (MYSQL_MONITOR *)arg; MONITOR_SERVERS *ptr; int replication_heartbeat = handle->replicationHeartbeat; +int detect_stale_master = handle->detectStaleMaster; int num_servers=0; MONITOR_SERVERS *root_master; @@ -545,6 +586,7 @@ MONITOR_SERVERS *root_master; "module. Exiting.\n"))); return; } + handle->status = MONITOR_RUNNING; while (1) { @@ -616,10 +658,19 @@ MONITOR_SERVERS *root_master; while (ptr) { if (! SERVER_IN_MAINT(ptr->server)) { - ptr->server->status = ptr->pending_status; + /* If "detect_stale_master" option is On, let's use the previus master */ + if (detect_stale_master && root_master && (!strcmp(ptr->server->name, root_master->server->name) && ptr->server->port == root_master->server->port) && (ptr->server->status & SERVER_MASTER) && !(ptr->pending_status & SERVER_MASTER)) { + /* in this case server->status will not be updated from pending_status */ + LOGIF(LM, (skygw_log_write_flush( + LOGFILE_MESSAGE, "[mysql_mon]: root server [%s:%i] is no longer Master, let's use it again even if it could be a stale master, you have been warned!", ptr->server->name, ptr->server->port))); + /* Set the STALE bit for this server in server struct */ + server_set_status(ptr->server, SERVER_STALE_STATUS); + } else { + ptr->server->status = ptr->pending_status; + } } - ptr = ptr->next; - } + ptr = ptr->next; + } /* Do now the heartbeat replication set/get for MySQL Replication Consistency */ if (replication_heartbeat && root_master && (SERVER_IS_MASTER(root_master->server) || SERVER_IS_RELAY_SERVER(root_master->server))) { @@ -665,19 +716,34 @@ setInterval(void *arg, unsigned long interval) { MYSQL_MONITOR *handle = (MYSQL_MONITOR *)arg; memcpy(&handle->interval, &interval, sizeof(unsigned long)); - } +} /** * Enable/Disable the MySQL Replication hearbeat, detecting slave lag behind master. * - * @param arg The handle allocated by startMonitor - * @param replicationHeartbeat To enable it 1, disable it with 0 + * @param arg The handle allocated by startMonitor + * @param enable To enable it 1, disable it with 0 */ static void -replicationHeartbeat(void *arg, int replicationHeartbeat) +replicationHeartbeat(void *arg, int enable) { MYSQL_MONITOR *handle = (MYSQL_MONITOR *)arg; - memcpy(&handle->replicationHeartbeat, &replicationHeartbeat, sizeof(int)); + memcpy(&handle->replicationHeartbeat, &enable, sizeof(int)); +} + +/** + * Enable/Disable the MySQL Replication Stale Master dectection, allowing a previouvsly detected master to still act as a Master. + * This option must be enabled in order to keep the Master when the replication is stopped or removed from slaves. + * If the replication is still stopped when MaxSclale is restarted no Master will be available. + * + * @param arg The handle allocated by startMonitor + * @param enable To enable it 1, disable it with 0 + */ +static void +detectStaleMaster(void *arg, int enable) +{ +MYSQL_MONITOR *handle = (MYSQL_MONITOR *)arg; + memcpy(&handle->detectStaleMaster, &enable, sizeof(int)); } static bool mon_status_changed( @@ -1038,6 +1104,10 @@ static MONITOR_SERVERS *get_replication_tree(MYSQL_MONITOR *handle, int num_serv monitor_set_pending_status(master, SERVER_MASTER); } else { if (current->master_id > 0) { + /* this server is slave of another server not in MaxScale configuration + * we cannot use it as a real slave. + */ + monitor_clear_pending_status(ptr, SERVER_SLAVE); monitor_set_pending_status(ptr, SERVER_SLAVE_OF_EXTERNAL_MASTER); } } diff --git a/server/modules/monitor/mysqlmon.h b/server/modules/monitor/mysqlmon.h index 0e06db6e4..eb2d37bcd 100644 --- a/server/modules/monitor/mysqlmon.h +++ b/server/modules/monitor/mysqlmon.h @@ -32,6 +32,7 @@ * 26/05/14 Massimiliano Pinto Default values for MONITOR_INTERVAL * 28/05/14 Massimiliano Pinto Addition of new fields in MYSQL_MONITOR struct * 24/06/14 Massimiliano Pinto Addition of master field in MYSQL_MONITOR struct and MONITOR_MAX_NUM_SLAVES + * 28/08/14 Massimiliano Pinto Addition of detectStaleMaster * * @endverbatim */ @@ -43,9 +44,9 @@ typedef struct monitor_servers { SERVER *server; /**< The server being monitored */ MYSQL *con; /**< The MySQL connection */ - int mon_err_count; - unsigned int mon_prev_status; - unsigned int pending_status; /**< Pending Status flag bitmap */ + int mon_err_count; + unsigned int mon_prev_status; + unsigned int pending_status; /**< Pending Status flag bitmap */ struct monitor_servers *next; /**< The next server in the list */ } MONITOR_SERVERS; @@ -54,17 +55,18 @@ typedef struct monitor_servers { * The handle for an instance of a MySQL Monitor module */ typedef struct { - SPINLOCK lock; /**< The monitor spinlock */ - pthread_t tid; /**< id of monitor thread */ - int shutdown; /**< Flag to shutdown the monitor thread */ - int status; /**< Monitor status */ - char *defaultUser; /**< Default username for monitoring */ - char *defaultPasswd; /**< Default password for monitoring */ - unsigned long interval; /**< Monitor sampling interval */ - unsigned long id; /**< Monitor ID */ + SPINLOCK lock; /**< The monitor spinlock */ + pthread_t tid; /**< id of monitor thread */ + int shutdown; /**< Flag to shutdown the monitor thread */ + int status; /**< Monitor status */ + char *defaultUser; /**< Default username for monitoring */ + char *defaultPasswd; /**< Default password for monitoring */ + unsigned long interval; /**< Monitor sampling interval */ + unsigned long id; /**< Monitor ID */ int replicationHeartbeat; /**< Monitor flag for MySQL replication heartbeat */ - MONITOR_SERVERS *master; /**< Master server for MySQL Master/Slave replication */ - MONITOR_SERVERS *databases; /**< Linked list of servers to monitor */ + int detectStaleMaster; /**< Monitor flag for MySQL replication Stale Master detection */ + MONITOR_SERVERS *master; /**< Master server for MySQL Master/Slave replication */ + MONITOR_SERVERS *databases; /**< Linked list of servers to monitor */ } MYSQL_MONITOR; #define MONITOR_RUNNING 1 diff --git a/server/modules/monitor/ndbcluster_mon.c b/server/modules/monitor/ndbcluster_mon.c new file mode 100644 index 000000000..840e30691 --- /dev/null +++ b/server/modules/monitor/ndbcluster_mon.c @@ -0,0 +1,471 @@ +/* + * This file is distributed as part of the SkySQL Gateway. It is free + * software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, + * version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright SkySQL Ab 2013 + */ + +/** + * @file ndbcluster_mon.c - A MySQL cluster SQL node monitor + * + * @verbatim + * Revision History + * + * Date Who Description + * 25/07/14 Massimiliano Pinto Initial implementation + * + * @endverbatim + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern int lm_enabled_logfiles_bitmask; + +static void monitorMain(void *); + +static char *version_str = "V1.0.0"; + +MODULE_INFO info = { + MODULE_API_MONITOR, + MODULE_BETA_RELEASE, + MONITOR_VERSION, + "A MySQL cluster SQL node monitor" +}; + +static void *startMonitor(void *); +static void stopMonitor(void *); +static void registerServer(void *, SERVER *); +static void unregisterServer(void *, SERVER *); +static void defaultUsers(void *, char *, char *); +static void diagnostics(DCB *, void *); +static void setInterval(void *, unsigned long); + +static MONITOR_OBJECT MyObject = { startMonitor, stopMonitor, registerServer, unregisterServer, defaultUsers, diagnostics, setInterval, NULL, NULL, NULL }; + +/** + * Implementation of the mandatory version entry point + * + * @return version string of the module + */ +char * +version() +{ + return version_str; +} + +/** + * The module initialisation routine, called when the module + * is first loaded. + */ +void +ModuleInit() +{ + LOGIF(LM, (skygw_log_write( + LOGFILE_MESSAGE, + "Initialise the MySQL Cluster Monitor module %s.\n", + version_str))); +} + +/** + * The module entry point routine. It is this routine that + * must populate the structure that is referred to as the + * "module object", this is a structure with the set of + * external entry points for this module. + * + * @return The module object + */ +MONITOR_OBJECT * +GetModuleObject() +{ + return &MyObject; +} + +/** + * Start the instance of the monitor, returning a handle on the monitor. + * + * This function creates a thread to execute the actual monitoring. + * + * @return A handle to use when interacting with the monitor + */ +static void * +startMonitor(void *arg) +{ +MYSQL_MONITOR *handle; + + if (arg != NULL) + { + handle = (MYSQL_MONITOR *)arg; + handle->shutdown = 0; + } + else + { + if ((handle = (MYSQL_MONITOR *)malloc(sizeof(MYSQL_MONITOR))) == NULL) + return NULL; + handle->databases = NULL; + handle->shutdown = 0; + handle->defaultUser = NULL; + handle->defaultPasswd = NULL; + handle->id = MONITOR_DEFAULT_ID; + handle->interval = MONITOR_INTERVAL; + spinlock_init(&handle->lock); + } + handle->tid = (THREAD)thread_start(monitorMain, handle); + return handle; +} + +/** + * Stop a running monitor + * + * @param arg Handle on thr running monior + */ +static void +stopMonitor(void *arg) +{ +MYSQL_MONITOR *handle = (MYSQL_MONITOR *)arg; + + handle->shutdown = 1; + thread_wait((void *)handle->tid); +} + +/** + * Register a server that must be added to the monitored servers for + * a monitoring module. + * + * @param arg A handle on the running monitor module + * @param server The server to add + */ +static void +registerServer(void *arg, SERVER *server) +{ +MYSQL_MONITOR *handle = (MYSQL_MONITOR *)arg; +MONITOR_SERVERS *ptr, *db; + + if ((db = (MONITOR_SERVERS *)malloc(sizeof(MONITOR_SERVERS))) == NULL) + return; + db->server = server; + db->con = NULL; + db->next = NULL; + spinlock_acquire(&handle->lock); + if (handle->databases == NULL) + handle->databases = db; + else + { + ptr = handle->databases; + while (ptr->next != NULL) + ptr = ptr->next; + ptr->next = db; + } + spinlock_release(&handle->lock); +} + +/** + * Remove a server from those being monitored by a monitoring module + * + * @param arg A handle on the running monitor module + * @param server The server to remove + */ +static void +unregisterServer(void *arg, SERVER *server) +{ +MYSQL_MONITOR *handle = (MYSQL_MONITOR *)arg; +MONITOR_SERVERS *ptr, *lptr; + + spinlock_acquire(&handle->lock); + if (handle->databases == NULL) + { + spinlock_release(&handle->lock); + return; + } + if (handle->databases->server == server) + { + ptr = handle->databases; + handle->databases = handle->databases->next; + free(ptr); + } + else + { + ptr = handle->databases; + while (ptr->next != NULL && ptr->next->server != server) + ptr = ptr->next; + if (ptr->next) + { + lptr = ptr->next; + ptr->next = ptr->next->next; + free(lptr); + } + } + spinlock_release(&handle->lock); +} + +/** + * Diagnostic interface + * + * @param dcb DCB to send output + * @param arg The monitor handle + */ +static void +diagnostics(DCB *dcb, void *arg) +{ +MYSQL_MONITOR *handle = (MYSQL_MONITOR *)arg; +MONITOR_SERVERS *db; +char *sep; + + switch (handle->status) + { + case MONITOR_RUNNING: + dcb_printf(dcb, "\tMonitor running\n"); + break; + case MONITOR_STOPPING: + dcb_printf(dcb, "\tMonitor stopping\n"); + break; + case MONITOR_STOPPED: + dcb_printf(dcb, "\tMonitor stopped\n"); + break; + } + + dcb_printf(dcb,"\tSampling interval:\t%lu milliseconds\n", handle->interval); + dcb_printf(dcb, "\tMonitored servers: "); + + db = handle->databases; + sep = ""; + while (db) + { + dcb_printf(dcb, "%s%s:%d", sep, db->server->name, db->server->port); + sep = ", "; + db = db->next; + } + dcb_printf(dcb, "\n"); +} + +/** + * Set the default username and password to use to monitor if the server does not + * override this. + * + * @param arg The handle allocated by startMonitor + * @param uname The default user name + * @param passwd The default password + */ +static void +defaultUsers(void *arg, char *uname, char *passwd) +{ +MYSQL_MONITOR *handle = (MYSQL_MONITOR *)arg; + + if (handle->defaultUser) + free(handle->defaultUser); + if (handle->defaultPasswd) + free(handle->defaultPasswd); + handle->defaultUser = strdup(uname); + handle->defaultPasswd = strdup(passwd); +} + +/** + * Monitor an individual server + * + * @param database The database to probe + */ +static void +monitorDatabase(MONITOR_SERVERS *database, char *defaultUser, char *defaultPasswd) +{ +MYSQL_ROW row; +MYSQL_RES *result; +int num_fields; +int isjoined = 0; +char *uname = defaultUser, *passwd = defaultPasswd; +unsigned long int server_version = 0; +char *server_string; + + if (database->server->monuser != NULL) + { + uname = database->server->monuser; + passwd = database->server->monpw; + } + if (uname == NULL) + return; + + /* Don't even probe server flagged as in maintenance */ + if (SERVER_IN_MAINT(database->server)) + return; + + if (database->con == NULL || mysql_ping(database->con) != 0) + { + char *dpwd = decryptPassword(passwd); + int rc; + int read_timeout = 1; + + database->con = mysql_init(NULL); + rc = mysql_options(database->con, MYSQL_OPT_READ_TIMEOUT, (void *)&read_timeout); + + if (mysql_real_connect(database->con, database->server->name, + uname, dpwd, NULL, database->server->port, NULL, 0) == NULL) + { + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Error : Monitor was unable to connect to " + "server %s:%d : \"%s\"", + database->server->name, + database->server->port, + mysql_error(database->con)))); + server_clear_status(database->server, SERVER_RUNNING); + if (mysql_errno(database->con) == ER_ACCESS_DENIED_ERROR) + { + server_set_status(database->server, SERVER_AUTH_ERROR); + } + database->server->node_id = -1; + free(dpwd); + return; + } + else + { + server_clear_status(database->server, SERVER_AUTH_ERROR); + } + free(dpwd); + } + + /* If we get this far then we have a working connection */ + server_set_status(database->server, SERVER_RUNNING); + + /* get server version from current server */ + server_version = mysql_get_server_version(database->con); + + /* get server version string */ + server_string = (char *)mysql_get_server_info(database->con); + if (server_string) { + database->server->server_string = realloc(database->server->server_string, strlen(server_string)+1); + if (database->server->server_string) + strcpy(database->server->server_string, server_string); + } + + /* Check if the the SQL node is able to contact one or more data nodes */ + if (mysql_query(database->con, "SHOW STATUS LIKE 'Ndb_number_of_ready_data_nodes'") == 0 + && (result = mysql_store_result(database->con)) != NULL) + { + num_fields = mysql_num_fields(result); + while ((row = mysql_fetch_row(result))) + { + if (atoi(row[1]) > 0) + isjoined = 1; + } + mysql_free_result(result); + } + + /* Check the the SQL node id in the MySQL cluster */ + if (mysql_query(database->con, "SHOW STATUS LIKE 'Ndb_cluster_node_id'") == 0 + && (result = mysql_store_result(database->con)) != NULL) + { + long cluster_node_id = -1; + num_fields = mysql_num_fields(result); + while ((row = mysql_fetch_row(result))) + { + cluster_node_id = strtol(row[1], NULL, 10); + if ((errno == ERANGE && (cluster_node_id == LONG_MAX + || cluster_node_id == LONG_MIN)) || (errno != 0 && cluster_node_id == 0)) + { + cluster_node_id = -1; + } + database->server->node_id = cluster_node_id; + } + mysql_free_result(result); + } + + if (isjoined) { + server_set_status(database->server, SERVER_NDB); + database->server->depth = 0; + } else { + server_clear_status(database->server, SERVER_NDB); + database->server->depth = -1; + } +} + +/** + * The entry point for the monitoring module thread + * + * @param arg The handle of the monitor + */ +static void +monitorMain(void *arg) +{ +MYSQL_MONITOR *handle = (MYSQL_MONITOR *)arg; +MONITOR_SERVERS *ptr; +long master_id; + + if (mysql_thread_init()) + { + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Fatal : mysql_thread_init failed in monitor " + "module. Exiting.\n"))); + return; + } + handle->status = MONITOR_RUNNING; + while (1) + { + master_id = -1; + + if (handle->shutdown) + { + handle->status = MONITOR_STOPPING; + mysql_thread_end(); + handle->status = MONITOR_STOPPED; + return; + } + + ptr = handle->databases; + + while (ptr) + { + unsigned int prev_status = ptr->server->status; + monitorDatabase(ptr, handle->defaultUser, handle->defaultPasswd); + + if (ptr->server->status != prev_status || + SERVER_IS_DOWN(ptr->server)) + { + LOGIF(LM, (skygw_log_write_flush( + LOGFILE_MESSAGE, + "Backend server %s:%d state : %s", + ptr->server->name, + ptr->server->port, + STRSRVSTATUS(ptr->server)))); + } + + ptr = ptr->next; + } + + thread_millisleep(handle->interval); + } +} + +/** + * Set the monitor sampling interval. + * + * @param arg The handle allocated by startMonitor + * @param interval The interval to set in monitor struct, in milliseconds + */ +static void +setInterval(void *arg, unsigned long interval) +{ +MYSQL_MONITOR *handle = (MYSQL_MONITOR *)arg; + memcpy(&handle->interval, &interval, sizeof(unsigned long)); +} diff --git a/server/modules/protocol/maxscaled.c b/server/modules/protocol/maxscaled.c index 738c78111..f580764f8 100644 --- a/server/modules/protocol/maxscaled.c +++ b/server/modules/protocol/maxscaled.c @@ -235,6 +235,7 @@ maxscaled_error(DCB *dcb) static int maxscaled_hangup(DCB *dcb) { + dcb_close(dcb); return 0; } @@ -313,9 +314,11 @@ maxscaled_close(DCB *dcb) MAXSCALED *maxscaled = dcb->protocol; if (maxscaled && maxscaled->username) + { free(maxscaled->username); + maxscaled->username = NULL; + } - dcb_close(dcb); return 0; } diff --git a/server/modules/protocol/mysql_backend.c b/server/modules/protocol/mysql_backend.c index ce7b6ef97..ca365066e 100644 --- a/server/modules/protocol/mysql_backend.c +++ b/server/modules/protocol/mysql_backend.c @@ -422,7 +422,6 @@ static int gw_read_backend_event(DCB *dcb) { GWBUF *read_buffer = NULL; ROUTER_OBJECT *router = NULL; ROUTER *router_instance = NULL; - void *rsession = NULL; SESSION *session = dcb->session; int nbytes_read = 0; @@ -497,7 +496,7 @@ static int gw_read_backend_event(DCB *dcb) { { if (nbytes_read < 5) { - gwbuf_append(dcb->dcb_readqueue, read_buffer); + dcb->dcb_readqueue = gwbuf_append(dcb->dcb_readqueue, read_buffer); rc = 0; goto return_rc; } diff --git a/server/modules/protocol/mysql_client.c b/server/modules/protocol/mysql_client.c index 6ffe4e56f..3c8a70bd9 100644 --- a/server/modules/protocol/mysql_client.c +++ b/server/modules/protocol/mysql_client.c @@ -34,6 +34,7 @@ * 28/02/2014 Massimiliano Pinto Added: client IPv4 in dcb->ipv4 and inet_ntop for string representation * 11/03/2014 Massimiliano Pinto Added: Unix socket support * 07/05/2014 Massimiliano Pinto Added: specific version string in server handshake + * 09/09/2014 Massimiliano Pinto Added: 777 permission for socket path * */ #include @@ -798,7 +799,7 @@ int gw_read_client_event( } /** succeed */ - if (rc == 1) { + if (rc) { rc = 0; /**< here '0' means success */ } else { GWBUF* errbuf; @@ -985,6 +986,16 @@ int gw_MySQLListener( return 0; } + + /* set permission for all users */ + if (chmod(config_bind, 0777) < 0) { + fprintf(stderr, + "\n* chmod failed for %s due error %i, %s.\n\n", + config_bind, + errno, + strerror(errno)); + } + break; case AF_INET: @@ -1420,7 +1431,6 @@ static int route_by_statement( int rc = -1; GWBUF* packetbuf; #if defined(SS_DEBUG) - gwbuf_type_t prevtype; GWBUF* tmpbuf; tmpbuf = *p_readbuf; diff --git a/server/modules/routing/Makefile b/server/modules/routing/Makefile index 8287bdaea..637d5f41c 100644 --- a/server/modules/routing/Makefile +++ b/server/modules/routing/Makefile @@ -51,6 +51,7 @@ MODULES= libdebugcli.so libreadconnroute.so libtestroute.so libcli.so all: $(MODULES) + (cd readwritesplit; make) libtestroute.so: $(TESTOBJ) $(CC) $(LDFLAGS) $(TESTOBJ) $(LIBS) -o $@ @@ -88,19 +89,15 @@ install: $(MODULES) (cd readwritesplit; make DEST=$(DEST) install) cleantests: - $(MAKE) -C readwritesplit/test cleantests $(MAKE) -C test cleantests - + buildtests: - $(MAKE) -C readwritesplit/test DEBUG=Y buildtests $(MAKE) -C test DEBUG=Y buildtests - + runtests: $(MAKE) -C test runtests - $(MAKE) -C readwritesplit runtests - + testall: $(MAKE) -C test testall - $(MAKE) -C readwritesplit testall - + include depend.mk diff --git a/server/modules/routing/binlog/Makefile b/server/modules/routing/binlog/Makefile new file mode 100644 index 000000000..6e9282ea1 --- /dev/null +++ b/server/modules/routing/binlog/Makefile @@ -0,0 +1,65 @@ +# This file is distributed as part of the SkySQL Gateway. It is free +# software: you can redistribute it and/or modify it under the terms of the +# GNU General Public License as published by the Free Software Foundation, +# version 2. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Copyright SkySQL Ab 2013 +# +# Revision History +# Date Who Description +# 2/04/14 Mark Riddoch Initial framework put in place + +include ../../../../build_gateway.inc + +LOGPATH := $(ROOT_PATH)/log_manager +UTILSPATH := $(ROOT_PATH)/utils +QCLASSPATH := $(ROOT_PATH)/query_classifier + +CC=cc +CFLAGS=-c -fPIC -I/usr/include -I../../include -I../../../include \ + -I$(LOGPATH) -I$(UTILSPATH) -I$(QCLASSPATH) \ + $(MYSQL_HEADERS) -Wall -g + +include ../../../../makefile.inc + +LDFLAGS=-shared -L$(LOGPATH) -L$(QCLASSPATH) -L$(EMBEDDED_LIB) \ + -Wl,-rpath,$(DEST)/lib \ + -Wl,-rpath,$(LOGPATH) -Wl,-rpath,$(UTILSPATH) -Wl,-rpath,$(QCLASSPATH) \ + -Wl,-rpath,$(EMBEDDED_LIB) + +SRCS=blr.c blr_master.c blr_cache.c blr_slave.c blr_file.c +OBJ=$(SRCS:.c=.o) +LIBS=-lssl -pthread -llog_manager -lmysqld +MODULES=libbinlogrouter.so + +all: $(MODULES) + +$(MODULES): $(OBJ) + $(CC) $(LDFLAGS) $(OBJ) $(UTILSPATH)/skygw_utils.o $(LIBS) -o $@ + +.c.o: + $(CC) $(CFLAGS) $< -o $@ + +clean: + rm -f $(OBJ) $(MODULES) + +tags: + ctags $(SRCS) $(HDRS) + +depend: + @rm -f depend.mk + cc -M $(CFLAGS) $(SRCS) > depend.mk + +install: $(MODULES) + install -D $(MODULES) $(DEST)/MaxScale/modules + +include depend.mk diff --git a/server/modules/routing/binlog/README b/server/modules/routing/binlog/README new file mode 100644 index 000000000..514b48341 --- /dev/null +++ b/server/modules/routing/binlog/README @@ -0,0 +1,53 @@ +The binlog router is not a "normal" MaxScale router, it is not +designed to be used to route client requests to a database in the +usual proxy fashion. Rather it is designed to allow MaxScale to be +used as a relay server in a MySQL replication environment. + +In this environment MaxScale sits between a master MySQL server and +a set of slave servers. The slaves servers execute a change master +to the MaxScale server, otehrwise they are configured in exactly +the same way as a normal MySQL slave server. + +The master server configuration is unaltered, it simply sees a +single slave server. + +MaxScale is configured as usual, with a service definition that +references the binlog router. The major configuration option to +consider is the router_options paramter, in the binlog router this +provides the binlog specific configuration parameters. + + uuid= + This is the UUID that MaxScale uses when it connects + to the real master. It will report the master's + UUID to slaves that connect to it. + + server-id= + The server-id that MaxScale uses when it connects + to the real master server. Again it will reports + the master's server-id to the slaves that connect + to it. + user= + The user that MaxScale uses to login to the real + master + password= + The password that MaxScale uses to login to the + real master + master-id= + The server-id of the real master. MaxScale should + get this by sending a query, but at the moment it + is in the configuration file for ease of implementation + + +An example binlog service configuration is shown below: + +[Binlog Service] +type=service +router=binlogrouter +servers=master +router_options=uuid=f12fcb7f-b97b-11e3-bc5e-0401152c4c22,server-id=3,user=repl,password=slavepass,master-id=1 +user=maxscale +passwd=Mhu87p2D + +The servers list for a binlog router service should contain just +the master server. In future a list will be given and the monitor +used to determine which server is the current master server. diff --git a/server/modules/routing/binlog/STATUS b/server/modules/routing/binlog/STATUS new file mode 100644 index 000000000..db3a190f5 --- /dev/null +++ b/server/modules/routing/binlog/STATUS @@ -0,0 +1,13 @@ +The binlog router contained here is a prototype implementation and +should not be consider as production ready. + +The router has been written and tested with MySQL 5.6 as a reference +for the replication behaviour, more investigation and implementation +is likely to be needed in order to use other versions of MySQL, +MariaDB or Percona Server. + +To Do List: + +1. The router does not implement the replication heartbeat mechanism. + +2. Performance measurements have yet to be made. diff --git a/server/modules/routing/binlog/blr.c b/server/modules/routing/binlog/blr.c new file mode 100644 index 000000000..dec20f8b4 --- /dev/null +++ b/server/modules/routing/binlog/blr.c @@ -0,0 +1,770 @@ +/* + * This file is distributed as part of MaxScale. It is free + * software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, + * version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright SkySQL Ab 2014 + */ + +/** + * @file blr.c - binlog router, allows MaxScale to act as an intermediatory for replication + * + * The binlog router is designed to be used in replication environments to + * increase the replication fanout of a master server. It provides a transparant + * mechanism to read the binlog entries for multiple slaves while requiring + * only a single connection to the actual master to support the slaves. + * + * The current prototype implement is designed to support MySQL 5.6 and has + * a number of limitations. This prototype is merely a proof of concept and + * should not be considered production ready. + * + * @verbatim + * Revision History + * + * Date Who Description + * 02/04/2014 Mark Riddoch Initial implementation + * + * @endverbatim + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +extern int lm_enabled_logfiles_bitmask; + +static char *version_str = "V1.0.6"; + +/* The router entry points */ +static ROUTER *createInstance(SERVICE *service, char **options); +static void *newSession(ROUTER *instance, SESSION *session); +static void closeSession(ROUTER *instance, void *router_session); +static void freeSession(ROUTER *instance, void *router_session); +static int routeQuery(ROUTER *instance, void *router_session, GWBUF *queue); +static void diagnostics(ROUTER *instance, DCB *dcb); +static void clientReply( + ROUTER *instance, + void *router_session, + GWBUF *queue, + DCB *backend_dcb); +static void errorReply( + ROUTER *instance, + void *router_session, + GWBUF *message, + DCB *backend_dcb, + error_action_t action, + bool *succp); +static uint8_t getCapabilities (ROUTER* inst, void* router_session); + + +/** The module object definition */ +static ROUTER_OBJECT MyObject = { + createInstance, + newSession, + closeSession, + freeSession, + routeQuery, + diagnostics, + clientReply, + errorReply, + getCapabilities +}; + +static bool rses_begin_locked_router_action(ROUTER_SLAVE *); +static void rses_end_locked_router_action(ROUTER_SLAVE *); + +static SPINLOCK instlock; +static ROUTER_INSTANCE *instances; + +/** + * Implementation of the mandatory version entry point + * + * @return version string of the module + */ +char * +version() +{ + return version_str; +} + +/** + * The module initialisation routine, called when the module + * is first loaded. + */ +void +ModuleInit() +{ + LOGIF(LM, (skygw_log_write( + LOGFILE_MESSAGE, + "Initialise binlog router module %s.\n", version_str))); + spinlock_init(&instlock); + instances = NULL; +} + +/** + * The module entry point routine. It is this routine that + * must populate the structure that is referred to as the + * "module object", this is a structure with the set of + * external entry points for this module. + * + * @return The module object + */ +ROUTER_OBJECT * +GetModuleObject() +{ + return &MyObject; +} + +/** + * Create an instance of the router for a particular service + * within MaxScale. + * + * The process of creating the instance causes the router to register + * with the master server and begin replication of the binlogs from + * the master server to MaxScale. + * + * @param service The service this router is being create for + * @param options An array of options for this query router + * + * @return The instance data for this new instance + */ +static ROUTER * +createInstance(SERVICE *service, char **options) +{ +ROUTER_INSTANCE *inst; +char *value; +int i; + + if ((inst = calloc(1, sizeof(ROUTER_INSTANCE))) == NULL) { + return NULL; + } + + memset(&inst->stats, 0, sizeof(ROUTER_STATS)); + memset(&inst->saved_master, 0, sizeof(MASTER_RESPONSES)); + + inst->service = service; + spinlock_init(&inst->lock); + + inst->low_water = DEF_LOW_WATER; + inst->high_water = DEF_HIGH_WATER; + + /* + * We only support one server behind this router, since the server is + * the master from which we replicate binlog records. Therefore check + * that only one server has been defined. + * + * A later improvement will be to define multiple servers and have the + * router use the information that is supplied by the monitor to find + * which of these servers is currently the master and replicate from + * that server. + */ + if (service->databases == NULL || service->databases->nextdb != NULL) + { + LOGIF(LE, (skygw_log_write( + LOGFILE_ERROR, + "Error : Exactly one database server may be " + "for use with the binlog router."))); + } + + + /* + * Process the options. + * We have an array of attrbute values passed to us that we must + * examine. Supported attributes are: + * uuid= + * server-id= + * user= + * password= + * master-id= + * filestem= + * lowwater= + * highwater= + */ + if (options) + { + for (i = 0; options[i]; i++) + { + if ((value = strchr(options[i], '=')) == NULL) + { + LOGIF(LE, (skygw_log_write( + LOGFILE_ERROR, "Warning : Unsupported router " + "option %s for binlog router.", + options[i]))); + } + else + { + *value = 0; + value++; + if (strcmp(options[i], "uuid") == 0) + { + inst->uuid = strdup(value); + } + else if (strcmp(options[i], "server-id") == 0) + { + inst->serverid = atoi(value); + } + else if (strcmp(options[i], "user") == 0) + { + inst->user = strdup(value); + } + else if (strcmp(options[i], "password") == 0) + { + inst->password = strdup(value); + } + else if (strcmp(options[i], "master-id") == 0) + { + inst->masterid = atoi(value); + } + else if (strcmp(options[i], "filestem") == 0) + { + inst->fileroot = strdup(value); + } + else if (strcmp(options[i], "lowwater") == 0) + { + inst->low_water = atoi(value); + } + else if (strcmp(options[i], "highwater") == 0) + { + inst->high_water = atoi(value); + } + else + { + LOGIF(LE, (skygw_log_write( + LOGFILE_ERROR, + "Warning : Unsupported router " + "option %s for binlog router.", + options[i]))); + } + } + } + if (inst->fileroot == NULL) + inst->fileroot = strdup(BINLOG_NAME_ROOT); + } + + /* + * We have completed the creation of the instance data, so now + * insert this router instance into the linked list of routers + * that have been created with this module. + */ + spinlock_acquire(&instlock); + inst->next = instances; + instances = inst; + spinlock_release(&instlock); + + inst->active_logs = 0; + inst->reconnect_pending = 0; + inst->handling_threads = 0; + inst->residual = NULL; + inst->slaves = NULL; + inst->next = NULL; + + /* + * Initialise the binlog file and position + */ + blr_file_init(inst); + LOGIF(LT, (skygw_log_write( + LOGFILE_TRACE, + "Binlog router: current binlog file is: %s, current position %u\n", + inst->binlog_name, inst->binlog_position))); + + /* + * Initialise the binlog cache for this router instance + */ + blr_init_cache(inst); + + /* + * Now start the replication from the master to MaxScale + */ + blr_start_master(inst); + + return (ROUTER *)inst; +} + +/** + * Associate a new session with this instance of the router. + * + * In the case of the binlog router a new session equates to a new slave + * connecting to MaxScale and requesting binlog records. We need to go + * through the slave registration process for this new slave. + * + * @param instance The router instance data + * @param session The session itself + * @return Session specific data for this session + */ +static void * +newSession(ROUTER *instance, SESSION *session) +{ +ROUTER_INSTANCE *inst = (ROUTER_INSTANCE *)instance; +ROUTER_SLAVE *slave; + + LOGIF(LD, (skygw_log_write_flush( + LOGFILE_DEBUG, + "binlog router: %lu [newSession] new router session with " + "session %p, and inst %p.", + pthread_self(), + session, + inst))); + + + if ((slave = (ROUTER_SLAVE *)calloc(1, sizeof(ROUTER_SLAVE))) == NULL) + { + LOGIF(LD, (skygw_log_write_flush( + LOGFILE_ERROR, + "Insufficient memory to create new slave session for binlog router"))); + return NULL; + } + +#if defined(SS_DEBUG) + slave->rses_chk_top = CHK_NUM_ROUTER_SES; + slave->rses_chk_tail = CHK_NUM_ROUTER_SES; +#endif + + memset(&slave->stats, 0, sizeof(SLAVE_STATS)); + atomic_add(&inst->stats.n_slaves, 1); + slave->state = BLRS_CREATED; /* Set initial state of the slave */ + slave->cstate = 0; + slave->pthread = 0; + slave->overrun = 0; + spinlock_init(&slave->catch_lock); + slave->dcb = session->client; + slave->router = inst; + + /** + * Add this session to the list of active sessions. + */ + spinlock_acquire(&inst->lock); + slave->next = inst->slaves; + inst->slaves = slave; + spinlock_release(&inst->lock); + + CHK_CLIENT_RSES(slave); + + return (void *)slave; +} + +/** + * The session is no longer required. Shutdown all operation and free memory + * associated with this session. In this case a single session is associated + * to a slave of MaxScale. Therefore this is called when that slave is no + * longer active and should remove of reference to that slave, free memory + * and prevent any further forwarding of binlog records to that slave. + * + * Parameters: + * @param router_instance The instance of the router + * @param router_cli_ses The particular session to free + * + */ +static void freeSession( + ROUTER* router_instance, + void* router_client_ses) +{ +ROUTER_INSTANCE *router = (ROUTER_INSTANCE *)router_instance; +ROUTER_SLAVE *slave = (ROUTER_SLAVE *)router_client_ses; +int prev_val; + + prev_val = atomic_add(&router->stats.n_slaves, -1); + ss_dassert(prev_val > 0); + + /* + * Remove the slave session form the list of slaves that are using the + * router currently. + */ + spinlock_acquire(&router->lock); + if (router->slaves == slave) { + router->slaves = slave->next; + } else { + ROUTER_SLAVE *ptr = router->slaves; + + while (ptr != NULL && ptr->next != slave) { + ptr = ptr->next; + } + + if (ptr != NULL) { + ptr->next = slave->next; + } + } + spinlock_release(&router->lock); + + LOGIF(LD, (skygw_log_write_flush( + LOGFILE_DEBUG, + "%lu [freeSession] Unlinked router_client_session %p from " + "router %p. Connections : %d. ", + pthread_self(), + slave, + router, + prev_val-1))); + + if (slave->hostname) + free(slave->hostname); + if (slave->user) + free(slave->user); + if (slave->passwd) + free(slave->passwd); + free(slave); +} + + +/** + * Close a session with the router, this is the mechanism + * by which a router may cleanup data structure etc. + * + * @param instance The router instance data + * @param router_session The session being closed + */ +static void +closeSession(ROUTER *instance, void *router_session) +{ +ROUTER_INSTANCE *router = (ROUTER_INSTANCE *)instance; +ROUTER_SLAVE *slave = (ROUTER_SLAVE *)router_session; + + if (slave == NULL) + { + /* + * We must be closing the master session. + * + * TODO: Handle closure of master session + */ + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, "Binlog router close session with master"))); + blr_master_reconnect(router); + return; + } + CHK_CLIENT_RSES(slave); + /** + * Lock router client session for secure read and update. + */ + if (rses_begin_locked_router_action(slave)) + { + /* decrease server registered slaves counter */ + atomic_add(&router->stats.n_registered, -1); + + /* + * Mark the slave as unregistered to prevent the forwarding + * of any more binlog records to this slave. + */ + slave->state = BLRS_UNREGISTERED; + + /* Unlock */ + rses_end_locked_router_action(slave); + } +} + +/** + * We have data from the client, this is likely to be packets related to + * the registration of the slave to receive binlog records. Unlike most + * MaxScale routers there is no forwarding to the backend database, merely + * the return of either predefined server responses that have been cached + * or binlog records. + * + * @param instance The router instance + * @param router_session The router session returned from the newSession call + * @param queue The queue of data buffers to route + * @return The number of bytes sent + */ +static int +routeQuery(ROUTER *instance, void *router_session, GWBUF *queue) +{ +ROUTER_INSTANCE *router = (ROUTER_INSTANCE *)instance; +ROUTER_SLAVE *slave = (ROUTER_SLAVE *)router_session; + + return blr_slave_request(router, slave, queue); +} + +static char *event_names[] = { + "Invalid", "Start Event V3", "Query Event", "Stop Event", "Rotate Event", + "Integer Session Variable", "Load Event", "Slave Event", "Create File Event", + "Append Block Event", "Exec Load Event", "Delete File Event", + "New Load Event", "Rand Event", "User Variable Event", "Format Description Event", + "Transaction ID Event (2 Phase Commit)", "Begin Load Query Event", + "Execute Load Query Event", "Table Map Event", "Write Rows Event (v0)", + "Update Rows Event (v0)", "Delete Rows Event (v0)", "Write Rows Event (v1)", + "Update Rows Event (v1)", "Delete Rows Event (v1)", "Incident Event", + "Heartbeat Event", "Ignorable Event", "Rows Query Event", "Write Rows Event (v2)", + "Update Rows Event (v2)", "Delete Rows Event (v2)", "GTID Event", + "Anonymous GTID Event", "Previous GTIDS Event" +}; + +/** + * Display an entry from the spinlock statistics data + * + * @param dcb The DCB to print to + * @param desc Description of the statistic + * @param value The statistic value + */ +static void +spin_reporter(void *dcb, char *desc, int value) +{ + dcb_printf((DCB *)dcb, "\t\t%-35s %d\n", desc, value); +} + +/** + * Display router diagnostics + * + * @param instance Instance of the router + * @param dcb DCB to send diagnostics to + */ +static void +diagnostics(ROUTER *router, DCB *dcb) +{ +ROUTER_INSTANCE *router_inst = (ROUTER_INSTANCE *)router; +ROUTER_SLAVE *session; +int i = 0; +char buf[40]; +struct tm tm; + + spinlock_acquire(&router_inst->lock); + session = router_inst->slaves; + while (session) + { + i++; + session = session->next; + } + spinlock_release(&router_inst->lock); + + dcb_printf(dcb, "\tMaster connection DCB: %p\n", + router_inst->master); + dcb_printf(dcb, "\tMaster connection state: %s\n", + blrm_states[router_inst->master_state]); + + localtime_r(&router_inst->stats.lastReply, &tm); + asctime_r(&tm, buf); + + dcb_printf(dcb, "\tNumber of master connects: %d\n", + router_inst->stats.n_masterstarts); + dcb_printf(dcb, "\tNumber of delayed reconnects: %d\n", + router_inst->stats.n_delayedreconnects); + dcb_printf(dcb, "\tCurrent binlog file: %s\n", + router_inst->binlog_name); + dcb_printf(dcb, "\tCurrent binlog position: %u\n", + router_inst->binlog_position); + dcb_printf(dcb, "\tNumber of slave servers: %u\n", + router_inst->stats.n_slaves); + dcb_printf(dcb, "\tNumber of binlog events received: %u\n", + router_inst->stats.n_binlogs); + dcb_printf(dcb, "\tNumber of fake binlog events: %u\n", + router_inst->stats.n_fakeevents); + dcb_printf(dcb, "\tNumber of artificial binlog events: %u\n", + router_inst->stats.n_artificial); + dcb_printf(dcb, "\tNumber of binlog events in error: %u\n", + router_inst->stats.n_binlog_errors); + dcb_printf(dcb, "\tNumber of binlog rotate events: %u\n", + router_inst->stats.n_rotates); + dcb_printf(dcb, "\tNumber of binlog cache hits: %u\n", + router_inst->stats.n_cachehits); + dcb_printf(dcb, "\tNumber of binlog cache misses: %u\n", + router_inst->stats.n_cachemisses); + dcb_printf(dcb, "\tNumber of heartbeat events: %u\n", + router_inst->stats.n_heartbeats); + dcb_printf(dcb, "\tNumber of packets received: %u\n", + router_inst->stats.n_reads); + dcb_printf(dcb, "\tNumber of residual data packets: %u\n", + router_inst->stats.n_residuals); + dcb_printf(dcb, "\tAverage events per packet %.1f\n", + (double)router_inst->stats.n_binlogs / router_inst->stats.n_reads); + dcb_printf(dcb, "\tLast event from master at: %s", + buf); + dcb_printf(dcb, "\t (%d seconds ago)\n", + time(0) - router_inst->stats.lastReply); + dcb_printf(dcb, "\tLast event from master: 0x%x\n", + router_inst->lastEventReceived); + if (router_inst->active_logs) + dcb_printf(dcb, "\tRouter processing binlog records\n"); + if (router_inst->reconnect_pending) + dcb_printf(dcb, "\tRouter pending reconnect to master\n"); + dcb_printf(dcb, "\tEvents received:\n"); + for (i = 0; i < 0x24; i++) + { + dcb_printf(dcb, "\t\t%-38s: %u\n", event_names[i], router_inst->stats.events[i]); + } + +#if SPINLOCK_PROFILE + dcb_printf(dcb, "\tSpinlock statistics (instlock):\n"); + spinlock_stats(&instlock, spin_reporter, dcb); + dcb_printf(dcb, "\tSpinlock statistics (instance lock):\n"); + spinlock_stats(&router_inst->lock, spin_reporter, dcb); +#endif + + if (router_inst->slaves) + { + dcb_printf(dcb, "\tSlaves:\n"); + spinlock_acquire(&router_inst->lock); + session = router_inst->slaves; + while (session) + { + dcb_printf(dcb, "\t\tServer-id: %d\n", session->serverid); + if (session->hostname) + dcb_printf(dcb, "\t\tHostname: %s\n", session->hostname); + dcb_printf(dcb, "\t\tSlave DCB: %p\n", session->dcb); + dcb_printf(dcb, "\t\tNext Sequence No: %d\n", session->seqno); + dcb_printf(dcb, "\t\tState: %s\n", blrs_states[session->state]); + dcb_printf(dcb, "\t\tBinlog file: %s\n", session->binlogfile); + dcb_printf(dcb, "\t\tBinlog position: %u\n", session->binlog_pos); + if (session->nocrc) + dcb_printf(dcb, "\t\tMaster Binlog CRC: None\n"); + dcb_printf(dcb, "\t\tNo. requests: %u\n", session->stats.n_requests); + dcb_printf(dcb, "\t\tNo. events sent: %u\n", session->stats.n_events); + dcb_printf(dcb, "\t\tNo. bursts sent: %u\n", session->stats.n_bursts); + dcb_printf(dcb, "\t\tNo. flow control: %u\n", session->stats.n_flows); + dcb_printf(dcb, "\t\tNo. catchup NRs: %u\n", session->stats.n_catchupnr); + dcb_printf(dcb, "\t\tNo. already up to date: %u\n", session->stats.n_alreadyupd); + dcb_printf(dcb, "\t\tNo. up to date: %u\n", session->stats.n_upd); + dcb_printf(dcb, "\t\tNo. of low water cbs %u\n", session->stats.n_cb); + dcb_printf(dcb, "\t\tNo. of drained cbs %u\n", session->stats.n_dcb); + dcb_printf(dcb, "\t\tNo. of low water cbs N/A %u\n", session->stats.n_cbna); + dcb_printf(dcb, "\t\tNo. of events > high water %u\n", session->stats.n_above); + dcb_printf(dcb, "\t\tNo. of failed reads %u\n", session->stats.n_failed_read); + dcb_printf(dcb, "\t\tNo. of nested distribute events %u\n", session->stats.n_overrun); + dcb_printf(dcb, "\t\tNo. of distribute action 1 %u\n", session->stats.n_actions[0]); + dcb_printf(dcb, "\t\tNo. of distribute action 2 %u\n", session->stats.n_actions[1]); + dcb_printf(dcb, "\t\tNo. of distribute action 3 %u\n", session->stats.n_actions[2]); + if ((session->cstate & CS_UPTODATE) == 0) + { + dcb_printf(dcb, "\t\tSlave is in catchup mode. %s\n", + ((session->cstate & CS_EXPECTCB) == 0 ? "" : + "Waiting for DCB queue to drain.")); + + } + else + { + dcb_printf(dcb, "\t\tSlave is in normal mode.\n"); + if (session->binlog_pos != router_inst->binlog_position) + { + dcb_printf(dcb, "\t\tSlave reports up to date however " + "the slave binlog position does not match the master\n"); + } + } +#if SPINLOCK_PROFILE + dcb_printf(dcb, "\tSpinlock statistics (catch_lock):\n"); + spinlock_stats(&session->catch_lock, spin_reporter, dcb); + dcb_printf(dcb, "\tSpinlock statistics (rses_lock):\n"); + spinlock_stats(&session->rses_lock, spin_reporter, dcb); +#endif + + session = session->next; + } + spinlock_release(&router_inst->lock); + } +} + +/** + * Client Reply routine - in this case this is a message from the + * master server, It should be sent to the state machine that manages + * master packets as it may be binlog records or part of the registration + * handshake that takes part during connection establishment. + * + * + * @param instance The router instance + * @param router_session The router session + * @param master_dcb The DCB for the connection to the master + * @param queue The GWBUF with reply data + */ +static void +clientReply(ROUTER *instance, void *router_session, GWBUF *queue, DCB *backend_dcb) +{ +ROUTER_INSTANCE *router = (ROUTER_INSTANCE *)instance; + + atomic_add(&router->stats.n_reads, 1); + blr_master_response(router, queue); + router->stats.lastReply = time(0); +} + +/** + * Error Reply routine + * + * The routine will reply to client errors and/or closing the session + * or try to open a new backend connection. + * + * @param instance The router instance + * @param router_session The router session + * @param message The error message to reply + * @param backend_dcb The backend DCB + * @param action The action: REPLY, REPLY_AND_CLOSE, NEW_CONNECTION + * @param succp Result of action + * + */ +static void +errorReply(ROUTER *instance, void *router_session, GWBUF *message, DCB *backend_dcb, error_action_t action, bool *succp) +{ + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, "Erorr Reply '%s'", message))); + *succp = false; +} + +/** to be inline'd */ +/** + * @node Acquires lock to router client session if it is not closed. + * + * Parameters: + * @param rses - in, use + * + * + * @return true if router session was not closed. If return value is true + * it means that router is locked, and must be unlocked later. False, if + * router was closed before lock was acquired. + * + * + * @details (write detailed description here) + * + */ +static bool rses_begin_locked_router_action(ROUTER_SLAVE *rses) +{ + bool succp = false; + + CHK_CLIENT_RSES(rses); + + spinlock_acquire(&rses->rses_lock); + succp = true; + + return succp; +} + +/** to be inline'd */ +/** + * @node Releases router client session lock. + * + * Parameters: + * @param rses - + * + * + * @return void + * + * + * @details (write detailed description here) + * + */ +static void rses_end_locked_router_action(ROUTER_SLAVE * rses) +{ + CHK_CLIENT_RSES(rses); + spinlock_release(&rses->rses_lock); +} + + +static uint8_t getCapabilities(ROUTER *inst, void *router_session) +{ + return 0; +} diff --git a/server/modules/routing/binlog/blr_cache.c b/server/modules/routing/binlog/blr_cache.c new file mode 100644 index 000000000..5bc46f036 --- /dev/null +++ b/server/modules/routing/binlog/blr_cache.c @@ -0,0 +1,69 @@ +/* + * This file is distributed as part of MaxScale. It is free + * software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, + * version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright SkySQL Ab 2014 + */ + +/** + * @file blr_cache.c - binlog router cache, manage the binlog cache + * + * The binlog router is designed to be used in replication environments to + * increase the replication fanout of a master server. It provides a transparant + * mechanism to read the binlog entries for multiple slaves while requiring + * only a single connection to the actual master to support the slaves. + * + * The current prototype implement is designed to support MySQL 5.6 and has + * a number of limitations. This prototype is merely a proof of concept and + * should not be considered production ready. + * + * @verbatim + * Revision History + * + * Date Who Description + * 07/04/2014 Mark Riddoch Initial implementation + * + * @endverbatim + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + + +extern int lm_enabled_logfiles_bitmask; + + +/** + * Initialise the cache for this instanceof the binlog router. As a side + * effect also determine the binlog file to read and the position to read + * from. + * + * @param router The router instance + */ +void +blr_init_cache(ROUTER_INSTANCE *router) +{ +} diff --git a/server/modules/routing/binlog/blr_file.c b/server/modules/routing/binlog/blr_file.c new file mode 100644 index 000000000..4f7232e64 --- /dev/null +++ b/server/modules/routing/binlog/blr_file.c @@ -0,0 +1,346 @@ +/* + * This file is distributed as part of MaxScale. It is free + * software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, + * version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright SkySQL Ab 2014 + */ + +/** + * @file blr_file.c - contains code for the router binlog file management + * + * + * @verbatim + * Revision History + * + * Date Who Description + * 14/04/2014 Mark Riddoch Initial implementation + * + * @endverbatim + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + + +extern int lm_enabled_logfiles_bitmask; + +static void blr_file_create(ROUTER_INSTANCE *router, char *file); +static void blr_file_append(ROUTER_INSTANCE *router, char *file); +static uint32_t extract_field(uint8_t *src, int bits); + +/** + * Initialise the binlog file for this instance. MaxScale will look + * for all the binlogs that it has on local disk, determien the next + * binlog to use and initialise it for writing, determining the + * next record to be fetched from the real master. + * + * @param router The router instance this defines the master for this replication chain + */ +void +blr_file_init(ROUTER_INSTANCE *router) +{ +char *ptr, path[1024], filename[1050]; +int file_found, n = 1; +int root_len, i; +DIR *dirp; +struct dirent *dp; + + strcpy(path, "/usr/local/skysql/MaxScale"); + if ((ptr = getenv("MAXSCALE_HOME")) != NULL) + { + strcpy(path, ptr); + } + strcat(path, "/"); + strcat(path, router->service->name); + + if (access(path, R_OK) == -1) + mkdir(path, 0777); + + /* First try to find a binlog file number by reading the directory */ + root_len = strlen(router->fileroot); + dirp = opendir(path); + while ((dp = readdir(dirp)) != NULL) + { + if (strncmp(dp->d_name, router->fileroot, root_len) == 0) + { + i = atoi(dp->d_name + root_len + 1); + if (i > n) + n = i; + } + } + closedir(dirp); + + + file_found = 0; + do { + sprintf(filename, "%s/" BINLOG_NAMEFMT, path, router->fileroot, n); + if (access(filename, R_OK) != -1) + { + file_found = 1; + n++; + } + else + file_found = 0; + } while (file_found); + n--; + + if (n == 0) // No binlog files found + { + sprintf(filename, BINLOG_NAMEFMT, router->fileroot, 1); + blr_file_create(router, filename); + } + else + { + sprintf(filename, BINLOG_NAMEFMT, router->fileroot, n); + blr_file_append(router, filename); + } + +} + +void +blr_file_rotate(ROUTER_INSTANCE *router, char *file, uint64_t pos) +{ + blr_file_create(router, file); +} + + +/** + * Create a new binlog file for the router to use. + * + * @param router The router instance + * @param file The binlog file name + */ +static void +blr_file_create(ROUTER_INSTANCE *router, char *file) +{ +char *ptr, path[1024]; +int fd; +unsigned char magic[] = BINLOG_MAGIC; + + strcpy(path, "/usr/local/skysql/MaxScale"); + if ((ptr = getenv("MAXSCALE_HOME")) != NULL) + { + strcpy(path, ptr); + } + strcat(path, "/"); + strcat(path, router->service->name); + strcat(path, "/"); + strcat(path, file); + + if ((fd = open(path, O_RDWR|O_CREAT, 0666)) != -1) + { + write(fd, magic, 4); + } + else + { + LOGIF(LE, (skygw_log_write(LOGFILE_ERROR, + "Failed to create binlog file %s\n", path))); + } + fsync(fd); + close(router->binlog_fd); + strcpy(router->binlog_name, file); + router->binlog_position = 4; /* Initial position after the magic number */ + router->binlog_fd = fd; +} + + +/** + * Prepare an existing binlog file to be appened to. + * + * @param router The router instance + * @param file The binlog file name + */ +static void +blr_file_append(ROUTER_INSTANCE *router, char *file) +{ +char *ptr, path[1024]; +int fd; + + strcpy(path, "/usr/local/skysql/MaxScale"); + if ((ptr = getenv("MAXSCALE_HOME")) != NULL) + { + strcpy(path, ptr); + } + strcat(path, "/"); + strcat(path, router->service->name); + strcat(path, "/"); + strcat(path, file); + + if ((fd = open(path, O_RDWR|O_APPEND, 0666)) == -1) + { + LOGIF(LE, (skygw_log_write(LOGFILE_ERROR, + "Failed to open binlog file %s for append.\n", + path))); + return; + } + fsync(fd); + close(router->binlog_fd); + strcpy(router->binlog_name, file); + router->binlog_position = lseek(fd, 0L, SEEK_END); + router->binlog_fd = fd; +} + +/** + * Write a binlog entry to disk. + * + * @param router The router instance + * @param buf The binlog record + * @param len The length of the binlog record + */ +void +blr_write_binlog_record(ROUTER_INSTANCE *router, REP_HEADER *hdr, uint8_t *buf) +{ + pwrite(router->binlog_fd, buf, hdr->event_size, hdr->next_pos - hdr->event_size); + router->binlog_position = hdr->next_pos; +} + +/** + * Flush the content of the binlog file to disk. + * + * @param router The binlog router + */ +void +blr_file_flush(ROUTER_INSTANCE *router) +{ + fsync(router->binlog_fd); +} + +int +blr_open_binlog(ROUTER_INSTANCE *router, char *binlog) +{ +char *ptr, path[1024]; +int rval; + + strcpy(path, "/usr/local/skysql/MaxScale"); + if ((ptr = getenv("MAXSCALE_HOME")) != NULL) + { + strcpy(path, ptr); + } + strcat(path, "/"); + strcat(path, router->service->name); + strcat(path, "/"); + strcat(path, binlog); + + if ((rval = open(path, O_RDONLY, 0666)) == -1) + { + LOGIF(LE, (skygw_log_write(LOGFILE_ERROR, + "Failed to open binlog file %s\n", path))); + } + + return rval; +} + +/** + * Read a replication event into a GWBUF structure. + * + * @param fd File descriptor of the binlog file + * @param pos Position of binlog record to read + * @param hdr Binlog header to populate + * @return The binlog record wrapped in a GWBUF structure + */ +GWBUF * +blr_read_binlog(int fd, unsigned int pos, REP_HEADER *hdr) +{ +uint8_t hdbuf[19]; +GWBUF *result; +unsigned char *data; +int n; + + if (lseek(fd, pos, SEEK_SET) != pos) + { + LOGIF(LE, (skygw_log_write(LOGFILE_ERROR, + "Failed to seek for binlog entry, " + "at %d.\n", pos))); + return NULL; + } + + /* Read the header information from the file */ + if ((n = read(fd, hdbuf, 19)) != 19) + { + LOGIF(LE, (skygw_log_write(LOGFILE_ERROR, + "Failed to read header for binlog entry, " + "at %d (%s).\n", pos, strerror(errno)))); + if (n> 0 && n < 19) + LOGIF(LE, (skygw_log_write(LOGFILE_ERROR, + "Short read when reading the header. " + "Expected 19 bytes got %d bytes.\n", + n))); + return NULL; + } + hdr->timestamp = extract_field(hdbuf, 32); + hdr->event_type = hdbuf[4]; + hdr->serverid = extract_field(&hdbuf[5], 32); + hdr->event_size = extract_field(&hdbuf[9], 32); + hdr->next_pos = extract_field(&hdbuf[13], 32); + hdr->flags = extract_field(&hdbuf[17], 16); + if ((result = gwbuf_alloc(hdr->event_size)) == NULL) + { + LOGIF(LE, (skygw_log_write(LOGFILE_ERROR, + "Failed to allocate memory for binlog entry, " + "size %d at %d.\n", + hdr->event_size, pos))); + return NULL; + } + data = GWBUF_DATA(result); + memcpy(data, hdbuf, 19); // Copy the header in + if ((n = read(fd, &data[19], hdr->event_size - 19)) + != hdr->event_size - 19) // Read the balance + { + LOGIF(LE, (skygw_log_write(LOGFILE_ERROR, + "Short read when reading the event at %d. " + "Expected %d bytes got %d bytes.\n", + pos, n))); + gwbuf_consume(result, hdr->event_size); + return NULL; + } + return result; +} + +/** + * Extract a numeric field from a packet of the specified number of bits + * + * @param src The raw packet source + * @param birs The number of bits to extract (multiple of 8) + */ +static uint32_t +extract_field(uint8_t *src, int bits) +{ +uint32_t rval = 0, shift = 0; + + while (bits > 0) + { + rval |= (*src++) << shift; + shift += 8; + bits -= 8; + } + return rval; +} diff --git a/server/modules/routing/binlog/blr_master.c b/server/modules/routing/binlog/blr_master.c new file mode 100644 index 000000000..412276e48 --- /dev/null +++ b/server/modules/routing/binlog/blr_master.c @@ -0,0 +1,1024 @@ +/* + * This file is distributed as part of MaxScale. It is free + * software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, + * version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright SkySQL Ab 2014 + */ + +/** + * @file blr_master.c - contains code for the router to master communication + * + * The binlog router is designed to be used in replication environments to + * increase the replication fanout of a master server. It provides a transparant + * mechanism to read the binlog entries for multiple slaves while requiring + * only a single connection to the actual master to support the slaves. + * + * The current prototype implement is designed to support MySQL 5.6 and has + * a number of limitations. This prototype is merely a proof of concept and + * should not be considered production ready. + * + * @verbatim + * Revision History + * + * Date Who Description + * 02/04/2014 Mark Riddoch Initial implementation + * + * @endverbatim + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +/* Temporary requirement for auth data */ +#include + +extern int lm_enabled_logfiles_bitmask; + +static GWBUF *blr_make_query(char *statement); +static GWBUF *blr_make_registration(ROUTER_INSTANCE *router); +static GWBUF *blr_make_binlog_dump(ROUTER_INSTANCE *router); +static void encode_value(unsigned char *data, unsigned int value, int len); +static void blr_handle_binlog_record(ROUTER_INSTANCE *router, GWBUF *pkt); +static void blr_rotate_event(ROUTER_INSTANCE *router, uint8_t *pkt, REP_HEADER *hdr); +static void blr_distribute_binlog_record(ROUTER_INSTANCE *router, REP_HEADER *hdr, uint8_t *ptr); +static void *CreateMySQLAuthData(char *username, char *password, char *database); +static void blr_extract_header(uint8_t *pkt, REP_HEADER *hdr); +static uint32_t extract_field(uint8_t *src, int bits); +static void blr_log_packet(logfile_id_t file, char *msg, uint8_t *ptr, int len); + +static int keepalive = 1; + +/** + * blr_start_master - controls the connection of the binlog router to the + * master MySQL server and triggers the slave registration process for + * the router. + * + * @param router The router instance + */ +void +blr_start_master(ROUTER_INSTANCE *router) +{ +DCB *client; +GWBUF *buf; + + if ((client = dcb_alloc(DCB_ROLE_INTERNAL)) == NULL) + { + LOGIF(LE, (skygw_log_write_flush(LOGFILE_ERROR, + "Binlog router: failed to create DCB for dummy client\n"))); + return; + } + router->client = client; + client->data = CreateMySQLAuthData(router->user, router->password, ""); + if ((router->session = session_alloc(router->service, client)) == NULL) + { + LOGIF(LE, (skygw_log_write_flush(LOGFILE_ERROR, + "Binlog router: failed to create session for connection to master\n"))); + return; + } + client->session = router->session; + if ((router->master = dcb_connect(router->service->databases, router->session, BLR_PROTOCOL)) == NULL) + { + LOGIF(LE, (skygw_log_write_flush(LOGFILE_ERROR, + "Binlog router: failed to connect to master\n"))); + return; + } + +if (setsockopt(router->master->fd, SOL_SOCKET, SO_KEEPALIVE, &keepalive , sizeof(keepalive ))) +perror("setsockopt"); + + router->master_state = BLRM_AUTHENTICATED; + buf = blr_make_query("SELECT UNIX_TIMESTAMP()"); + router->master->func.write(router->master, buf); + router->master_state = BLRM_TIMESTAMP; + + router->stats.n_masterstarts++; +} + +/** + * Reconnect to the master server. + * + * IMPORTANT - must be called with router->active_logs set by the + * thread that set active_logs. + * + * @param router The router instance + */ +static void +blr_restart_master(ROUTER_INSTANCE *router) +{ +GWBUF *ptr; + + dcb_close(router->master); + dcb_free(router->master); + dcb_free(router->client); + + /* Discard the queued residual data */ + ptr = router->residual; + while (ptr) + { + ptr = gwbuf_consume(ptr, GWBUF_LENGTH(ptr)); + } + router->residual = NULL; + + /* Now it is safe to unleash other threads on this router instance */ + spinlock_acquire(&router->lock); + router->reconnect_pending = 0; + router->active_logs = 0; + spinlock_release(&router->lock); + blr_start_master(router); +} + +/** + * Request a reconnect to the master. + * + * If another thread is active processing messages from the master + * then merely set a flag for that thread to do the restart. If no + * threads are active then directly call the restart routine to + * reconnect to the master. + * + * @param router The router instance + */ +void +blr_master_reconnect(ROUTER_INSTANCE *router) +{ +int do_reconnect = 0; + + spinlock_acquire(&router->lock); + if (router->active_logs) + { + /* Currently processing a response, set a flag + * and get the thread that is process a response + * to deal with the reconnect. + */ + router->reconnect_pending = 1; + router->stats.n_delayedreconnects++; + } + else + { + router->active_logs = 1; + do_reconnect = 1; + } + spinlock_release(&router->lock); + if (do_reconnect) + { + blr_restart_master(router); + spinlock_acquire(&router->lock); + router->active_logs = 0; + spinlock_release(&router->lock); + } +} + +/** + * Binlog router master side state machine event handler. + * + * Handles an incoming response from the master server to the binlog + * router. + * + * @param router The router instance + * @param buf The incoming packet + */ +void +blr_master_response(ROUTER_INSTANCE *router, GWBUF *buf) +{ +char query[128]; + + atomic_add(&router->handling_threads, 1); + ss_dassert(router->handling_threads == 1); + spinlock_acquire(&router->lock); + router->active_logs = 1; + spinlock_release(&router->lock); + if (router->master_state < 0 || router->master_state > BLRM_MAXSTATE) + { + LOGIF(LE, (skygw_log_write( + LOGFILE_ERROR, "Invalid master state machine state (%d) for binlog router.\n", + router->master_state))); + gwbuf_consume(buf, gwbuf_length(buf)); + spinlock_acquire(&router->lock); + if (router->reconnect_pending) + { + router->active_logs = 0; + spinlock_release(&router->lock); + atomic_add(&router->handling_threads, -1); + blr_restart_master(router); + return; + } + router->active_logs = 0; + spinlock_release(&router->lock); + atomic_add(&router->handling_threads, -1); + return; + } + + if (router->master_state != BLRM_BINLOGDUMP && MYSQL_RESPONSE_ERR(buf)) + { + LOGIF(LE, (skygw_log_write( + LOGFILE_ERROR, + "Received error: %d, %s from master during %s phase of the master state machine.\n", + MYSQL_ERROR_CODE(buf), MYSQL_ERROR_MSG(buf), blrm_states[router->master_state] + ))); + gwbuf_consume(buf, gwbuf_length(buf)); + spinlock_acquire(&router->lock); + router->active_logs = 0; + if (router->reconnect_pending) + { + spinlock_release(&router->lock); + atomic_add(&router->handling_threads, -1); + blr_restart_master(router); + return; + } + spinlock_release(&router->lock); + atomic_add(&router->handling_threads, -1); + return; + } + switch (router->master_state) + { + case BLRM_TIMESTAMP: + // Response to a timestamp message, no need to save this. + gwbuf_consume(buf, GWBUF_LENGTH(buf)); + buf = blr_make_query("SHOW VARIABLES LIKE 'SERVER_ID'"); + router->master_state = BLRM_SERVERID; + router->master->func.write(router->master, buf); + break; + case BLRM_SERVERID: + // Response to fetch of master's server-id + router->saved_master.server_id = buf; + // TODO: Extract the value of server-id and place in router->master_id + buf = blr_make_query("SET @master_heartbeat_period = 1799999979520"); + router->master_state = BLRM_HBPERIOD; + router->master->func.write(router->master, buf); + break; + case BLRM_HBPERIOD: + // Response to set the heartbeat period + router->saved_master.heartbeat = buf; + buf = blr_make_query("SET @master_binlog_checksum = @@global.binlog_checksum"); + router->master_state = BLRM_CHKSUM1; + router->master->func.write(router->master, buf); + break; + case BLRM_CHKSUM1: + // Response to set the master binlog checksum + router->saved_master.chksum1 = buf; + buf = blr_make_query("SELECT @master_binlog_checksum"); + router->master_state = BLRM_CHKSUM2; + router->master->func.write(router->master, buf); + break; + case BLRM_CHKSUM2: + // Response to the master_binlog_checksum, should be stored + router->saved_master.chksum2 = buf; + buf = blr_make_query("SELECT @@GLOBAL.GTID_MODE"); + router->master_state = BLRM_GTIDMODE; + router->master->func.write(router->master, buf); + break; + case BLRM_GTIDMODE: + // Response to the GTID_MODE, should be stored + router->saved_master.gtid_mode = buf; + buf = blr_make_query("SHOW VARIABLES LIKE 'SERVER_UUID'"); + router->master_state = BLRM_MUUID; + router->master->func.write(router->master, buf); + break; + case BLRM_MUUID: + // Response to the SERVER_UUID, should be stored + router->saved_master.uuid = buf; + sprintf(query, "SET @slave_uuid='%s'", router->uuid); + buf = blr_make_query(query); + router->master_state = BLRM_SUUID; + router->master->func.write(router->master, buf); + break; + case BLRM_SUUID: + // Response to the SET @server_uuid, should be stored + router->saved_master.setslaveuuid = buf; + buf = blr_make_query("SET NAMES latin1"); + router->master_state = BLRM_LATIN1; + router->master->func.write(router->master, buf); + break; + case BLRM_LATIN1: + // Response to the SET NAMES latin1, should be stored + router->saved_master.setnames = buf; + buf = blr_make_query("SET NAMES utf8"); + router->master_state = BLRM_UTF8; + router->master->func.write(router->master, buf); + break; + case BLRM_UTF8: + // Response to the SET NAMES utf8, should be stored + router->saved_master.utf8 = buf; + buf = blr_make_query("SELECT 1"); + router->master_state = BLRM_SELECT1; + router->master->func.write(router->master, buf); + break; + case BLRM_SELECT1: + // Response to the SELECT 1, should be stored + router->saved_master.select1 = buf; + buf = blr_make_query("SELECT VERSION();"); + router->master_state = BLRM_SELECTVER; + router->master->func.write(router->master, buf); + break; + case BLRM_SELECTVER: + // Response to SELECT VERSION should be stored + router->saved_master.selectver = buf; + buf = blr_make_registration(router); + router->master_state = BLRM_REGISTER; + router->master->func.write(router->master, buf); + break; + case BLRM_REGISTER: + // Request a dump of the binlog file + buf = blr_make_binlog_dump(router); + router->master_state = BLRM_BINLOGDUMP; + router->master->func.write(router->master, buf); + break; + case BLRM_BINLOGDUMP: + // Main body, we have received a binlog record from the master + blr_handle_binlog_record(router, buf); + break; + } + + if (router->reconnect_pending) + blr_restart_master(router); + spinlock_acquire(&router->lock); + router->active_logs = 0; + spinlock_release(&router->lock); + atomic_add(&router->handling_threads, -1); +} + +/** + * Build a MySQL query into a GWBUF that we can send to the master database + * + * @param query The text of the query to send + */ +static GWBUF * +blr_make_query(char *query) +{ +GWBUF *buf; +unsigned char *data; +int len; + + if ((buf = gwbuf_alloc(strlen(query) + 5)) == NULL) + return NULL; + data = GWBUF_DATA(buf); + len = strlen(query) + 1; + encode_value(&data[0], len, 24); // Payload length + data[3] = 0; // Sequence id + // Payload + data[4] = COM_QUERY; // Command + memcpy(&data[5], query, strlen(query)); + + return buf; +} + +/** + * Build a MySQL slave registration into a GWBUF that we can send to the + * master database + * + * @param router The router instance + * @return A MySQL Replication registration message in a GWBUF structure + */ +static GWBUF * +blr_make_registration(ROUTER_INSTANCE *router) +{ +GWBUF *buf; +unsigned char *data; +int len = 18; + + if ((buf = gwbuf_alloc(len + 4)) == NULL) + return NULL; + data = GWBUF_DATA(buf); + encode_value(&data[0], len, 24); // Payload length + data[3] = 0; // Sequence ID + data[4] = COM_REGISTER_SLAVE; // Command + encode_value(&data[5], router->serverid, 32); // Slave Server ID + data[9] = 0; // Slave hostname length + data[10] = 0; // Slave username length + data[11] = 0; // Slave password length + encode_value(&data[12], + router->service->ports->port, 16); // Slave master port + encode_value(&data[14], 0, 32); // Replication rank + encode_value(&data[18], router->masterid, 32); // Master server-id + + return buf; +} + + +/** + * Build a Binlog dump command into a GWBUF that we can send to the + * master database + * + * @param router The router instance + * @return A MySQL Replication COM_BINLOG_DUMP message in a GWBUF structure + */ +static GWBUF * +blr_make_binlog_dump(ROUTER_INSTANCE *router) +{ +GWBUF *buf; +unsigned char *data; +int len = 0x1b; + + if ((buf = gwbuf_alloc(len + 4)) == NULL) + return NULL; + data = GWBUF_DATA(buf); + + encode_value(&data[0], len,24); // Payload length + data[3] = 0; // Sequence ID + data[4] = COM_BINLOG_DUMP; // Command + encode_value(&data[5], + router->binlog_position, 32); // binlog position + encode_value(&data[9], 0, 16); // Flags + encode_value(&data[11], + router->serverid, 32); // Server-id of MaxScale + strncpy((char *)&data[15], router->binlog_name, + BINLOG_FNAMELEN); // binlog filename + return buf; +} + + +/** + * Encode a value into a number of bits in a MySQL packet + * + * @param data Point to location in target packet + * @param value The value to pack + * @param len Number of bits to encode value into + */ +static void +encode_value(unsigned char *data, unsigned int value, int len) +{ + while (len > 0) + { + *data++ = value & 0xff; + value >>= 8; + len -= 8; + } +} + +/** + * blr_handle_binlog_record - we have received binlog records from + * the master and we must now work out what to do with them. + * + * @param router The router instance + * @param pkt The binlog records + */ +static void +blr_handle_binlog_record(ROUTER_INSTANCE *router, GWBUF *pkt) +{ +uint8_t *msg = NULL, *ptr, *pdata; +REP_HEADER hdr; +unsigned int len, reslen; +unsigned int pkt_length; +int no_residual = 1; +int preslen = -1; +int prev_length = -1; +int n_bufs = -1, pn_bufs = -1; +static REP_HEADER phdr; + + /* + * Prepend any residual buffer to the buffer chain we have + * been called with. + */ + if (router->residual) + { + pkt = gwbuf_append(router->residual, pkt); + router->residual = NULL; + no_residual = 0; + } + + pkt_length = gwbuf_length(pkt); + while (pkt && pkt_length > 24) + { + reslen = GWBUF_LENGTH(pkt); + pdata = GWBUF_DATA(pkt); + if (reslen < 3) // Payload length straddles buffers + { + /* Get the length of the packet from the residual and new packet */ + if (reslen >= 3) + { + len = extract_field(pdata, 24); + } + else if (reslen == 2) + { + len = extract_field(pdata, 16); + len |= (extract_field(GWBUF_DATA(pkt->next), 8) << 16); + } + else if (reslen == 1) + { + len = extract_field(pdata, 8); + len |= (extract_field(GWBUF_DATA(pkt->next), 16) << 8); + } + len += 4; // Allow space for the header + } + else + { + len = extract_field(pdata, 24) + 4; + } + + if (reslen < len && pkt_length >= len) + { + /* + * The message is contained in more than the current + * buffer, however we have the complete messasge in + * this buffer and the chain of remaining buffers. + * + * Allocate a contiguous buffer for the binlog message + * and copy the complete message into this buffer. + */ + int remainder = len; + GWBUF *p = pkt; + + if ((msg = malloc(len)) == NULL) + { + LOGIF(LE,(skygw_log_write( + LOGFILE_ERROR, + "Insufficient memory to buffer event " + "of %d bytes. Binlog %s @ %d\n.", + len, router->binlog_name, + router->binlog_position))); + break; + } + + n_bufs = 0; + ptr = msg; + while (p && remainder > 0) + { + int plen = GWBUF_LENGTH(p); + int n = (remainder > plen ? plen : remainder); + memcpy(ptr, GWBUF_DATA(p), n); + remainder -= n; + ptr += n; + if (remainder > 0) + p = p->next; + n_bufs++; + } + if (remainder) + { + LOGIF(LE,(skygw_log_write( + LOGFILE_ERROR, + "Expected entire message in buffer " + "chain, but failed to create complete " + "message as expected. %s @ %d\n", + router->binlog_name, + router->binlog_position))); + free(msg); + msg = NULL; + break; + } + + ptr = msg; + } + else if (reslen < len) + { + /* + * The message is not fully contained in the current + * and we do not have the complete message in the + * buffer chain. Therefore we must stop processing + * until we receive the next buffer. + */ + router->stats.n_residuals++; + LOGIF(LD,(skygw_log_write( + LOGFILE_DEBUG, + "Residual data left after %d records. %s @ %d\n", + router->stats.n_binlogs, + router->binlog_name, router->binlog_position))); + break; + } + else + { + /* + * The message is fully contained in the current buffer + */ + ptr = pdata; + n_bufs = 1; + } + + blr_extract_header(ptr, &hdr); + + if (hdr.event_size != len - 5) + { + LOGIF(LE,(skygw_log_write( + LOGFILE_ERROR, + "Packet length is %d, but event size is %d, " + "binlog file %s position %d" + "reslen is %d and preslen is %d, " + "length of previous event %d. %s", + len, hdr.event_size, + router->binlog_name, + router->binlog_position, + reslen, preslen, prev_length, + (prev_length == -1 ? + (no_residual ? "No residual data from previous call" : "Residual data from previous call") : "") + ))); + blr_log_packet(LOGFILE_ERROR, "Packet:", ptr, len); + LOGIF(LE,(skygw_log_write( + LOGFILE_ERROR, + "This event (0x%x) was contained in %d GWBUFs, " + "the previous events was contained in %d GWBUFs", + router->lastEventReceived, n_bufs, pn_bufs))); + if (msg) + { + free(msg); + msg = NULL; + } + break; + } + phdr = hdr; + if (hdr.ok == 0) + { + router->stats.n_binlogs++; + router->lastEventReceived = hdr.event_type; + +// #define SHOW_EVENTS +#ifdef SHOW_EVENTS + printf("blr: event type 0x%02x, flags 0x%04x, event size %d\n", hdr.event_type, hdr.flags, hdr.event_size); +#endif + if (hdr.event_type >= 0 && hdr.event_type < 0x24) + router->stats.events[hdr.event_type]++; + if (hdr.event_type == FORMAT_DESCRIPTION_EVENT && hdr.next_pos == 0) + { + // Fake format description message + LOGIF(LD,(skygw_log_write(LOGFILE_DEBUG, + "Replication fake event. " + "Binlog %s @ %d.\n", + router->binlog_name, + router->binlog_position))); + router->stats.n_fakeevents++; + if (hdr.event_type == FORMAT_DESCRIPTION_EVENT) + { + /* + * We need to save this to replay to new + * slaves that attach later. + */ + if (router->saved_master.fde_event) + free(router->saved_master.fde_event); + router->saved_master.fde_len = hdr.event_size; + router->saved_master.fde_event = malloc(hdr.event_size); + if (router->saved_master.fde_event) + memcpy(router->saved_master.fde_event, + ptr + 5, hdr.event_size); + } + } + else + { + if (hdr.event_type == HEARTBEAT_EVENT) + { +#ifdef SHOW_EVENTS + printf("Replication heartbeat\n"); +#endif + LOGIF(LD,(skygw_log_write( + LOGFILE_DEBUG, + "Replication heartbeat. " + "Binlog %s @ %d.\n", + router->binlog_name, + router->binlog_position))); + router->stats.n_heartbeats++; + } + else if (hdr.flags != LOG_EVENT_ARTIFICIAL_F) + { + ptr = ptr + 5; // We don't put the first byte of the payload + // into the binlog file + blr_write_binlog_record(router, &hdr, ptr); + if (hdr.event_type == ROTATE_EVENT) + { + blr_rotate_event(router, ptr, &hdr); + } + blr_distribute_binlog_record(router, &hdr, ptr); + } + else + { + router->stats.n_artificial++; + LOGIF(LD,(skygw_log_write( + LOGFILE_DEBUG, + "Artificial event not written " + "to disk or distributed. " + "Type 0x%x, Length %d, Binlog " + "%s @ %d\n.", + hdr.event_type, + hdr.event_size, + router->binlog_name, + router->binlog_position))); + ptr += 5; + if (hdr.event_type == ROTATE_EVENT) + { + blr_rotate_event(router, ptr, &hdr); + } + } + } + } + else + { + printf("Binlog router error: %s\n", &ptr[7]); + LOGIF(LE,(skygw_log_write(LOGFILE_ERROR, + "Error packet in binlog stream.%s @ %d\n.", + router->binlog_name, + router->binlog_position))); + blr_log_packet(LOGFILE_ERROR, "Error Packet:", + ptr, len); + router->stats.n_binlog_errors++; + } + + if (msg) + { + free(msg); + msg = NULL; + } + prev_length = len; + while (len > 0) + { + int n, plen; + plen = GWBUF_LENGTH(pkt); + n = (plen < len ? plen : len); + pkt = gwbuf_consume(pkt, n); + len -= n; + pkt_length -= n; + } + preslen = reslen; + pn_bufs = n_bufs; + } + + /* + * Check if we have a residual, part binlog message to deal with. + * Just simply store the GWBUF for next time + */ + if (pkt) + { + router->residual = pkt; + ss_dassert(pkt_length != 0); + } + else + { + ss_dassert(pkt_length == 0); + } + blr_file_flush(router); +} + +/** + * Populate a header structure for a replication message from a GWBUF structure. + * + * @param pkt The incoming packet in a GWBUF chain + * @param hdr The packet header to populate + */ +static void +blr_extract_header(uint8_t *ptr, REP_HEADER *hdr) +{ + + hdr->payload_len = extract_field(ptr, 24); + hdr->seqno = ptr[3]; + hdr->ok = ptr[4]; + hdr->timestamp = extract_field(&ptr[5], 32); + hdr->event_type = ptr[9]; + hdr->serverid = extract_field(&ptr[10], 32); + hdr->event_size = extract_field(&ptr[14], 32); + hdr->next_pos = extract_field(&ptr[18], 32); + hdr->flags = extract_field(&ptr[22], 16); +} + +/** + * Extract a numeric field from a packet of the specified number of bits + * + * @param src The raw packet source + * @param birs The number of bits to extract (multiple of 8) + */ +static uint32_t +extract_field(uint8_t *src, int bits) +{ +uint32_t rval = 0, shift = 0; + + while (bits > 0) + { + rval |= (*src++) << shift; + shift += 8; + bits -= 8; + } + return rval; +} + +/** + * Process a binlog rotate event. + * + * @param router The instance of the router + * @param ptr The packet containing the rotate event + * @param hdr The replication message header + */ +static void +blr_rotate_event(ROUTER_INSTANCE *router, uint8_t *ptr, REP_HEADER *hdr) +{ +int len, slen; +uint64_t pos; +char file[BINLOG_FNAMELEN+1]; + + ptr += 19; // Skip event header + len = hdr->event_size - 19; // Event size minus header + pos = extract_field(ptr+4, 32); + pos <<= 32; + pos |= extract_field(ptr, 32); + slen = len - 8; + if (slen > BINLOG_FNAMELEN) + slen = BINLOG_FNAMELEN; + memcpy(file, ptr + 8, slen); + file[slen] = 0; + +#ifdef VERBOSE_ROTATE + printf("binlog rotate: "); + while (len--) + printf("0x%02x ", *ptr++); + printf("\n"); + printf("New file: %s @ %ld\n", file, pos); +#endif + + if (strncmp(router->binlog_name, file, slen) != 0) + { + router->stats.n_rotates++; + blr_file_rotate(router, file, pos); + } +} + +/** + * Create the auth data needed to be able to call dcb_connect. + * + * This doesn't really belong here and should be moved at some stage. + */ +static void * +CreateMySQLAuthData(char *username, char *password, char *database) +{ +MYSQL_session *auth_info; + + if (username == NULL || password == NULL) + { + LOGIF(LE,(skygw_log_write( + LOGFILE_ERROR, + "You must specify both username and password for the binlog router.\n"))); + return NULL; + } + + if ((auth_info = calloc(1, sizeof(MYSQL_session))) == NULL) + return NULL; + strcpy(auth_info->user, username); + strcpy(auth_info->db, database); + gw_sha1_str((const uint8_t *)password, strlen(password), auth_info->client_sha1); + + return auth_info; +} + +/** + * Distribute the binlog record we have just received to all the registered slaves. + * + * @param router The router instance + * @param hdr The replication event header + * @param ptr The raw replication event data + */ +static void +blr_distribute_binlog_record(ROUTER_INSTANCE *router, REP_HEADER *hdr, uint8_t *ptr) +{ +GWBUF *pkt; +uint8_t *buf; +ROUTER_SLAVE *slave; +int action; + + spinlock_acquire(&router->lock); + slave = router->slaves; + while (slave) + { + spinlock_acquire(&slave->catch_lock); + if ((slave->cstate & (CS_UPTODATE|CS_DIST)) == CS_UPTODATE) + { + /* Slave is up to date with the binlog and no distribute is + * running on this slave. + */ + action = 1; + slave->cstate |= CS_DIST; + } + else if ((slave->cstate & (CS_UPTODATE|CS_DIST)) == (CS_UPTODATE|CS_DIST)) + { + /* Slave is up to date with the binlog and a distribute is + * running on this slave. + */ + slave->overrun = 1; + action = 2; + } + else if ((slave->cstate & CS_UPTODATE) == 0) + { + /* Slave is in catchup mode */ + action = 3; + } + slave->stats.n_actions[action-1]++; + spinlock_release(&slave->catch_lock); + if (action == 1) + { + if ((slave->binlog_pos == hdr->next_pos - hdr->event_size) + && (strcmp(slave->binlogfile, router->binlog_name) == 0 || + hdr->event_type == ROTATE_EVENT)) + { + pkt = gwbuf_alloc(hdr->event_size + 5); + buf = GWBUF_DATA(pkt); + encode_value(buf, hdr->event_size + 1, 24); + buf += 3; + *buf++ = slave->seqno++; + *buf++ = 0; // OK + memcpy(buf, ptr, hdr->event_size); + if (hdr->event_type == ROTATE_EVENT) + { + blr_slave_rotate(slave, ptr); + } + slave->dcb->func.write(slave->dcb, pkt); + if (hdr->event_type != ROTATE_EVENT) + { + slave->binlog_pos = hdr->next_pos; + } + spinlock_acquire(&slave->catch_lock); + if (slave->overrun) + { + slave->stats.n_overrun++; + slave->overrun = 0; + spinlock_release(&router->lock); + slave->cstate &= ~(CS_UPTODATE|CS_DIST); + spinlock_release(&slave->catch_lock); + blr_slave_catchup(router, slave); + spinlock_acquire(&router->lock); + slave = router->slaves; + if (slave) + continue; + else + break; + } + else + { + slave->cstate &= ~CS_DIST; + } + spinlock_release(&slave->catch_lock); + } + else if ((slave->binlog_pos > hdr->next_pos - hdr->event_size) + && strcmp(slave->binlogfile, router->binlog_name) == 0) + { + LOGIF(LE, (skygw_log_write_flush(LOGFILE_ERROR, + "Slave %d is ahead of expected position %s@%d. " + "Expected position %d", + slave->serverid, slave->binlogfile, + slave->binlog_pos, + hdr->next_pos - hdr->event_size))); + } + else if ((hdr->event_type != ROTATE_EVENT) + && (slave->binlog_pos != hdr->next_pos - hdr->event_size || + strcmp(slave->binlogfile, router->binlog_name) != 0)) + { + /* Check slave is in catchup mode and if not + * force it to go into catchup mode. + */ + if (slave->cstate & CS_UPTODATE) + { + spinlock_release(&router->lock); + LOGIF(LD, (skygw_log_write_flush(LOGFILE_DEBUG, + "Force slave %d into catchup mode %s@%d\n", + slave->serverid, slave->binlogfile, + slave->binlog_pos))); + spinlock_acquire(&slave->catch_lock); + slave->cstate &= ~(CS_UPTODATE|CS_DIST); + spinlock_release(&slave->catch_lock); + blr_slave_catchup(router, slave); + spinlock_acquire(&router->lock); + slave = router->slaves; + if (slave) + continue; + else + break; + } + } + } + + slave = slave->next; + } + spinlock_release(&router->lock); +} + +static void +blr_log_packet(logfile_id_t file, char *msg, uint8_t *ptr, int len) +{ +char buf[400], *bufp; +int i; + + bufp = buf; + bufp += sprintf(bufp, "%s length = %d: ", msg, len); + for (i = 0; i < len && i < 40; i++) + bufp += sprintf(bufp, "0x%02x ", ptr[i]); + if (i < len) + skygw_log_write_flush(file, "%s...\n", buf); + else + skygw_log_write_flush(file, "%s\n", buf); + +} diff --git a/server/modules/routing/binlog/blr_slave.c b/server/modules/routing/binlog/blr_slave.c new file mode 100644 index 000000000..176efbe4c --- /dev/null +++ b/server/modules/routing/binlog/blr_slave.c @@ -0,0 +1,944 @@ +/* + * This file is distributed as part of MaxScale. It is free + * software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, + * version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright SkySQL Ab 2014 + */ + +/** + * @file blr_slave.c - contains code for the router to slave communication + * + * The binlog router is designed to be used in replication environments to + * increase the replication fanout of a master server. It provides a transparant + * mechanism to read the binlog entries for multiple slaves while requiring + * only a single connection to the actual master to support the slaves. + * + * The current prototype implement is designed to support MySQL 5.6 and has + * a number of limitations. This prototype is merely a proof of concept and + * should not be considered production ready. + * + * @verbatim + * Revision History + * + * Date Who Description + * 14/04/2014 Mark Riddoch Initial implementation + * + * @endverbatim + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + + +static uint32_t extract_field(uint8_t *src, int bits); +static void encode_value(unsigned char *data, unsigned int value, int len); +static int blr_slave_query(ROUTER_INSTANCE *router, ROUTER_SLAVE *slave, GWBUF *queue); +static int blr_slave_replay(ROUTER_INSTANCE *router, ROUTER_SLAVE *slave, GWBUF *master); +static void blr_slave_send_error(ROUTER_INSTANCE *router, ROUTER_SLAVE *slave, char *msg); +static int blr_slave_send_timestamp(ROUTER_INSTANCE *router, ROUTER_SLAVE *slave); +static int blr_slave_register(ROUTER_INSTANCE *router, ROUTER_SLAVE *slave, GWBUF *queue); +static int blr_slave_binlog_dump(ROUTER_INSTANCE *router, ROUTER_SLAVE *slave, GWBUF *queue); +int blr_slave_catchup(ROUTER_INSTANCE *router, ROUTER_SLAVE *slave); +static uint8_t *blr_build_header(GWBUF *pkt, REP_HEADER *hdr); +static int blr_slave_callback(DCB *dcb, DCB_REASON reason, void *data); + +extern int lm_enabled_logfiles_bitmask; + +/** + * Process a request packet from the slave server. + * + * The router can handle a limited subset of requests from the slave, these + * include a subset of general SQL queries, a slave registeration command and + * the binlog dump command. + * + * The strategy for responding to these commands is to use caches responses + * for the the same commands that have previously been made to the real master + * if this is possible, if it is not then the router itself will synthesize a + * response. + * + * @param router The router instance this defines the master for this replication chain + * @param slave The slave specific data + * @param queue The incoming request packet + */ +int +blr_slave_request(ROUTER_INSTANCE *router, ROUTER_SLAVE *slave, GWBUF *queue) +{ + if (slave->state < 0 || slave->state > BLRS_MAXSTATE) + { + LOGIF(LE, (skygw_log_write( + LOGFILE_ERROR, "Invalid slave state machine state (%d) for binlog router.\n", + slave->state))); + gwbuf_consume(queue, gwbuf_length(queue)); + return 0; + } + + atomic_add(&slave->stats.n_requests, 1); + switch (MYSQL_COMMAND(queue)) + { + case COM_QUERY: + return blr_slave_query(router, slave, queue); + break; + case COM_REGISTER_SLAVE: + return blr_slave_register(router, slave, queue); + break; + case COM_BINLOG_DUMP: + return blr_slave_binlog_dump(router, slave, queue); + break; + case COM_QUIT: + LOGIF(LD, (skygw_log_write(LOGFILE_DEBUG, + "COM_QUIT received from slave with server_id %d\n", + slave->serverid))); + break; + default: + LOGIF(LE, (skygw_log_write( + LOGFILE_ERROR, + "Unexpected MySQL Command (%d) received from slave\n", + MYSQL_COMMAND(queue)))); + break; + } + return 0; +} + +/** + * Handle a query from the slave. This is expected to be one of the "standard" + * queries we expect as part of the registraton process. Most of these can + * be dealt with by replying the stored responses we got from the master + * when MaxScale registered as a slave. The exception to the rule is the + * request to obtain the current timestamp value of the server. + * + * Five select statements are currently supported: + * SELECT UNIX_TIMESTAMP(); + * SELECT @master_binlog_checksum + * SELECT @@GLOBAL.GTID_MODE + * SELECT VERSION() + * SELECT 1 + * + * Two show commands are supported: + * SHOW VARIABLES LIKE 'SERVER_ID' + * SHOW VARIABLES LIKE 'SERVER_UUID' + * + * Five set commands are supported: + * SET @master_binlog_checksum = @@global.binlog_checksum + * SET @master_heartbeat_period=... + * SET @slave_slave_uuid=... + * SET NAMES latin1 + * SET NAMES utf8 + * + * @param router The router instance this defines the master for this replication chain + * @param slave The slave specific data + * @param queue The incoming request packet + * @return Non-zero if data has been sent + */ +static int +blr_slave_query(ROUTER_INSTANCE *router, ROUTER_SLAVE *slave, GWBUF *queue) +{ +char *qtext, *query_text; +char *sep = " ,="; +char *word, *brkb; +int query_len; + + qtext = GWBUF_DATA(queue); + query_len = extract_field((uint8_t *)qtext, 24) - 1; + qtext += 5; // Skip header and first byte of the payload + query_text = strndup(qtext, query_len); + + LOGIF(LT, (skygw_log_write( + LOGFILE_TRACE, "Execute statement from the slave '%s'\n", query_text))); + /* + * Implement a very rudimental "parsing" of the query text by extarcting the + * words from the statement and matchng them against the subset of queries we + * are expecting from the slave. We already have responses to these commands, + * except for the select of UNIX_TIMESTAMP(), that we have saved from MaxScale's + * own interaction with the real master. We simply replay these saved responses + * to the slave. + */ + word = strtok_r(query_text, sep, &brkb); + if (strcasecmp(word, "SELECT") == 0) + { + word = strtok_r(NULL, sep, &brkb); + if (strcasecmp(word, "UNIX_TIMESTAMP()") == 0) + { + free(query_text); + return blr_slave_send_timestamp(router, slave); + } + else if (strcasecmp(word, "@master_binlog_checksum") == 0) + { + free(query_text); + return blr_slave_replay(router, slave, router->saved_master.chksum2); + } + else if (strcasecmp(word, "@@GLOBAL.GTID_MODE") == 0) + { + free(query_text); + return blr_slave_replay(router, slave, router->saved_master.gtid_mode); + } + else if (strcasecmp(word, "1") == 0) + { + free(query_text); + return blr_slave_replay(router, slave, router->saved_master.select1); + } + else if (strcasecmp(word, "VERSION()") == 0) + { + free(query_text); + return blr_slave_replay(router, slave, router->saved_master.selectver); + } + } + else if (strcasecmp(word, "SHOW") == 0) + { + word = strtok_r(NULL, sep, &brkb); + if (strcasecmp(word, "VARIABLES") == 0) + { + word = strtok_r(NULL, sep, &brkb); + if (strcasecmp(word, "LIKE") == 0) + { + word = strtok_r(NULL, sep, &brkb); + if (strcasecmp(word, "'SERVER_ID'") == 0) + { + free(query_text); + return blr_slave_replay(router, slave, router->saved_master.server_id); + } + else if (strcasecmp(word, "'SERVER_UUID'") == 0) + { + free(query_text); + return blr_slave_replay(router, slave, router->saved_master.uuid); + } + } + } + } + else if (strcasecmp(query_text, "SET") == 0) + { + word = strtok_r(NULL, sep, &brkb); + if (strcasecmp(word, "@master_heartbeat_period") == 0) + { + free(query_text); + return blr_slave_replay(router, slave, router->saved_master.heartbeat); + } + else if (strcasecmp(word, "@master_binlog_checksum") == 0) + { + word = strtok_r(NULL, sep, &brkb); + if (strcasecmp(word, "'none'") == 0) + slave->nocrc = 1; + else + slave->nocrc = 0; + free(query_text); + return blr_slave_replay(router, slave, router->saved_master.chksum1); + } + else if (strcasecmp(word, "@slave_uuid") == 0) + { + free(query_text); + return blr_slave_replay(router, slave, router->saved_master.setslaveuuid); + } + else if (strcasecmp(word, "NAMES") == 0) + { + word = strtok_r(NULL, sep, &brkb); + if (strcasecmp(word, "latin1") == 0) + { + free(query_text); + return blr_slave_replay(router, slave, router->saved_master.setnames); + } + else if (strcasecmp(word, "utf8") == 0) + { + free(query_text); + return blr_slave_replay(router, slave, router->saved_master.utf8); + } + } + } + free(query_text); + + query_text = strndup(qtext, query_len); + LOGIF(LE, (skygw_log_write( + LOGFILE_ERROR, "Unexpected query from slave server %s\n", query_text))); + free(query_text); + blr_slave_send_error(router, slave, "Unexpected SQL query received from slave."); + return 0; +} + + +/** + * Send a reply to a command we have received from the slave. The reply itself + * is merely a copy of a previous message we received from the master when we + * registered as a slave. Hence we just replay this saved reply. + * + * @param router The binlog router instance + * @param slave The slave server to which we are sending the response + * @param master The saved master response + * @return Non-zero if data was sent + */ +static int +blr_slave_replay(ROUTER_INSTANCE *router, ROUTER_SLAVE *slave, GWBUF *master) +{ +GWBUF *clone; + + if (!master) + return 0; + if ((clone = gwbuf_clone(master)) != NULL) + { + return slave->dcb->func.write(slave->dcb, clone); + } + else + { + LOGIF(LE, (skygw_log_write(LOGFILE_ERROR, + "Failed to clone server response to send to slave.\n"))); + return 0; + } +} + +/** + * Construct an error response + * + * @param router The router instance + * @param slave The slave server instance + * @param msg The error message to send + */ +static void +blr_slave_send_error(ROUTER_INSTANCE *router, ROUTER_SLAVE *slave, char *msg) +{ +GWBUF *pkt; +unsigned char *data; +int len; + + if ((pkt = gwbuf_alloc(strlen(msg) + 13)) == NULL) + return; + data = GWBUF_DATA(pkt); + len = strlen(msg) + 1; + encode_value(&data[0], len, 24); // Payload length + data[3] = 0; // Sequence id + // Payload + data[4] = 0xff; // Error indicator + data[5] = 0; // Error Code + data[6] = 0; // Error Code + strncpy((char *)&data[7], "#00000", 6); + memcpy(&data[13], msg, strlen(msg)); // Error Message + slave->dcb->func.write(slave->dcb, pkt); +} + +/* + * Some standard packets that have been captured from a network trace of server + * interactions. These packets are the schema definition sent in response to + * a SELECT UNIX_TIMESTAMP() statement and the EOF packet that marks the end + * of transmission of the result set. + */ +static uint8_t timestamp_def[] = { + 0x01, 0x00, 0x00, 0x01, 0x01, 0x26, 0x00, 0x00, 0x02, 0x03, 0x64, 0x65, 0x66, 0x00, 0x00, 0x00, + 0x10, 0x55, 0x4e, 0x49, 0x58, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x53, 0x54, 0x41, 0x4d, 0x50, 0x28, + 0x29, 0x00, 0x0c, 0x3f, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x08, 0x81, 0x00, 0x00, 0x00, 0x00, 0x05, + 0x00, 0x00, 0x03, 0xfe, 0x00, 0x00, 0x02, 0x00 +}; +static uint8_t timestamp_eof[] = { 0x05, 0x00, 0x00, 0x05, 0xfe, 0x00, 0x00, 0x02, 0x00 }; + +/** + * Send a response to a "SELECT UNIX_TIMESTAMP()" request. This differs from the other + * requests since we do not save a copy of the original interaction with the master + * and simply replay it. We want to always send the current time. We have stored a typcial + * response, which gives us the schema information normally returned. This is sent to the + * client and then we add a dynamic part that will insert the current timestamp data. + * Finally we send a preprepaed EOF packet to end the response stream. + * + * @param router The binlog router instance + * @param slave The slave server to which we are sending the response + * @return Non-zero if data was sent + */ +static int +blr_slave_send_timestamp(ROUTER_INSTANCE *router, ROUTER_SLAVE *slave) +{ +GWBUF *pkt; +char timestamp[20]; +uint8_t *ptr; +int len, ts_len; + + sprintf(timestamp, "%ld", time(0)); + ts_len = strlen(timestamp); + len = sizeof(timestamp_def) + sizeof(timestamp_eof) + 5 + ts_len; + if ((pkt = gwbuf_alloc(len)) == NULL) + return 0; + ptr = GWBUF_DATA(pkt); + memcpy(ptr, timestamp_def, sizeof(timestamp_def)); // Fixed preamble + ptr += sizeof(timestamp_def); + encode_value(ptr, ts_len + 1, 24); // Add length of data packet + ptr += 3; + *ptr++ = 0x04; // Sequence number in response + *ptr++ = ts_len; // Length of result string + strncpy((char *)ptr, timestamp, ts_len); // Result string + ptr += ts_len; + memcpy(ptr, timestamp_eof, sizeof(timestamp_eof)); // EOF packet to terminate result + return slave->dcb->func.write(slave->dcb, pkt); +} + +/** + * Process a slave replication registration message. + * + * We store the various bits of information the slave gives us and generate + * a reply message. + * + * @param router The router instance + * @param slave The slave server + * @param queue The BINLOG_DUMP packet + * @return Non-zero if data was sent + */ +static int +blr_slave_register(ROUTER_INSTANCE *router, ROUTER_SLAVE *slave, GWBUF *queue) +{ +GWBUF *resp; +uint8_t *ptr; +int len, slen; + + ptr = GWBUF_DATA(queue); + len = extract_field(ptr, 24); + ptr += 4; // Skip length and sequence number + if (*ptr++ != COM_REGISTER_SLAVE) + return 0; + slave->serverid = extract_field(ptr, 32); + ptr += 4; + slen = *ptr++; + if (slen != 0) + { + slave->hostname = strndup((char *)ptr, slen); + ptr += slen; + } + else + slave->hostname = NULL; + slen = *ptr++; + if (slen != 0) + { + ptr += slen; + slave->user = strndup((char *)ptr, slen); + } + else + slave->user = NULL; + slen = *ptr++; + if (slen != 0) + { + slave->passwd = strndup((char *)ptr, slen); + ptr += slen; + } + else + slave->passwd = NULL; + slave->port = extract_field(ptr, 16); + ptr += 2; + slave->rank = extract_field(ptr, 32); + + /* + * Now construct a response + */ + if ((resp = gwbuf_alloc(11)) == NULL) + return 0; + ptr = GWBUF_DATA(resp); + encode_value(ptr, 7, 24); // Payload length + ptr += 3; + *ptr++ = 1; // Sequence number + encode_value(ptr, 0, 24); + ptr += 3; + encode_value(ptr, slave->serverid, 32); + slave->state = BLRS_REGISTERED; + return slave->dcb->func.write(slave->dcb, resp); +} + +/** + * Process a COM_BINLOG_DUMP message from the slave. This is the + * final step in the process of registration. The new master, MaxScale + * must send a response packet and generate a fake BINLOG_ROTATE event + * with the binlog file requested by the slave. And then send a + * FORMAT_DESCRIPTION_EVENT that has been saved from the real master. + * + * Once send MaxScale must continue to send binlog events to the slave. + * + * @param router The router instance + * @param slave The slave server + * @param queue The BINLOG_DUMP packet + * @return The number of bytes written to the slave + */ +static int +blr_slave_binlog_dump(ROUTER_INSTANCE *router, ROUTER_SLAVE *slave, GWBUF *queue) +{ +GWBUF *resp; +uint8_t *ptr; +int len, flags, serverid, rval; +REP_HEADER hdr; +uint32_t chksum; + + ptr = GWBUF_DATA(queue); + len = extract_field(ptr, 24); + ptr += 4; // Skip length and sequence number + if (*ptr++ != COM_BINLOG_DUMP) + { + LOGIF(LE, (skygw_log_write( + LOGFILE_ERROR, + "blr_slave_binlog_dump expected a COM_BINLOG_DUMP but received %d\n", + *(ptr-1)))); + return 0; + } + + slave->binlog_pos = extract_field(ptr, 32); + ptr += 4; + flags = extract_field(ptr, 16); + ptr += 2; + serverid = extract_field(ptr, 32); + ptr += 4; + strncpy(slave->binlogfile, (char *)ptr, BINLOG_FNAMELEN); + + slave->state = BLRS_DUMPING; + slave->seqno = 1; + + if (slave->nocrc) + len = 0x2b; + else + len = 0x2f; + + // Build a fake rotate event + resp = gwbuf_alloc(len + 5); + hdr.payload_len = len + 1; + hdr.seqno = slave->seqno++; + hdr.ok = 0; + hdr.timestamp = 0L; + hdr.event_type = ROTATE_EVENT; + hdr.serverid = router->masterid; + hdr.event_size = len; + hdr.next_pos = 0; + hdr.flags = 0x20; + ptr = blr_build_header(resp, &hdr); + encode_value(ptr, slave->binlog_pos, 64); + ptr += 8; + memcpy(ptr, slave->binlogfile, BINLOG_FNAMELEN); + ptr += BINLOG_FNAMELEN; + + if (!slave->nocrc) + { + /* + * Now add the CRC to the fake binlog rotate event. + * + * The algorithm is first to compute the checksum of an empty buffer + * and then the checksum of the event portion of the message, ie we do not + * include the length, sequence number and ok byte that makes up the first + * 5 bytes of the message. We also do not include the 4 byte checksum itself. + */ + chksum = crc32(0L, NULL, 0); + chksum = crc32(chksum, GWBUF_DATA(resp) + 5, hdr.event_size - 4); + encode_value(ptr, chksum, 32); + } + + rval = slave->dcb->func.write(slave->dcb, resp); + + /* Send the FORMAT_DESCRIPTION_EVENT */ + if (router->saved_master.fde_event) + { + resp = gwbuf_alloc(router->saved_master.fde_len + 5); + ptr = GWBUF_DATA(resp); + encode_value(ptr, router->saved_master.fde_len + 1, 24); // Payload length + ptr += 3; + *ptr++ = slave->seqno++; + *ptr++ = 0; // OK + memcpy(ptr, router->saved_master.fde_event, router->saved_master.fde_len); + encode_value(ptr, time(0), 32); // Overwrite timestamp + /* + * Since we have changed the timestamp we must recalculate the CRC + * + * Position ptr to the start of the event header, + * calculate a new checksum + * and write it into the header + */ + ptr = GWBUF_DATA(resp) + 5 + router->saved_master.fde_len - 4; + chksum = crc32(0L, NULL, 0); + chksum = crc32(chksum, GWBUF_DATA(resp) + 5, router->saved_master.fde_len - 4); + encode_value(ptr, chksum, 32); + rval = slave->dcb->func.write(slave->dcb, resp); + } + + slave->dcb->low_water = router->low_water; + slave->dcb->high_water = router->high_water; + dcb_add_callback(slave->dcb, DCB_REASON_LOW_WATER, blr_slave_callback, slave); + dcb_add_callback(slave->dcb, DCB_REASON_DRAINED, blr_slave_callback, slave); + + if (slave->binlog_pos != router->binlog_position || + strcmp(slave->binlogfile, router->binlog_name) != 0) + { + spinlock_acquire(&slave->catch_lock); + slave->cstate &= ~CS_UPTODATE; + spinlock_release(&slave->catch_lock); + rval = blr_slave_catchup(router, slave); + } + + return rval; +} + +/** + * Extract a numeric field from a packet of the specified number of bits, + * the number of bits must be a multiple of 8. + * + * @param src The raw packet source + * @param bits The number of bits to extract (multiple of 8) + * @return The extracted value + */ +static uint32_t +extract_field(uint8_t *src, int bits) +{ +uint32_t rval = 0, shift = 0; + + while (bits > 0) + { + rval |= (*src++) << shift; + shift += 8; + bits -= 8; + } + return rval; +} + +/** + * Encode a value into a number of bits in a MySQL packet + * + * @param data Pointer to location in target packet + * @param value The value to encode into the buffer + * @param len Number of bits to encode value into + */ +static void +encode_value(unsigned char *data, unsigned int value, int len) +{ + while (len > 0) + { + *data++ = value & 0xff; + value >>= 8; + len -= 8; + } +} + + +/** + * Populate a header structure for a replication message from a GWBUF structure. + * + * @param pkt The incoming packet in a GWBUF chain + * @param hdr The packet header to populate + * @return A pointer to the first byte following the event header + */ +static uint8_t * +blr_build_header(GWBUF *pkt, REP_HEADER *hdr) +{ +uint8_t *ptr; + + ptr = GWBUF_DATA(pkt); + + encode_value(ptr, hdr->payload_len, 24); + ptr += 3; + *ptr++ = hdr->seqno; + *ptr++ = hdr->ok; + encode_value(ptr, hdr->timestamp, 32); + ptr += 4; + *ptr++ = hdr->event_type; + encode_value(ptr, hdr->serverid, 32); + ptr += 4; + encode_value(ptr, hdr->event_size, 32); + ptr += 4; + encode_value(ptr, hdr->next_pos, 32); + ptr += 4; + encode_value(ptr, hdr->flags, 16); + ptr += 2; + + return ptr; +} + +/** + * We have a registered slave that is behind the current leading edge of the + * binlog. We must replay the log entries to bring this node up to speed. + * + * There may be a large numebr of records to send to the slave, the process + * is triggered by the slave COM_BINLOG_DUMP message and all the events must + * be sent without receiving any new event. This measn there is no trigger into + * MaxScale other than this initial message. However, if we simply send all the + * events we end up with an extremely long write queue on the DCB and risk running + * the server out of resources. + * + * To resolve this the concept of high and low water marks within the DCB has been + * added, with the ability for the DCB code to call user defined callbacks when the + * write queue is completely drained, when it crosses above the high water mark and + * when it crosses below the low water mark. + * + * The blr_slave_catchup routine will send binlog events to the slave until the high + * water mark is reached, at which point it will return. Later, when a low water mark + * callback is generated by the code that drains the DCB of data the blr_slave_catchup + * routine will again be called to write more events. The process is repeated until + * the slave has caught up with the master. + * + * Note: an additional check that the DCB is still above the low water mark is done + * prior to the return from this function to allow for any delays due to the call to + * the close system call, since this may cause thread rescheduling. + * + * @param router The binlog router + * @param slave The slave that is behind + * @return The number of bytes written + */ +int +blr_slave_catchup(ROUTER_INSTANCE *router, ROUTER_SLAVE *slave) +{ +GWBUF *head, *record; +REP_HEADER hdr; +int written, fd, rval = 1, burst = 0; +uint8_t *ptr; +struct timespec req; + + + spinlock_acquire(&slave->catch_lock); + slave->cstate &= ~CS_EXPECTCB; + spinlock_release(&slave->catch_lock); +doitagain: + /* + * We have a slightly complex syncronisation mechansim here, + * we need to make sure that we do not have multiple threads + * running the catchup loop, but we need to be very careful + * that we do not loose a call that is coming via a callback + * call as this will stall the binlog catchup process. + * + * We don't want to simply use a traditional mutex here for + * the loop, since this would block a MaxScale thread for + * an unacceptable length of time. + * + * We have two status bits, the CS_READING that says we are + * in the outer loop and the CS_INNERLOOP, to say we are in + * the inner loop. + * + * If just CS_READING is set the other thread may be about to + * enter the inner loop or may be about to exit the function + * completely. Therefore we have to wait to see if CS_READING + * is cleared or CS_INNERLOOP is set. + * + * If CS_READING gets cleared then this thread should proceed + * into the loop. + * + * If CS_INNERLOOP get's set then this thread does not need to + * proceed. + * + * If CS_READING is not set then this thread simply enters the + * loop. + */ + req.tv_sec = 0; + req.tv_nsec = 1000; + spinlock_acquire(&slave->catch_lock); + if (slave->cstate & CS_UPTODATE) + { + LOGIF(LM, (skygw_log_write(LOGFILE_MESSAGE, + "blr_slave_catchup called with up to date slave %d at " + "%s@%d. Reading position %s@%d\n", + slave->serverid, slave->binlogfile, + slave->binlog_pos, router->binlog_name, + router->binlog_position))); + slave->stats.n_alreadyupd++; + spinlock_release(&slave->catch_lock); + return 1; + } + while (slave->cstate & CS_READING) + { + // Wait until we know what the other thread is doing + while ((slave->cstate & (CS_READING|CS_INNERLOOP)) == CS_READING) + { + spinlock_release(&slave->catch_lock); + nanosleep(&req, NULL); + spinlock_acquire(&slave->catch_lock); + } + // Other thread is in the innerloop + if ((slave->cstate & (CS_READING|CS_INNERLOOP)) == (CS_READING|CS_INNERLOOP)) + { + spinlock_release(&slave->catch_lock); + LOGIF(LM, (skygw_log_write( + LOGFILE_MESSAGE, + "blr_slave_catchup thread returning due to " + "lock being held by another thread. %s@%d\n", + slave->binlogfile, + slave->binlog_pos))); + slave->stats.n_catchupnr++; + return 1; // We cheat here and return 1 because otherwise + // an error would be sent and we do not want that + } + + /* Release the lock for a short time to allow the other + * thread to exit the outer reading loop. + */ + spinlock_release(&slave->catch_lock); + nanosleep(&req, NULL); + spinlock_acquire(&slave->catch_lock); + } + if (slave->pthread) + LOGIF(LD, (skygw_log_write(LOGFILE_DEBUG, "Multiple threads sending to same thread.\n"))); + slave->pthread = pthread_self(); + slave->cstate |= CS_READING; + spinlock_release(&slave->catch_lock); + + if (DCB_ABOVE_HIGH_WATER(slave->dcb)) + LOGIF(LT, (skygw_log_write(LOGFILE_TRACE, "blr_slave_catchup above high water on entry.\n"))); + + do { + if ((fd = blr_open_binlog(router, slave->binlogfile)) == -1) + { + spinlock_acquire(&slave->catch_lock); + slave->cstate &= ~CS_READING; + spinlock_release(&slave->catch_lock); + LOGIF(LE, (skygw_log_write( + LOGFILE_ERROR, + "blr_slave_catchup failed to open binlog file %s\n", + slave->binlogfile))); + return 0; + } + atomic_add(&slave->stats.n_bursts, 1); + spinlock_acquire(&slave->catch_lock); + slave->cstate |= CS_INNERLOOP; + spinlock_release(&slave->catch_lock); + while ((!DCB_ABOVE_HIGH_WATER(slave->dcb)) && + (record = blr_read_binlog(fd, slave->binlog_pos, &hdr)) != NULL) + { +if (hdr.event_size > DEF_HIGH_WATER) slave->stats.n_above++; + head = gwbuf_alloc(5); + ptr = GWBUF_DATA(head); + encode_value(ptr, hdr.event_size + 1, 24); + ptr += 3; + *ptr++ = slave->seqno++; + *ptr++ = 0; // OK + head = gwbuf_append(head, record); + if (hdr.event_type == ROTATE_EVENT) + { + close(fd); + blr_slave_rotate(slave, GWBUF_DATA(record)); + if ((fd = blr_open_binlog(router, slave->binlogfile)) == -1) + { + LOGIF(LE, (skygw_log_write( + LOGFILE_ERROR, + "blr_slave_catchup failed to open binlog file %s\n", + slave->binlogfile))); + break; + } + } + written = slave->dcb->func.write(slave->dcb, head); + if (written && hdr.event_type != ROTATE_EVENT) + { + slave->binlog_pos = hdr.next_pos; + } + rval = written; + atomic_add(&slave->stats.n_events, 1); + burst++; + } + if (record == NULL) + slave->stats.n_failed_read++; + spinlock_acquire(&slave->catch_lock); + slave->cstate &= ~CS_INNERLOOP; + spinlock_release(&slave->catch_lock); + + close(fd); + } while (record && DCB_BELOW_LOW_WATER(slave->dcb)); + if (record) + { + atomic_add(&slave->stats.n_flows, 1); + spinlock_acquire(&slave->catch_lock); + slave->cstate |= CS_EXPECTCB; + spinlock_release(&slave->catch_lock); + } + else + { + int state_change = 0; + spinlock_acquire(&slave->catch_lock); + if ((slave->cstate & CS_UPTODATE) == 0) + { + atomic_add(&slave->stats.n_upd, 1); + slave->cstate |= CS_UPTODATE; + state_change = 1; + } + spinlock_release(&slave->catch_lock); + if (state_change) + LOGIF(LM, (skygw_log_write(LOGFILE_MESSAGE, + "blr_slave_catchup slave is up to date %s, %u\n", + slave->binlogfile, slave->binlog_pos))); + } + spinlock_acquire(&slave->catch_lock); +#if 0 +if (slave->pthread != pthread_self()) +{ + LOGIF(LE, (skygw_log_write(LOGFILE_ERROR, "Multple threads in catchup for same slave: %x and %x\n", slave->pthread, pthread_self()))); +abort(); +} +#endif + slave->pthread = 0; +#if 0 +if (DCB_BELOW_LOW_WATER(slave->dcb) && slave->binlog_pos != router->binlog_position) abort(); +#endif + slave->cstate &= ~CS_READING; + spinlock_release(&slave->catch_lock); +if (DCB_BELOW_LOW_WATER(slave->dcb) && slave->binlog_pos != router->binlog_position) +{ + LOGIF(LE, (skygw_log_write(LOGFILE_ERROR, "Expected to be above low water\n"))); +goto doitagain; +} + return rval; +} + +/** + * The DCB callback used by the slave to obtain DCB_REASON_LOW_WATER callbacks + * when the server sends all the the queue data for a DCB. This is the mechanism + * that is used to implement the flow control mechanism for the sending of + * large quantities of binlog records during the catchup process. + * + * @param dcb The DCB of the slave connection + * @param reason The reason the callback was called + * @param data The user data, in this case the server structure + */ +static int +blr_slave_callback(DCB *dcb, DCB_REASON reason, void *data) +{ +ROUTER_SLAVE *slave = (ROUTER_SLAVE *)data; +ROUTER_INSTANCE *router = slave->router; + + if (reason == DCB_REASON_DRAINED) + { + if (slave->state == BLRS_DUMPING && + slave->binlog_pos != router->binlog_position) + { + atomic_add(&slave->stats.n_dcb, 1); + blr_slave_catchup(router, slave); + } + } + + if (reason == DCB_REASON_LOW_WATER) + { + if (slave->state == BLRS_DUMPING) + { + atomic_add(&slave->stats.n_cb, 1); + blr_slave_catchup(router, slave); + } + else + { + atomic_add(&slave->stats.n_cbna, 1); + } + } + return 0; +} + +/** + * Rotate the slave to the new binlog file + * + * @param slave The slave instance + * @param ptr The rotate event (minux header and OK byte) + */ +void +blr_slave_rotate(ROUTER_SLAVE *slave, uint8_t *ptr) +{ + ptr += 19; // Skip header + slave->binlog_pos = extract_field(ptr, 32); + slave->binlog_pos += (extract_field(ptr+4, 32) << 32); + memcpy(slave->binlogfile, ptr + 8, BINLOG_FNAMELEN); + slave->binlogfile[BINLOG_FNAMELEN] = 0; +} diff --git a/server/modules/routing/debugcmd.c b/server/modules/routing/debugcmd.c index a9a5da12a..f238e16e6 100644 --- a/server/modules/routing/debugcmd.c +++ b/server/modules/routing/debugcmd.c @@ -160,6 +160,10 @@ struct subcommand showoptions[] = { "Show all active sessions in MaxScale", "Show all active sessions in MaxScale", {0, 0, 0} }, + { "threads", 0, dShowThreads, + "Show the status of the polling threads in MaxScale", + "Show the status of the polling threads in MaxScale", + {0, 0, 0} }, { "users", 0, telnetdShowUsers, "Show statistics and user names for the debug interface", "Show statistics and user names for the debug interface", @@ -208,6 +212,10 @@ struct subcommand listoptions[] = { "List all the active sessions within MaxScale", "List all the active sessions within MaxScale", {0, 0, 0} }, + { "threads", 0, dShowThreads, + "List the status of the polling threads in MaxScale", + "List the status of the polling threads in MaxScale", + {0, 0, 0} }, { NULL, 0, NULL, NULL, NULL, {0, 0, 0} } }; @@ -831,6 +839,7 @@ static struct { { "master", SERVER_MASTER }, { "slave", SERVER_SLAVE }, { "synced", SERVER_JOINED }, + { "ndb", SERVER_NDB }, { "maintenance", SERVER_MAINT }, { "maint", SERVER_MAINT }, { NULL, 0 } diff --git a/server/modules/routing/readconnroute.c b/server/modules/routing/readconnroute.c index 0fc5fd4ce..a5c648c67 100644 --- a/server/modules/routing/readconnroute.c +++ b/server/modules/routing/readconnroute.c @@ -311,6 +311,11 @@ char *weightby; inst->bitmask |= (SERVER_JOINED); inst->bitvalue |= SERVER_JOINED; } + else if (!strcasecmp(options[i], "ndb")) + { + inst->bitmask |= (SERVER_NDB); + inst->bitvalue |= SERVER_NDB; + } else { LOGIF(LM, (skygw_log_write( @@ -318,7 +323,7 @@ char *weightby; "* Warning : Unsupported router " "option \'%s\' for readconnroute. " "Expected router options are " - "[slave|master|synced]", + "[slave|master|synced|ndb]", options[i]))); } } diff --git a/server/modules/routing/readwritesplit/readwritesplit.c b/server/modules/routing/readwritesplit/readwritesplit.c index 1c22d4979..47e21a029 100644 --- a/server/modules/routing/readwritesplit/readwritesplit.c +++ b/server/modules/routing/readwritesplit/readwritesplit.c @@ -31,6 +31,7 @@ #include #include #include +#include #include MODULE_INFO info = { @@ -97,6 +98,12 @@ static int rses_get_max_slavecount(ROUTER_CLIENT_SES* rses, int router_nservers static int rses_get_max_replication_lag(ROUTER_CLIENT_SES* rses); static backend_ref_t* get_bref_from_dcb(ROUTER_CLIENT_SES* rses, DCB* dcb); +static route_target_t get_route_target ( + skygw_query_type_t qtype, + bool trx_active, + target_t use_sql_variables_in, + HINT* hint); + static uint8_t getCapabilities (ROUTER* inst, void* router_session); #if defined(NOT_USED) @@ -153,7 +160,9 @@ static bool select_connect_backend_servers( static bool get_dcb( DCB** dcb, ROUTER_CLIENT_SES* rses, - backend_type_t btype); + backend_type_t btype, + char* name, + int max_rlag); static void rwsplit_process_router_options( ROUTER_INSTANCE* router, @@ -272,6 +281,47 @@ static bool have_enough_servers( static SPINLOCK instlock; static ROUTER_INSTANCE* instances; +static int hashkeyfun(void* key); +static int hashcmpfun (void *, void *); + +static int hashkeyfun( + void* key) +{ + if(key == NULL){ + return 0; + } + unsigned int hash = 0,c = 0; + char* ptr = (char*)key; + while((c = *ptr++)){ + hash = c + (hash << 6) + (hash << 16) - hash; + } + return *(int *)key; +} + +static int hashcmpfun( + void* v1, + void* v2) +{ + char* i1 = (char*) v1; + char* i2 = (char*) v2; + + return strcmp(i1,i2); +} + +static void* hstrdup(void* fval) +{ + char* str = (char*)fval; + return strdup(str); +} + + +static void* hfree(void* fval) +{ + free (fval); + return NULL; +} + + /** * Implementation of the mandatory version entry point * @@ -316,7 +366,8 @@ static void refreshInstance( { CONFIG_PARAMETER* param; bool refresh_single; - + config_param_type_t paramtype; + if (singleparam != NULL) { param = singleparam; @@ -327,39 +378,81 @@ static void refreshInstance( param = router->service->svc_config_param; refresh_single = false; } - + paramtype = config_get_paramtype(param); + while (param != NULL) { - config_param_type_t paramtype; - - paramtype = config_get_paramtype(param); - + /** Catch unused parameter types */ + ss_dassert(paramtype == COUNT_TYPE || + paramtype == PERCENT_TYPE || + paramtype == SQLVAR_TARGET_TYPE); + if (paramtype == COUNT_TYPE) { if (strncmp(param->name, "max_slave_connections", MAX_PARAM_LEN) == 0) { + int val; + bool succp; + router->rwsplit_config.rw_max_slave_conn_percent = 0; - router->rwsplit_config.rw_max_slave_conn_count = - config_get_valint(param, NULL, paramtype); + + succp = config_get_valint(&val, param, NULL, paramtype); + + if (succp) + { + router->rwsplit_config.rw_max_slave_conn_count = val; + } } else if (strncmp(param->name, "max_slave_replication_lag", MAX_PARAM_LEN) == 0) { - router->rwsplit_config.rw_max_slave_replication_lag = - config_get_valint(param, NULL, paramtype); - } + int val; + bool succp; + + succp = config_get_valint(&val, param, NULL, paramtype); + + if (succp) + { + router->rwsplit_config.rw_max_slave_replication_lag = val; + } + } } else if (paramtype == PERCENT_TYPE) { if (strncmp(param->name, "max_slave_connections", MAX_PARAM_LEN) == 0) { + int val; + bool succp; + router->rwsplit_config.rw_max_slave_conn_count = 0; - router->rwsplit_config.rw_max_slave_conn_percent = - config_get_valint(param, NULL, paramtype); + + succp = config_get_valint(&val, param, NULL, paramtype); + + if (succp) + { + router->rwsplit_config.rw_max_slave_conn_percent = val; + } } } - + else if (paramtype == SQLVAR_TARGET_TYPE) + { + if (strncmp(param->name, + "use_sql_variables_in", + MAX_PARAM_LEN) == 0) + { + target_t valtarget; + bool succp; + + succp = config_get_valtarget(&valtarget, param, NULL, paramtype); + + if (succp) + { + router->rwsplit_config.rw_use_sql_variables_in = valtarget; + } + } + } + if (refresh_single) { break; @@ -409,6 +502,7 @@ static void refreshInstance( } } #endif /*< NOT_USED */ + } /** @@ -484,6 +578,11 @@ createInstance(SERVICE *service, char **options) } router->servers[nservers] = NULL; + /* + * Until we know otherwise assume we have some available slaves. + */ + router->available_slaves = true; + /* * If server weighting has been defined calculate the percentage * of load that will be sent to each server. This is only used for @@ -587,6 +686,14 @@ createInstance(SERVICE *service, char **options) refreshInstance(router, param); } router->rwsplit_version = service->svc_config_version; + /** Set default values */ + router->rwsplit_config.rw_use_sql_variables_in = CONFIG_SQL_VARIABLES_IN; + param = config_get_param(service->svc_config_param, "use_sql_variables_in"); + + if (param != NULL) + { + refreshInstance(router, param); + } /** * We have completed the creation of the router data, so now * insert this router into the linked list of routers @@ -636,6 +743,8 @@ static void* newSession( client_rses->rses_chk_top = CHK_NUM_ROUTER_SES; client_rses->rses_chk_tail = CHK_NUM_ROUTER_SES; #endif + + client_rses->router = router; /** * If service config has been changed, reload config from service to * router instance first. @@ -703,7 +812,7 @@ static void* newSession( backend_ref[i].bref_sescmd_cur.scmd_cur_ptr_property = &client_rses->rses_properties[RSES_PROP_TYPE_SESCMD]; backend_ref[i].bref_sescmd_cur.scmd_cur_cmd = NULL; - } + } max_nslaves = rses_get_max_slavecount(client_rses, router_nservers); max_slave_rlag = rses_get_max_replication_lag(client_rses); @@ -912,19 +1021,29 @@ static void freeSession( } /** - * Provide a pointer to a suitable backend dcb. + * Provide the router with a pointer to a suitable backend dcb. * Detect failures in server statuses and reselect backends if necessary. + * If name is specified, server name becomes primary selection criteria. + * + * @param p_dcb Address of the pointer to the resulting DCB + * @param rses Pointer to router client session + * @param btype Backend type + * @param name Name of the backend which is primarily searched. May be NULL. + * + * @return True if proper DCB was found, false otherwise. */ static bool get_dcb( DCB** p_dcb, ROUTER_CLIENT_SES* rses, - backend_type_t btype) + backend_type_t btype, + char* name, + int max_rlag) { backend_ref_t* backend_ref; int smallest_nconn = -1; int i; bool succp = false; - BACKEND *master_host = NULL; + BACKEND* master_host; CHK_CLIENT_RSES(rses); ss_dassert(p_dcb != NULL && *(p_dcb) == NULL); @@ -935,31 +1054,81 @@ static bool get_dcb( } backend_ref = rses->rses_backend_ref; - /* get root master from availbal servers */ + /** get root master from available servers */ master_host = get_root_master(backend_ref, rses->rses_nbackends); + if (name != NULL) /*< Choose backend by name from a hint */ + { + ss_dassert(btype != BE_MASTER); /*< Master dominates and no name should be passed with it */ + + for (i=0; irses_nbackends; i++) + { + BACKEND* b = backend_ref[i].bref_backend; + + /** + * To become chosen: + * backend must be in use, name must match, + * root master node must be found, + * backend's role must be either slave, relay + * server, or master. + */ + if (BREF_IS_IN_USE((&backend_ref[i])) && + (strncasecmp( + name, + b->backend_server->unique_name, + PATH_MAX) == 0) && + master_host != NULL && + (SERVER_IS_SLAVE(b->backend_server) || + SERVER_IS_RELAY_SERVER(b->backend_server) || + SERVER_IS_MASTER(b->backend_server))) + { + *p_dcb = backend_ref[i].bref_dcb; + succp = true; + ss_dassert(backend_ref[i].bref_dcb->state != DCB_STATE_ZOMBIE); + break; + } + } + if (succp) + { + goto return_succp; + } + } + if (btype == BE_SLAVE) { - for (i=0; irses_nbackends; i++) - { - BACKEND* b = backend_ref[i].bref_backend; - /* check slave bit, also for relay servers (Master & Servers) */ - if (BREF_IS_IN_USE((&backend_ref[i])) && - (SERVER_IS_SLAVE(b->backend_server) || SERVER_IS_RELAY_SERVER(b->backend_server)) && - (master_host != NULL && b->backend_server != master_host->backend_server) && - (smallest_nconn == -1 || - b->backend_conn_count < smallest_nconn)) - { - *p_dcb = backend_ref[i].bref_dcb; - smallest_nconn = b->backend_conn_count; - succp = true; - ss_dassert(backend_ref[i].bref_dcb->state != DCB_STATE_ZOMBIE); - } - } + for (i=0; irses_nbackends; i++) + { + BACKEND* b = backend_ref[i].bref_backend; + /** + * To become chosen: + * backend must be in use, + * root master node must be found, + * backend is not allowed to be the master, + * backend's role can be either slave or relay + * server and it must have least connections + * at the moment. + */ + if (BREF_IS_IN_USE((&backend_ref[i])) && + master_host != NULL && + b->backend_server != master_host->backend_server && + (max_rlag == MAX_RLAG_UNDEFINED || + (b->backend_server->rlag != MAX_RLAG_NOT_AVAILABLE && + b->backend_server->rlag <= max_rlag)) && + (SERVER_IS_SLAVE(b->backend_server) || + SERVER_IS_RELAY_SERVER(b->backend_server)) && + (smallest_nconn == -1 || + b->backend_conn_count < smallest_nconn)) + { + *p_dcb = backend_ref[i].bref_dcb; + smallest_nconn = b->backend_conn_count; + succp = true; + ss_dassert(backend_ref[i].bref_dcb->state != DCB_STATE_ZOMBIE); + } + } - if (!succp) + if (!succp) /*< No valid slave was found, search master next */ { - backend_ref = rses->rses_master_ref; + btype = BE_MASTER; if (BREF_IS_IN_USE(backend_ref)) { @@ -972,18 +1141,34 @@ static bool get_dcb( (master_host && (backend_ref->bref_backend->backend_server == master_host->backend_server)) && smallest_nconn == -1); - LOGIF(LE, (skygw_log_write_flush( - LOGFILE_ERROR, - "Warning : No slaves connected nor " - "available. Choosing master %s:%d " - "instead.", + if (rses->router->available_slaves) + { + rses->router->available_slaves = false; + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Warning : No slaves available " + "for the service %s. " + "Using master %s:%d " + "instead.", + rses->router->service->name, backend_ref->bref_backend->backend_server->name, backend_ref->bref_backend->backend_server->port))); + } } - } + } + else if (rses->router->available_slaves == false) + { + rses->router->available_slaves = true; + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "At least one slave has become avilable for " + "the service %s.", + rses->router->service->name))); + } ss_dassert(succp); } - else if (btype == BE_MASTER) + + if (btype == BE_MASTER) { for (i=0; irses_nbackends; i++) { @@ -998,10 +1183,419 @@ static bool get_dcb( } } } + return_succp: return succp; } +/** + * Examine the query type, transaction state and routing hints. Find out the + * target for query routing. + * + * @param qtype Type of query + * @param trx_active Is transacation active or not + * @param hint Pointer to list of hints attached to the query buffer + * + * @return bitfield including the routing target, or the target server name + * if the query would otherwise be routed to slave. + */ +static route_target_t get_route_target ( + skygw_query_type_t qtype, + bool trx_active, + target_t use_sql_variables_in, + HINT* hint) +{ + route_target_t target; + /** + * These queries are not affected by hints + */ + if (!trx_active && + (QUERY_IS_TYPE(qtype, QUERY_TYPE_PREPARE_STMT) || + QUERY_IS_TYPE(qtype, QUERY_TYPE_PREPARE_NAMED_STMT) || + /** Configured to allow writing variables to all nodes */ + (use_sql_variables_in == TYPE_ALL && + (QUERY_IS_TYPE(qtype, QUERY_TYPE_SESSION_WRITE) || + QUERY_IS_TYPE(qtype, QUERY_TYPE_GSYSVAR_WRITE))))) + { + /** hints don't affect on routing */ + target = TARGET_ALL; + } + /** + * Hints may affect on routing of the following queries + */ + else if (!trx_active && + (QUERY_IS_TYPE(qtype, QUERY_TYPE_READ) || /*< any SELECT */ + QUERY_IS_TYPE(qtype, QUERY_TYPE_USERVAR_READ)|| /*< read user var */ + QUERY_IS_TYPE(qtype, QUERY_TYPE_SYSVAR_READ) || /*< read sys var */ + QUERY_IS_TYPE(qtype, QUERY_TYPE_EXEC_STMT) || /*< prepared stmt exec */ + QUERY_IS_TYPE(qtype, QUERY_TYPE_GSYSVAR_READ))) /*< read global sys var */ + { + /** First set expected targets before evaluating hints */ + if (QUERY_IS_TYPE(qtype, QUERY_TYPE_READ) || + /** Configured to allow reading variables from slaves */ + (use_sql_variables_in == TYPE_ALL && + (QUERY_IS_TYPE(qtype, QUERY_TYPE_USERVAR_READ) || + QUERY_IS_TYPE(qtype, QUERY_TYPE_SYSVAR_READ) || + QUERY_IS_TYPE(qtype, QUERY_TYPE_GSYSVAR_READ)))) + { + target = TARGET_SLAVE; + } + else if (QUERY_IS_TYPE(qtype, QUERY_TYPE_EXEC_STMT) || + /** Configured not to allow reading variables from slaves */ + (use_sql_variables_in == TYPE_MASTER && + (QUERY_IS_TYPE(qtype, QUERY_TYPE_USERVAR_READ) || + QUERY_IS_TYPE(qtype, QUERY_TYPE_SYSVAR_READ)))) + { + target = TARGET_MASTER; + } + /** process routing hints */ + while (hint != NULL) + { + if (hint->type == HINT_ROUTE_TO_MASTER) + { + target = TARGET_MASTER; /*< override */ + LOGIF(LT, (skygw_log_write( + LOGFILE_TRACE, + "Hint: route to master."))); + break; + } + else if (hint->type == HINT_ROUTE_TO_NAMED_SERVER) + { + /** + * Searching for a named server. If it can't be + * found, the oroginal target is chosen. + */ + target |= TARGET_NAMED_SERVER; + LOGIF(LT, (skygw_log_write( + LOGFILE_TRACE, + "Hint: route to named server : "))); + } + else if (hint->type == HINT_ROUTE_TO_UPTODATE_SERVER) + { + /** not implemented */ + } + else if (hint->type == HINT_ROUTE_TO_ALL) + { + /** not implemented */ + } + else if (hint->type == HINT_PARAMETER) + { + if (strncasecmp( + (char *)hint->data, + "max_slave_replication_lag", + strlen("max_slave_replication_lag")) == 0) + { + target |= TARGET_RLAG_MAX; + } + else + { + LOGIF(LT, (skygw_log_write( + LOGFILE_TRACE, + "Error : Unknown hint parameter " + "'%s' when 'max_slave_replication_lag' " + "was expected.", + (char *)hint->data))); + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Error : Unknown hint parameter " + "'%s' when 'max_slave_replication_lag' " + "was expected.", + (char *)hint->data))); + } + } + else if (hint->type == HINT_ROUTE_TO_SLAVE) + { + target = TARGET_SLAVE; + LOGIF(LT, (skygw_log_write( + LOGFILE_TRACE, + "Hint: route to slave."))); + } + hint = hint->next; + } /*< while (hint != NULL) */ + } + else + { + /** hints don't affect on routing */ + ss_dassert(trx_active || + (QUERY_IS_TYPE(qtype, QUERY_TYPE_WRITE) || + QUERY_IS_TYPE(qtype, QUERY_TYPE_MASTER_READ) || + QUERY_IS_TYPE(qtype, QUERY_TYPE_SESSION_WRITE) || + (QUERY_IS_TYPE(qtype, QUERY_TYPE_USERVAR_READ) && + use_sql_variables_in == TYPE_MASTER) || + (QUERY_IS_TYPE(qtype, QUERY_TYPE_SYSVAR_READ) && + use_sql_variables_in == TYPE_MASTER) || + (QUERY_IS_TYPE(qtype, QUERY_TYPE_GSYSVAR_READ) && + use_sql_variables_in == TYPE_MASTER) || + (QUERY_IS_TYPE(qtype, QUERY_TYPE_GSYSVAR_WRITE) && + use_sql_variables_in == TYPE_MASTER) || + QUERY_IS_TYPE(qtype, QUERY_TYPE_BEGIN_TRX) || + QUERY_IS_TYPE(qtype, QUERY_TYPE_ENABLE_AUTOCOMMIT) || + QUERY_IS_TYPE(qtype, QUERY_TYPE_DISABLE_AUTOCOMMIT) || + QUERY_IS_TYPE(qtype, QUERY_TYPE_ROLLBACK) || + QUERY_IS_TYPE(qtype, QUERY_TYPE_COMMIT) || + QUERY_IS_TYPE(qtype, QUERY_TYPE_EXEC_STMT) || + QUERY_IS_TYPE(qtype, QUERY_TYPE_CREATE_TMP_TABLE) || + QUERY_IS_TYPE(qtype, QUERY_TYPE_READ_TMP_TABLE) || + QUERY_IS_TYPE(qtype, QUERY_TYPE_UNKNOWN))); + target = TARGET_MASTER; + } + return target; +} + +/** + * Check if the query is a DROP TABLE... query and + * if it targets a temporary table, remove it from the hashtable. + * @param instance Router instance + * @param router_session Router client session + * @param querybuf GWBUF containing the query + * @param type The type of the query resolved so far + */ +void check_drop_tmp_table( + ROUTER* instance, + void* router_session, + GWBUF* querybuf, + skygw_query_type_t type) +{ + + int tsize = 0, klen = 0,i; + char** tbl; + char *hkey,*dbname; + MYSQL_session* data; + + ROUTER_CLIENT_SES* router_cli_ses = (ROUTER_CLIENT_SES *)router_session; + DCB* master_dcb = NULL; + rses_property_t* rses_prop_tmp; + + rses_prop_tmp = router_cli_ses->rses_properties[RSES_PROP_TYPE_TMPTABLES]; + master_dcb = router_cli_ses->rses_master_ref->bref_dcb; + + CHK_DCB(master_dcb); + + data = (MYSQL_session*)master_dcb->session->data; + dbname = (char*)data->db; + + if (is_drop_table_query(querybuf)) + { + tbl = skygw_get_table_names(querybuf,&tsize,false); + + for(i = 0; irses_prop_data.temp_tables) + { + if (hashtable_delete(rses_prop_tmp->rses_prop_data.temp_tables, + (void *)hkey)) + { + LOGIF(LT, (skygw_log_write(LOGFILE_TRACE, + "Temporary table dropped: %s",hkey))); + } + } + free(tbl[i]); + free(hkey); + } + free(tbl); + } +} + +/** + * Check if the query targets a temporary table. + * @param instance Router instance + * @param router_session Router client session + * @param querybuf GWBUF containing the query + * @param type The type of the query resolved so far + * @return The type of the query + */ +skygw_query_type_t is_read_tmp_table( + ROUTER* instance, + void* router_session, + GWBUF* querybuf, + skygw_query_type_t type) +{ + + bool target_tmp_table = false; + int tsize = 0, klen = 0,i; + char** tbl; + char *hkey,*dbname; + MYSQL_session* data; + + ROUTER_CLIENT_SES* router_cli_ses = (ROUTER_CLIENT_SES *)router_session; + DCB* master_dcb = NULL; + skygw_query_type_t qtype = type; + rses_property_t* rses_prop_tmp; + + rses_prop_tmp = router_cli_ses->rses_properties[RSES_PROP_TYPE_TMPTABLES]; + master_dcb = router_cli_ses->rses_master_ref->bref_dcb; + + CHK_DCB(master_dcb); + + data = (MYSQL_session*)master_dcb->session->data; + dbname = (char*)data->db; + + if (QUERY_IS_TYPE(qtype, QUERY_TYPE_READ) || + QUERY_IS_TYPE(qtype, QUERY_TYPE_LOCAL_READ) || + QUERY_IS_TYPE(qtype, QUERY_TYPE_USERVAR_READ) || + QUERY_IS_TYPE(qtype, QUERY_TYPE_SYSVAR_READ) || + QUERY_IS_TYPE(qtype, QUERY_TYPE_GSYSVAR_READ)) + { + tbl = skygw_get_table_names(querybuf,&tsize,false); + + if (tsize > 0) + { + /** Query targets at least one table */ + for(i = 0; irses_prop_data.temp_tables) + { + + if( (target_tmp_table = + (bool)hashtable_fetch(rses_prop_tmp->rses_prop_data.temp_tables,(void *)hkey))) + { + /**Query target is a temporary table*/ + qtype = QUERY_TYPE_READ_TMP_TABLE; + LOGIF(LT, + (skygw_log_write(LOGFILE_TRACE, + "Query targets a temporary table: %s",hkey))); + } + } + + free(hkey); + free(tbl[i]); + } + + free(tbl); + } + } + + return qtype; +} + +/** + * If query is of type QUERY_TYPE_CREATE_TMP_TABLE then find out + * the database and table name, create a hashvalue and + * add it to the router client session's property. If property + * doesn't exist then create it first. + * + * @param instance Router instance + * @param router_session Router client session + * @param querybuf GWBUF containing the query + * @param type The type of the query resolved so far + */ +void check_create_tmp_table( + ROUTER* instance, + void* router_session, + GWBUF* querybuf, + skygw_query_type_t type) +{ + + int klen = 0; + + char *hkey,*dbname; + MYSQL_session* data; + + ROUTER_CLIENT_SES* router_cli_ses = (ROUTER_CLIENT_SES *)router_session; + DCB* master_dcb = NULL; + rses_property_t* rses_prop_tmp; + HASHTABLE* h; + + rses_prop_tmp = router_cli_ses->rses_properties[RSES_PROP_TYPE_TMPTABLES]; + master_dcb = router_cli_ses->rses_master_ref->bref_dcb; + + CHK_DCB(master_dcb); + + data = (MYSQL_session*)master_dcb->session->data; + dbname = (char*)data->db; + + + if (QUERY_IS_TYPE(type, QUERY_TYPE_CREATE_TMP_TABLE)) + { + bool is_temp = true; + char* tblname = NULL; + + tblname = skygw_get_created_table_name(querybuf); + + if (tblname && strlen(tblname) > 0) + { + klen = strlen(dbname) + strlen(tblname) + 2; + hkey = calloc(klen,sizeof(char)); + strcpy(hkey,dbname); + strcat(hkey,"."); + strcat(hkey,tblname); + } + else + { + hkey = NULL; + } + + if(rses_prop_tmp == NULL) + { + if((rses_prop_tmp = + (rses_property_t*)calloc(1,sizeof(rses_property_t)))) + { +#if defined(SS_DEBUG) + rses_prop_tmp->rses_prop_chk_top = CHK_NUM_ROUTER_PROPERTY; + rses_prop_tmp->rses_prop_chk_tail = CHK_NUM_ROUTER_PROPERTY; +#endif + rses_prop_tmp->rses_prop_rsession = router_cli_ses; + rses_prop_tmp->rses_prop_refcount = 1; + rses_prop_tmp->rses_prop_next = NULL; + rses_prop_tmp->rses_prop_type = RSES_PROP_TYPE_TMPTABLES; + router_cli_ses->rses_properties[RSES_PROP_TYPE_TMPTABLES] = rses_prop_tmp; + } + } + + if (rses_prop_tmp->rses_prop_data.temp_tables == NULL) + { + h = hashtable_alloc(7, hashkeyfun, hashcmpfun); + hashtable_memory_fns(h,hstrdup,NULL,hfree,NULL); + if (h != NULL) + { + rses_prop_tmp->rses_prop_data.temp_tables = h; + } + } + + if (hkey && + hashtable_add(rses_prop_tmp->rses_prop_data.temp_tables, + (void *)hkey, + (void *)is_temp) == 0) /*< Conflict in hash table */ + { + LOGIF(LT, (skygw_log_write( + LOGFILE_TRACE, + "Temporary table conflict in hashtable: %s", + hkey))); + } +#if defined(SS_DEBUG) + { + bool retkey = + hashtable_fetch( + rses_prop_tmp->rses_prop_data.temp_tables, + hkey); + if (retkey) + { + LOGIF(LT, (skygw_log_write( + LOGFILE_TRACE, + "Temporary table added: %s", + hkey))); + } + } +#endif + free(hkey); + free(tblname); + } +} + /** * The main routing entry, this is called with every packet that is * received and has to be forwarded to the backend database. @@ -1031,19 +1625,19 @@ static int routeQuery( GWBUF* querybuf) { skygw_query_type_t qtype = QUERY_TYPE_UNKNOWN; - GWBUF* plainsqlbuf = NULL; char* querystr = NULL; - char* startpos; mysql_server_cmd_t packet_type; uint8_t* packet; - int ret = 0; + int ret = 0; DCB* master_dcb = NULL; - DCB* slave_dcb = NULL; + DCB* target_dcb = NULL; ROUTER_INSTANCE* inst = (ROUTER_INSTANCE *)instance; ROUTER_CLIENT_SES* router_cli_ses = (ROUTER_CLIENT_SES *)router_session; bool rses_is_closed = false; - size_t len; - MYSQL* mysql = NULL; + route_target_t route_target; + bool succp = false; + int rlag_max = MAX_RLAG_UNDEFINED; + backend_type_t btype; /*< target backend type */ CHK_CLIENT_RSES(router_cli_ses); @@ -1057,6 +1651,7 @@ static int routeQuery( packet = GWBUF_DATA(querybuf); packet_type = packet[4]; + if (rses_is_closed) { @@ -1066,6 +1661,8 @@ static int routeQuery( */ if (packet_type != MYSQL_COM_QUIT) { + char* query_str = modutil_get_query(querybuf); + LOGIF(LE, (skygw_log_write_flush( LOGFILE_ERROR, @@ -1073,18 +1670,18 @@ static int routeQuery( "backend server. %s.", STRPACKETTYPE(packet_type), STRQTYPE(qtype), - (querystr == NULL ? "(empty)" : querystr), + (query_str == NULL ? "(empty)" : query_str), (rses_is_closed ? "Router was closed" : "Router has no backend servers where to " "route to")))); + free(querybuf); } - goto return_ret; + goto retblock; } inst->stats.n_queries++; - startpos = (char *)&packet[5]; master_dcb = router_cli_ses->rses_master_ref->bref_dcb; - CHK_DCB(master_dcb); + CHK_DCB(master_dcb); switch(packet_type) { case MYSQL_COM_QUIT: /*< 1 QUIT will close all sessions */ @@ -1105,44 +1702,16 @@ static int routeQuery( break; case MYSQL_COM_QUERY: - plainsqlbuf = gwbuf_clone_transform(querybuf, - GWBUF_TYPE_PLAINSQL); - len = GWBUF_LENGTH(plainsqlbuf); - /** unnecessary if buffer includes additional terminating null */ - querystr = (char *)malloc(len+1); - memcpy(querystr, startpos, len); - memset(&querystr[len], 0, 1); - /** - * Use mysql handle to query information from parse tree. - * call skygw_query_classifier_free before exit! - */ - qtype = skygw_query_classifier_get_type(querystr, 0, &mysql); + qtype = query_classifier_get_type(querybuf); break; case MYSQL_COM_STMT_PREPARE: - plainsqlbuf = gwbuf_clone_transform(querybuf, - GWBUF_TYPE_PLAINSQL); - len = GWBUF_LENGTH(plainsqlbuf); - /** unnecessary if buffer includes additional terminating null */ - querystr = (char *)malloc(len+1); - memcpy(querystr, startpos, len); - memset(&querystr[len], 0, 1); - qtype = skygw_query_classifier_get_type(querystr, 0, &mysql); + qtype = query_classifier_get_type(querybuf); qtype |= QUERY_TYPE_PREPARE_STMT; break; case MYSQL_COM_STMT_EXECUTE: /** Parsing is not needed for this type of packet */ -#if defined(NOT_USED) - plainsqlbuf = gwbuf_clone_transform(querybuf, - GWBUF_TYPE_PLAINSQL); - len = GWBUF_LENGTH(plainsqlbuf); - /** unnecessary if buffer includes additional terminating null */ - querystr = (char *)malloc(len+1); - memcpy(querystr, startpos, len); - memset(&querystr[len], 0, 1); - qtype = skygw_query_classifier_get_type(querystr, 0, &mysql); -#endif qtype = QUERY_TYPE_EXEC_STMT; break; @@ -1158,11 +1727,18 @@ static int routeQuery( break; } /**< switch by packet type */ + /** + * Check if the query has anything to do with temporary tables. + */ + qtype = is_read_tmp_table(instance,router_session,querybuf,qtype); + check_create_tmp_table(instance,router_session,querybuf,qtype); + check_drop_tmp_table(instance,router_session,querybuf,qtype); + /** - * If autocommit is disabled or transaction is explicitly started - * transaction becomes active and master gets all statements until - * transaction is committed and autocommit is enabled again. - */ + * If autocommit is disabled or transaction is explicitly started + * transaction becomes active and master gets all statements until + * transaction is committed and autocommit is enabled again. + */ if (router_cli_ses->rses_autocommit_enabled && QUERY_IS_TYPE(qtype, QUERY_TYPE_DISABLE_AUTOCOMMIT)) { @@ -1172,7 +1748,7 @@ static int routeQuery( { router_cli_ses->rses_transaction_active = true; } - } + } else if (!router_cli_ses->rses_transaction_active && QUERY_IS_TYPE(qtype, QUERY_TYPE_BEGIN_TRX)) { @@ -1193,152 +1769,208 @@ static int routeQuery( { router_cli_ses->rses_autocommit_enabled = true; router_cli_ses->rses_transaction_active = false; - } - /** - * Session update is always routed in the same way. - */ - if (QUERY_IS_TYPE(qtype, QUERY_TYPE_SESSION_WRITE) || - QUERY_IS_TYPE(qtype, QUERY_TYPE_PREPARE_STMT) || - QUERY_IS_TYPE(qtype, QUERY_TYPE_PREPARE_NAMED_STMT)) - { - /** - * It is not sure if the session command in question requires - * response. Statement is examined in route_session_write. - */ - bool succp = route_session_write( - router_cli_ses, - querybuf, - inst, - packet_type, - qtype); + } - if (succp) - { - ret = 1; - } - goto return_ret; - } - else if (QUERY_IS_TYPE(qtype, QUERY_TYPE_READ) && - !router_cli_ses->rses_transaction_active) + /** + * Find out where to route the query. Result may not be clear; it is + * possible to have a hint for routing to a named server which can + * be either slave or master. + * If query would otherwise be routed to slave then the hint determines + * actual target server if it exists. + * + * route_target is a bitfield and may include : + * TARGET_ALL + * - route to all connected backend servers + * TARGET_SLAVE[|TARGET_NAMED_SERVER|TARGET_RLAG_MAX] + * - route primarily according to hints, then to slave and if those + * failed, eventually to master + * TARGET_MASTER[|TARGET_NAMED_SERVER|TARGET_RLAG_MAX] + * - route primarily according to the hints and if they failed, + * eventually to master + */ + route_target = get_route_target(qtype, + router_cli_ses->rses_transaction_active, + router_cli_ses->rses_config.rw_use_sql_variables_in, + querybuf->hint); + + if (TARGET_IS_ALL(route_target)) + { + /** + * It is not sure if the session command in question requires + * response. Statement is examined in route_session_write. + * Router locking is done inside the function. + */ + succp = route_session_write(router_cli_ses, + gwbuf_clone(querybuf), + inst, + packet_type, + qtype); + + if (succp) + { + ret = 1; + } + goto retblock; + } + + /** Lock router session */ + if (!rses_begin_locked_router_action(router_cli_ses)) + { + goto retblock; + } + /** + * There is a hint which either names the target backend or + * hint which sets maximum allowed replication lag for the + * backend. + */ + if (TARGET_IS_NAMED_SERVER(route_target) || + TARGET_IS_RLAG_MAX(route_target)) + { + HINT* hint; + char* named_server = NULL; + + hint = querybuf->hint; + + while (hint != NULL) + { + if (hint->type == HINT_ROUTE_TO_NAMED_SERVER) + { + /** + * Set the name of searched + * backend server. + */ + named_server = hint->data; + LOGIF(LT, (skygw_log_write( + LOGFILE_TRACE, + "Hint: route to server " + "'%s'", + named_server))); + } + else if (hint->type == HINT_PARAMETER && + (strncasecmp((char *)hint->data, + "max_slave_replication_lag", + strlen("max_slave_replication_lag")) == 0)) + { + int val = (int) strtol((char *)hint->value, + (char **)NULL, 10); + + if (val != 0 || errno == 0) + { + /** + * Set max. acceptable + * replication lag + * value for backend srv + */ + rlag_max = val; + LOGIF(LT, (skygw_log_write( + LOGFILE_TRACE, + "Hint: " + "max_slave_replication_lag=%d", + rlag_max))); + } + } + hint = hint->next; + } /*< while */ + + if (rlag_max == MAX_RLAG_UNDEFINED) /*< no rlag max hint, use config */ + { + rlag_max = rses_get_max_replication_lag(router_cli_ses); + } + btype = BE_UNDEFINED; /*< target may be master or slave */ + /** + * Search backend server by name or replication lag. + * If it fails, then try to find valid slave or master. + */ + succp = get_dcb(&target_dcb, + router_cli_ses, + btype, + named_server, + rlag_max); + } + + if (!succp && TARGET_IS_SLAVE(route_target)) + { + btype = BE_SLAVE; + + if (rlag_max == MAX_RLAG_UNDEFINED) /*< no rlag max hint, use config */ + { + rlag_max = rses_get_max_replication_lag(router_cli_ses); + } + /** + * Search suitable backend server, get DCB in target_dcb + */ + succp = get_dcb(&target_dcb, + router_cli_ses, + BE_SLAVE, + NULL, + rlag_max); + } + + if (!succp && TARGET_IS_MASTER(route_target)) + { + if (master_dcb == NULL) + { + succp = get_dcb(&master_dcb, + router_cli_ses, + BE_MASTER, + NULL, + MAX_RLAG_UNDEFINED); + } + else + { + succp = true; + } + target_dcb = master_dcb; + } + ss_dassert(succp); + + + if (succp) /*< Have DCB of the target backend */ + { + if ((ret = target_dcb->func.write(target_dcb, gwbuf_clone(querybuf))) == 1) + { + backend_ref_t* bref; + + atomic_add(&inst->stats.n_slave, 1); + /** + * Add one query response waiter to backend reference + */ + bref = get_bref_from_dcb(router_cli_ses, target_dcb); + bref_set_state(bref, BREF_QUERY_ACTIVE); + bref_set_state(bref, BREF_WAITING_RESULT); + } + else + { + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Error : Routing query \"%s\" failed.", + querystr))); + } + } + rses_end_locked_router_action(router_cli_ses); +retblock: +#if defined(SS_DEBUG) { - bool succp; + char* canonical_query_str; - LOGIF(LT, (skygw_log_write( - LOGFILE_TRACE, - "[%s]\tRead-only query, routing to Slave.", - inst->service->name))); - ss_dassert(QUERY_IS_TYPE(qtype, QUERY_TYPE_READ)); + canonical_query_str = skygw_get_canonical(querybuf); - /** Lock router session */ - if (!rses_begin_locked_router_action(router_cli_ses)) + if (canonical_query_str != NULL) { - goto return_ret; - } - succp = get_dcb(&slave_dcb, router_cli_ses, BE_SLAVE); - - if (succp) - { - if ((ret = slave_dcb->func.write(slave_dcb, querybuf)) == 1) - { - backend_ref_t* bref; - - atomic_add(&inst->stats.n_slave, 1); - /** - * Add one query response waiter to backend reference - */ - bref = get_bref_from_dcb(router_cli_ses, slave_dcb); - bref_set_state(bref, BREF_QUERY_ACTIVE); - bref_set_state(bref, BREF_WAITING_RESULT); - } - else - { - LOGIF(LE, (skygw_log_write_flush( - LOGFILE_ERROR, - "Error : Routing query \"%s\" failed.", - querystr))); - } - } - rses_end_locked_router_action(router_cli_ses); - - ss_dassert(succp); - goto return_ret; - } - else - { - bool succp = true; - - if (LOG_IS_ENABLED(LOGFILE_TRACE)) - { - if (router_cli_ses->rses_transaction_active) /*< all to master */ - { - LOGIF(LT, (skygw_log_write( - LOGFILE_TRACE, - "Transaction is active, routing to Master."))); - } - else - { - LOGIF(LT, (skygw_log_write( - LOGFILE_TRACE, - "Begin transaction, write or unspecified type, " - "routing to Master."))); - } - } - /** Lock router session */ - if (!rses_begin_locked_router_action(router_cli_ses)) - { - goto return_ret; - } - - if (master_dcb == NULL) - { - succp = get_dcb(&master_dcb, router_cli_ses, BE_MASTER); - } - - if (succp) - { - if ((ret = master_dcb->func.write(master_dcb, querybuf)) == 1) - { - backend_ref_t* bref; - - atomic_add(&inst->stats.n_master, 1); - - /** - * Add one write response waiter to backend reference - */ - bref = get_bref_from_dcb(router_cli_ses, master_dcb); - bref_set_state(bref, BREF_QUERY_ACTIVE); - bref_set_state(bref, BREF_WAITING_RESULT); - } - } - rses_end_locked_router_action(router_cli_ses); - - ss_dassert(succp); - - if (ret == 0) - { - LOGIF(LE, (skygw_log_write_flush( - LOGFILE_ERROR, - "Error : Routing to master failed."))); + LOGIF(LT, (skygw_log_write( + LOGFILE_TRACE, + "Canonical version: %s", + canonical_query_str))); + free(canonical_query_str); } } -return_ret: - if (plainsqlbuf != NULL) - { - gwbuf_free(plainsqlbuf); - } - if (querystr != NULL) - { - free(querystr); - } - if (mysql != NULL) - { - skygw_query_classifier_free(mysql); - } +#endif + gwbuf_free(querybuf); return ret; } -/** to be inline'd */ + /** * @node Acquires lock to router client session if it is not closed. * @@ -1543,7 +2175,6 @@ static void clientReply ( if (LOG_IS_ENABLED(LOGFILE_ERROR) && MYSQL_IS_ERROR_PACKET(((uint8_t *)GWBUF_DATA(writebuf)))) { - SESSION* ses = backend_dcb->session; uint8_t* buf = (uint8_t *)GWBUF_DATA((scur->scmd_cur_cmd->my_sescmd_buf)); size_t len = MYSQL_GET_PACKET_LEN(buf); @@ -1868,7 +2499,9 @@ static bool select_connect_backend_servers( #endif /* assert with master_host */ ss_dassert(!master_connected || - (master_host && ((*p_master_ref)->bref_backend->backend_server == master_host->backend_server) && SERVER_MASTER)); + (master_host && + ((*p_master_ref)->bref_backend->backend_server == master_host->backend_server) && + SERVER_MASTER)); /** * Sort the pointer list to servers according to connection counts. As * a consequence those backends having least connections are in the @@ -1946,8 +2579,8 @@ static bool select_connect_backend_servers( { /* check also for relay servers and don't take the master_host */ if (slaves_found < max_nslaves && - (max_slave_rlag == -2 || - (b->backend_server->rlag != -1 && /*< information currently not available */ + (max_slave_rlag == MAX_RLAG_UNDEFINED || + (b->backend_server->rlag != MAX_RLAG_NOT_AVAILABLE && b->backend_server->rlag <= max_slave_rlag)) && (SERVER_IS_SLAVE(b->backend_server) || SERVER_IS_RELAY_SERVER(b->backend_server)) && (master_host != NULL && (b->backend_server != master_host->backend_server))) @@ -2077,7 +2710,8 @@ static bool select_connect_backend_servers( } /* assert with master_host */ ss_dassert(!master_connected || - (master_host && ((*p_master_ref)->bref_backend->backend_server == master_host->backend_server) && SERVER_MASTER)); + (master_host && ((*p_master_ref)->bref_backend->backend_server == master_host->backend_server) && + SERVER_MASTER)); #endif /** @@ -2139,15 +2773,11 @@ static bool select_connect_backend_servers( BACKEND* b = backend_ref[i].bref_backend; if (BREF_IS_IN_USE((&backend_ref[i]))) - { - backend_type_t btype = BACKEND_TYPE(b); - + { LOGIF(LT, (skygw_log_write( LOGFILE_TRACE, "Selected %s in \t%s:%d", - (btype == BE_MASTER ? "master" : - (btype == BE_SLAVE ? "slave" : - "unknown node type")), + STRSRVSTATUS(b->backend_server), b->backend_server->name, b->backend_server->port))); } @@ -2283,6 +2913,11 @@ static void rses_property_done( case RSES_PROP_TYPE_SESCMD: mysql_sescmd_done(&prop->rses_prop_data.sescmd); break; + + case RSES_PROP_TYPE_TMPTABLES: + hashtable_free(prop->rses_prop_data.temp_tables); + break; + default: LOGIF(LD, (skygw_log_write( LOGFILE_DEBUG, @@ -2682,8 +3317,23 @@ static bool execute_sescmd_in_backend( sescmd_cursor_clone_querybuf(scur)); break; - case MYSQL_COM_QUERY: - case MYSQL_COM_INIT_DB: + case MYSQL_COM_INIT_DB: + { + /** + * Record database name and store to session. + */ + GWBUF* tmpbuf; + MYSQL_session* data; + unsigned int qlen; + + data = dcb->session->data; + tmpbuf = scur->scmd_cur_cmd->my_sescmd_buf; + qlen = MYSQL_GET_PACKET_LEN((unsigned char*)tmpbuf->start); + memset(data->db,0,MYSQL_DATABASE_MAXLEN+1); + strncpy(data->db,tmpbuf->start+5,qlen - 1); + } + /** Fallthrough */ + case MYSQL_COM_QUERY: default: /** * Mark session command buffer, it triggers writing @@ -2966,10 +3616,9 @@ static bool route_session_write( /** Lock router session */ if (!rses_begin_locked_router_action(router_cli_ses)) { - rses_property_done(prop); succp = false; goto return_succp; - } + } /** * Additional reference is created to querybuf to * prevent it from being released before properties @@ -3036,6 +3685,7 @@ return_succp: } #if defined(NOT_USED) + static bool router_option_configured( ROUTER_INSTANCE* router, const char* optionstr, @@ -3212,6 +3862,11 @@ static bool handle_error_reply_client( CHK_DCB(client_dcb); client_dcb->func.write(client_dcb, errmsg); } + else + { + while ((errmsg=gwbuf_consume(errmsg, GWBUF_LENGTH(errmsg))) != NULL) + ; + } succp = false; /** false because new servers aren's selected. */ return succp; @@ -3266,6 +3921,11 @@ static bool handle_error_new_connection( client_dcb->func.write(client_dcb, errmsg); bref_clear_state(bref, BREF_WAITING_RESULT); } + else + { + while ((errmsg=gwbuf_consume(errmsg, GWBUF_LENGTH(errmsg))) != NULL) + ; + } bref_clear_state(bref, BREF_IN_USE); bref_set_state(bref, BREF_CLOSED); /** @@ -3332,8 +3992,8 @@ static void print_error_packet( } } ss_dassert(srv != NULL); - - bufstr = strndup(&ptr[7], len-3); + char* str = (char*)&ptr[7]; + bufstr = strndup(str, len-3); LOGIF(LE, (skygw_log_write_flush( LOGFILE_ERROR, @@ -3647,4 +4307,3 @@ static BACKEND *get_root_master(backend_ref_t *servers, int router_nservers) { } return master_host; } - diff --git a/server/modules/routing/readwritesplit/test/makefile b/server/modules/routing/readwritesplit/test/makefile index 87d22363d..b63030608 100644 --- a/server/modules/routing/readwritesplit/test/makefile +++ b/server/modules/routing/readwritesplit/test/makefile @@ -20,6 +20,7 @@ testall: -$(MAKE) cleantests -$(MAKE) DEBUG=Y buildtests -$(MAKE) runtests + -$(MAKE) -C test_hints testall buildtests: @@ -32,4 +33,4 @@ runtests: @echo "-------------------------------" >> $(TESTLOG) ./rwsplit.sh $(TESTLOG) $(THOST) $(TPORT_RW) $(TMASTER_ID) $(TUSER) $(TPWD) @echo "" >> $(TESTLOG) - @cat $(TESTLOG) >> $(TEST_MAXSCALE_LOG) \ No newline at end of file + @cat $(TESTLOG) >> $(TEST_MAXSCALE_LOG) diff --git a/server/modules/routing/readwritesplit/test/rwsplit.sh b/server/modules/routing/readwritesplit/test/rwsplit.sh index c2e403b16..6fefd8801 100755 --- a/server/modules/routing/readwritesplit/test/rwsplit.sh +++ b/server/modules/routing/readwritesplit/test/rwsplit.sh @@ -230,6 +230,16 @@ if [ "$a" != "$TRETVAL" ]; then else echo "$TINPUT PASSED">>$TLOG ; fi + +TINPUT=test_temporary_table.sql +a=`$RUNCMD < ./$TINPUT` +TRETVAL=1 +if [ "$a" != "$TRETVAL" ]; then + echo "$TINPUT FAILED, return value $a when $TRETVAL was expected">>$TLOG; +else + echo "$TINPUT PASSED">>$TLOG ; +fi + echo "-----------------------------------" >> $TLOG echo "Session variables: Stress Test 1" >> $TLOG echo "-----------------------------------" >> $TLOG @@ -238,6 +248,10 @@ RUNCMD=mysql\ --host=$THOST\ -P$TPORT\ -u$TUSER\ -p$TPWD\ --unbuffered=true\ --d TINPUT=test_sescmd2.sql for ((i = 0;i<1000;i++)) do + if [[ $(( i % 50 )) -eq 0 ]] + then + printf "." + fi a=`$RUNCMD < $TINPUT 2>&1` if [[ "`echo "$a"|grep -i 'error'`" != "" ]] then @@ -255,15 +269,19 @@ fi echo "-----------------------------------" >> $TLOG echo "Session variables: Stress Test 2" >> $TLOG echo "-----------------------------------" >> $TLOG - +echo "" err="" TINPUT=test_sescmd3.sql for ((j = 0;j<1000;j++)) do - b=`$RUNCMD < $TINPUT 2>&1` - if [[ "`echo "$b"|grep -i 'null'`" != "" ]] + if [[ $(( j % 50 )) -eq 0 ]] then - err=`echo "$b" | grep -i null` + printf "." + fi + b=`$RUNCMD < $TINPUT 2>&1` + if [[ "`echo "$b"|grep -i 'null\|error'`" != "" ]] + then + err=`echo "$b" | grep -i null\|error` break fi done diff --git a/server/modules/routing/readwritesplit/test/test_hints/Makefile b/server/modules/routing/readwritesplit/test/test_hints/Makefile new file mode 100644 index 000000000..f4b2d209d --- /dev/null +++ b/server/modules/routing/readwritesplit/test/test_hints/Makefile @@ -0,0 +1,53 @@ +# cleantests - clean local and subdirectories' tests +# buildtests - build all local and subdirectories' tests +# runtests - run all local tests +# testall - clean, build and run local and subdirectories' tests + +include ../../../../../../build_gateway.inc +include $(ROOT_PATH)/makefile.inc +include $(ROOT_PATH)/test.inc + +ARGS=6 + +CC=cc +TESTLOG := $(shell pwd)/testrwsplit_hints.log +RET := -1 + +cleantests: + - $(DEL) *.o + - $(DEL) *~ + - $(DEL) *.sql + - $(DEL) *.output + - $(DEL) *.log + +testall: + -$(MAKE) cleantests + -$(MAKE) DEBUG=Y buildtests + -$(MAKE) runtests + +buildtests: + + +runtests: + @echo "" >> $(TESTLOG) + @echo "-------------------------------" >> $(TESTLOG) + @echo $(shell date) >> $(TESTLOG) + @echo "Test Read/Write split router - hint routing" >> $(TESTLOG) + @echo "-------------------------------" >> $(TESTLOG) + @echo "Running simple tests" >> $(TESTLOG) + @echo "" >> $(TESTLOG) + ./rwsplit_hints.sh $(TESTLOG) $(THOST) $(TPORT_RW_HINT) $(TMASTER_ID) $(TUSER) $(TPWD) simple_tests + @echo "" >> $(TESTLOG) + @echo "Running syntax error tests" >> $(TESTLOG) + @echo "" >> $(TESTLOG) + ./syntax_check.sh $(TESTLOG) $(THOST) $(TPORT_RW_HINT) $(TMASTER_ID) $(TUSER) $(TPWD) error_tests + @echo "" >> $(TESTLOG) + @echo "Running complex tests" >> $(TESTLOG) + @echo "" >> $(TESTLOG) + ./rwsplit_hints.sh $(TESTLOG) $(THOST) $(TPORT_RW_HINT) $(TMASTER_ID) $(TUSER) $(TPWD) complex_tests + @echo "" >> $(TESTLOG) + @echo "Running stack tests" >> $(TESTLOG) + @echo "" >> $(TESTLOG) + ./rwsplit_hints.sh $(TESTLOG) $(THOST) $(TPORT_RW_HINT) $(TMASTER_ID) $(TUSER) $(TPWD) stack_tests + @echo "" >> $(TESTLOG) + @cat $(TESTLOG) >> $(TEST_MAXSCALE_LOG) diff --git a/server/modules/routing/readwritesplit/test/test_hints/complex_tests b/server/modules/routing/readwritesplit/test/test_hints/complex_tests new file mode 100644 index 000000000..a8ac640f9 --- /dev/null +++ b/server/modules/routing/readwritesplit/test/test_hints/complex_tests @@ -0,0 +1,48 @@ +select @@server_id; -- maxscale begin route to master:3000 +select @@server_id;:3000 +select @@server_id; -- maxscale route to server server3:3002 +select @@server_id;:3000 +select @@server_id; -- maxscale end: +select @@server_id; -- maxscale named1 prepare route to master: +select @@server_id; -- maxscale named1 begin:3000 +select @@server_id;:3000 +select @@server_id; -- maxscale route to server server3:3002 +select @@server_id;:3000 +select @@server_id; -- maxscale end: +select @@server_id; -- maxscale shorthand1 begin route to server server2:3001 +select @@server_id;:3001 +select @@server_id; -- maxscale route to server server3:3002 +select @@server_id;:3001 +select @@server_id; -- maxscale end: +select @@server_id; # maxscale begin route to master:3000 +select @@server_id;:3000 +select @@server_id; # maxscale route to server server3:3002 +select @@server_id;:3000 +select @@server_id; # maxscale end: +select @@server_id; # maxscale named2 prepare route to master: +select @@server_id; # maxscale named2 begin:3000 +select @@server_id;:3000 +select @@server_id; # maxscale route to server server3:3002 +select @@server_id;:3000 +select @@server_id; # maxscale end: +select @@server_id; # maxscale shorthand2 begin route to server server2:3001 +select @@server_id;:3001 +select @@server_id; # maxscale route to server server3:3002 +select @@server_id;:3001 +select @@server_id; # maxscale end: +select @@server_id/* maxscale begin route to master */;:3000 +select @@server_id;:3000 +select @@server_id/* maxscale route to server server3 */;:3002 +select @@server_id;:3000 +select @@server_id/* maxscale end */;: +select @@server_id/* maxscale named3 prepare route to master */;: +select @@server_id/* maxscale named3 begin */;:3000 +select @@server_id;:3000 +select @@server_id/* maxscale route to server server3 */;:3002 +select @@server_id;:3000 +select @@server_id/* maxscale end */;: +select @@server_id/* maxscale shorthand3 begin route to server server2 */; :3001 +select @@server_id;:3001 +select @@server_id/* maxscale route to server server3 */;:3002 +select @@server_id;:3001 +select @@server_id/* maxscale end */;: diff --git a/server/modules/routing/readwritesplit/test/test_hints/error_tests b/server/modules/routing/readwritesplit/test/test_hints/error_tests new file mode 100644 index 000000000..2decad100 --- /dev/null +++ b/server/modules/routing/readwritesplit/test/test_hints/error_tests @@ -0,0 +1,39 @@ +select @@server_id; -- maxscalemaxscale route to master: +select @@server_id; -- master to route maxscale: +select @@server_id; -- route to master: +select @@server_id; -- maxscale to master: +select @@server_id; -- maxscale route master: +select @@server_id; -- maxscale route to: +select @@server_id; -- maxscale begin master: +select @@server_id; -- maxscale master route to master: +select @@server_id; -- maxscale route to maxscale route to master: +select @@server_id; -- maxscale maxscale route to master: +select @@server_id; -- maxscale route to to server =): +select @@server_id; -- maxscale route to maxscale server server1: +select @@server_id; -- maxscale route to server1: +select @@server_id; # maxscalemaxscale route to master: +select @@server_id; # master to route maxscale: +select @@server_id; # route to master: +select @@server_id; # maxscale to master: +select @@server_id; # maxscale route master: +select @@server_id; # maxscale route to: +select @@server_id; # maxscale begin master: +select @@server_id; # maxscale master route to master: +select @@server_id; # maxscale route to maxscale route to master: +select @@server_id; # maxscale maxscale route to master: +select @@server_id; # maxscale route to to server =): +select @@server_id; # maxscale route to maxscale server server1: +select @@server_id; # maxscale route to server1: +select @@server_id; /* maxscalemaxscale route to master */;: +select @@server_id; /* master to route maxscale */;: +select @@server_id; /* route to master */;: +select @@server_id; /* maxscale to master */;: +select @@server_id; /* maxscale route master */;: +select @@server_id; /* maxscale route to */;: +select @@server_id; /* maxscale begin master */;: +select @@server_id; /* maxscale master route to master */;: +select @@server_id; /* maxscale route to maxscale route to master */;: +select @@server_id; /* maxscale maxscale route to master */;: +select @@server_id; /* maxscale route to to server =) */;: +select @@server_id; /* maxscale route to maxscale server server1 */;: +select @@server_id; /* maxscale route to server1 */;: \ No newline at end of file diff --git a/server/modules/routing/readwritesplit/test/test_hints/hints.txt b/server/modules/routing/readwritesplit/test/test_hints/hints.txt new file mode 100644 index 000000000..34196ca36 --- /dev/null +++ b/server/modules/routing/readwritesplit/test/test_hints/hints.txt @@ -0,0 +1,9 @@ +-- maxscale route to master:3000 +-- maxscale route to server server1:3000 +-- maxscale route to server server2:3001 +-- maxscale route to server server3:3002 +-- maxscale route to server server4:3003 +-- maxscale max_slave_replication_lag = 100: +-- maxscale max_slave_replication_lag= 100: +-- maxscale max_slave_replication_lag =100: +-- maxscale max_slave_replication_lag=100: diff --git a/server/modules/routing/readwritesplit/test/test_hints/rwsplit_hints.sh b/server/modules/routing/readwritesplit/test/test_hints/rwsplit_hints.sh new file mode 100755 index 000000000..cdbfb335b --- /dev/null +++ b/server/modules/routing/readwritesplit/test/test_hints/rwsplit_hints.sh @@ -0,0 +1,65 @@ +#!/bin/bash +NARGS=7 +TLOG=$1 +THOST=$2 +TPORT=$3 +TMASTER_ID=$4 +TUSER=$5 +TPWD=$6 +TESTINPUT=$7 + +if [ $# != $NARGS ] ; +then +echo"" +echo "Wrong number of arguments, gave "$#" but "$NARGS" is required" +echo "" +echo "Usage :" +echo " rwsplit_hints.sh " +echo "" +exit 1 +fi + + +RUNCMD=mysql\ --host=$THOST\ -P$TPORT\ -u$TUSER\ -p$TPWD\ --unbuffered=true\ --disable-reconnect\ --silent\ --comment +i=0 + +while read -r LINE +do +TINPUT[$i]=`echo "$LINE"|awk '{split($0,a,":");print a[1]}'` +TRETVAL[$i]=`echo "$LINE"|awk '{split($0,a,":");print a[2]}'` +echo "${TINPUT[i]}" >> $TESTINPUT.sql +i=$((i+1)) +done < $TESTINPUT + +`$RUNCMD < $TESTINPUT.sql > $TESTINPUT.output` + +x=0 +crash=1 +all_passed=1 + +while read -r TOUTPUT +do +crash=0 +if [ "$TOUTPUT" != "${TRETVAL[x]}" -a "${TRETVAL[x]}" != "" ] +then + all_passed=0 + echo "$TESTINPUT:$((x + 1)): ${TINPUT[x]} FAILED, return value $TOUTPUT when ${TRETVAL[x]} was expected">>$TLOG; +fi +x=$((x+1)) +done < $TESTINPUT.output + +if [ $crash -eq 1 ] +then + all_passed=0 + for ((v=0;v<$i;v++)) + do + echo "${TINPUT[v]} FAILED, nothing was returned">>$TLOG; + done +fi + +if [ $all_passed -eq 1 ] +then + echo "Test set: PASSED">>$TLOG; +else + echo "Test set: FAILED">>$TLOG; +fi diff --git a/server/modules/routing/readwritesplit/test/test_hints/simple_tests b/server/modules/routing/readwritesplit/test/test_hints/simple_tests new file mode 100644 index 000000000..67dd1f57e --- /dev/null +++ b/server/modules/routing/readwritesplit/test/test_hints/simple_tests @@ -0,0 +1,18 @@ +select @@server_id; -- maxscale route to master:3000 +select @@server_id; -- maxscale route to slave: +select @@server_id; -- maxscale route to server server1:3000 +select @@server_id; -- maxscale route to server server2:3001 +select @@server_id; -- maxscale route to server server3:3002 +select @@server_id; -- maxscale route to server server4:3003 +select @@server_id; # maxscale route to master:3000 +select @@server_id; # maxscale route to slave: +select @@server_id; # maxscale route to server server1:3000 +select @@server_id; # maxscale route to server server2:3001 +select @@server_id; # maxscale route to server server3:3002 +select @@server_id; # maxscale route to server server4:3003 +select @@server_id/* maxscale route to master */;:3000 +select @@server_id/* maxscale route to slave */;: +select @@server_id/* maxscale route to server server1 */;:3000 +select @@server_id/* maxscale route to server server2 */;:3001 +select @@server_id/* maxscale route to server server3 */;:3002 +select @@server_id/* maxscale route to server server4 */;:3003 diff --git a/server/modules/routing/readwritesplit/test/test_hints/stack_tests b/server/modules/routing/readwritesplit/test/test_hints/stack_tests new file mode 100644 index 000000000..0d4d51e05 --- /dev/null +++ b/server/modules/routing/readwritesplit/test/test_hints/stack_tests @@ -0,0 +1,50 @@ +select @@server_id; -- maxscale stack_named1 prepare route to server server1: +select @@server_id; -- maxscale stack_named2 prepare route to server server2: +select @@server_id; -- maxscale stack_named3 prepare route to server server3: +select @@server_id; -- maxscale stack_named4 prepare route to server server4: +select @@server_id; -- maxscale stack_named1 begin:3000 +select @@server_id;:3000 +select @@server_id; -- maxscale stack_named2 begin:3001 +select @@server_id;:3001 +select @@server_id; -- maxscale stack_named3 begin:3002 +select @@server_id;:3002 +select @@server_id; -- maxscale stack_named4 begin:3003 +select @@server_id;:3003 +select @@server_id; -- maxscale stack_shorthand1 begin route to server server1:3000 +select @@server_id;:3000 +select @@server_id; -- maxscale stack_shorthand2 begin route to server server2:3001 +select @@server_id;:3001 +select @@server_id; -- maxscale stack_shorthand3 begin route to server server3:3002 +select @@server_id;:3002 +select @@server_id; -- maxscale stack_shorthand4 begin route to server server4:3003 +select @@server_id;:3003 +select @@server_id; -- maxscale end:3002 +select @@server_id;:3002 +select @@server_id; -- maxscale end:3001 +select @@server_id;:3001 +select @@server_id; -- maxscale end:3000 +select @@server_id;:3000 +select @@server_id; -- maxscale end:3003 +select @@server_id;:3003 +select @@server_id; -- maxscale end:3002 +select @@server_id;:3002 +select @@server_id; -- maxscale end:3001 +select @@server_id;:3001 +select @@server_id; -- maxscale end:3000 +select @@server_id; -- maxscale end: +select @@server_id; -- maxscale stack_shorthand1 begin:3000 +select @@server_id; -- maxscale stack_shorthand2 begin:3001 +select @@server_id; -- maxscale stack_shorthand3 begin:3002 +select @@server_id; -- maxscale stack_shorthand4 begin:3003 +select @@server_id; -- maxscale stack_named1 begin:3000 +select @@server_id; -- maxscale stack_named2 begin:3001 +select @@server_id; -- maxscale stack_named3 begin:3002 +select @@server_id; -- maxscale stack_named4 begin:3003 +select @@server_id; -- maxscale end:3002 +select @@server_id; -- maxscale end:3001 +select @@server_id; -- maxscale end:3000 +select @@server_id; -- maxscale end:3003 +select @@server_id; -- maxscale end:3002 +select @@server_id; -- maxscale end:3001 +select @@server_id; -- maxscale end:3000 +select @@server_id; -- maxscale end: \ No newline at end of file diff --git a/server/modules/routing/readwritesplit/test/test_hints/syntax_check.sh b/server/modules/routing/readwritesplit/test/test_hints/syntax_check.sh new file mode 100755 index 000000000..8c09d7348 --- /dev/null +++ b/server/modules/routing/readwritesplit/test/test_hints/syntax_check.sh @@ -0,0 +1,33 @@ +#! /bin/bash +NARGS=7 +TLOG=$1 +THOST=$2 +TPORT=$3 +TMASTER_ID=$4 +TUSER=$5 +TPWD=$6 +TESTINPUT=$7 + +if [ $# != $NARGS ] ; +then +echo"" +echo "Wrong number of arguments, gave "$#" but "$NARGS" is required" +echo "" +echo "Usage :" +echo " syntax_check.sh " + echo "" +exit 1 +fi + +./rwsplit_hints.sh dummy.log $THOST $TPORT $TMASTER_ID $TUSER $TPWD $TESTINPUT + +exp_count=`cat error_tests|wc -l` +err_count=`tac ../../../../../test/log/skygw_err* | gawk '/enabled/{if(!bg){ bg = 1} else exit 0}{if(bg) print}'|grep -c 'Hint ignored'` + +if [[ $err_count -ge $exp_count ]] +then + echo "Test set: PASSED">>$TLOG; +else + echo "Expected $exp_count ignored hints in the error log but found $err_count instead">>$TLOG + echo "Test set: FAILED">>$TLOG; +fi diff --git a/server/modules/routing/readwritesplit/test/test_temporary_table.sql b/server/modules/routing/readwritesplit/test/test_temporary_table.sql new file mode 100644 index 000000000..374510389 --- /dev/null +++ b/server/modules/routing/readwritesplit/test/test_temporary_table.sql @@ -0,0 +1,5 @@ +use test; +drop table if exists t1; +create temporary table t1 (id integer); +insert into t1 values(1); +select id from t1; diff --git a/server/modules/routing/webserver.c b/server/modules/routing/webserver.c new file mode 100644 index 000000000..28102dfd4 --- /dev/null +++ b/server/modules/routing/webserver.c @@ -0,0 +1,616 @@ +/* + * This file is distributed as part of MaxScale. It is free + * software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, + * version 2. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright SkySQL Ab 2014 + */ +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * The instance structure for this router. + */ +typedef struct { + SERVICE *service; +} WEB_INSTANCE; + +/** + * The session structure for this router. + */ +typedef struct { + SESSION *session; +} WEB_SESSION; + +static char *version_str = "V1.0.0"; + +MODULE_INFO info = { + MODULE_API_ROUTER, + MODULE_IN_DEVELOPMENT, + ROUTER_VERSION, + "A test router - not for use in real systems" +}; + +static ROUTER *createInstance(SERVICE *service, char **options); +static void *newSession(ROUTER *instance, SESSION *session); +static void closeSession(ROUTER *instance, void *session); +static void freeSession(ROUTER *instance, void *session); +static int routeQuery(ROUTER *instance, void *session, GWBUF *queue); +static void diagnostic(ROUTER *instance, DCB *dcb); +static uint8_t getCapabilities (ROUTER* inst, void* router_session); + + +static ROUTER_OBJECT MyObject = { + createInstance, + newSession, + closeSession, + freeSession, + routeQuery, + diagnostic, + NULL, + NULL, + getCapabilities +}; + + +static void send_index(WEB_SESSION *); +static void send_css(WEB_SESSION *); +static void send_menu(WEB_SESSION *); +static void send_blank(WEB_SESSION *); +static void send_title(WEB_SESSION *); +static void send_frame1(WEB_SESSION *); +static void send_services(WEB_SESSION *); +static void send_sessions(WEB_SESSION *); +static void send_servers(WEB_SESSION *); +static void send_monitors(WEB_SESSION *); +static void respond_error(WEB_SESSION *, int, char *); + +/** + * A map of URL to function that implements the URL + */ +static struct { + char *page; /* URL */ + void (*fcn)(WEB_SESSION *); /* Function to call */ +} pages[] = { + { "index.html", send_index }, + { "services.html", send_services }, + { "menu.html", send_menu }, + { "sessions.html", send_sessions }, + { "blank.html", send_blank }, + { "title.html", send_title }, + { "frame1.html", send_frame1 }, + { "servers.html", send_servers }, + { "monitors.html", send_monitors }, + { "styles.css", send_css }, + { NULL, NULL } +}; + +/** + * Implementation of the mandatory version entry point + * + * @return version string of the module + */ +char * +version() +{ + return version_str; +} + +/** + * The module initialisation routine, called when the module + * is first loaded. + */ +void +ModuleInit() +{ + +} + +/** + * The module entry point routine. It is this routine that + * must populate the structure that is referred to as the + * "module object", this is a structure with the set of + * external entry points for this module. + * + * @return The module object + */ +ROUTER_OBJECT * +GetModuleObject() +{ + return &MyObject; +} + +/** + * Create an instance of the router for a particular service + * within the gateway. + * + * @param service The service this router is being create for + * @param options The options for this query router + * + * @return The instance data for this new instance + */ +static ROUTER * +createInstance(SERVICE *service, char **options) +{ +WEB_INSTANCE *inst; + + if ((inst = (WEB_INSTANCE *)malloc(sizeof(WEB_INSTANCE))) == NULL) + return NULL; + + inst->service = service; + return (ROUTER *)inst; +} + +/** + * Associate a new session with this instance of the router. + * + * @param instance The router instance data + * @param session The session itself + * @return Session specific data for this session + */ +static void * +newSession(ROUTER *instance, SESSION *session) +{ +WEB_SESSION *wsession; + + if ((wsession = (WEB_SESSION *)malloc(sizeof(WEB_SESSION))) == NULL) + return NULL; + + wsession->session = session; + return wsession; +} + +/** + * Close a session with the router, this is the mechanism + * by which a router may cleanup data structure etc. + * + * @param instance The router instance data + * @param session The session being closed + */ +static void +closeSession(ROUTER *instance, void *session) +{ + free(session); +} + +static void freeSession( + ROUTER* router_instance, + void* router_client_session) +{ + return; +} + +static int +routeQuery(ROUTER *instance, void *session, GWBUF *queue) +{ +WEB_SESSION *wsession = (WEB_SESSION *)session; +char *ptr; +int i, found = 0; +char *url; + + if ((url = gwbuf_get_property(queue, "URL")) == NULL) + { + respond_error(wsession, 404, "No URL available"); + } + + ptr = strrchr(url, '/'); + if (ptr) + ptr++; + else + ptr = url; + for (i = 0; pages[i].page; i++) + { + if (!strcmp(ptr, pages[i].page)) + { + (pages[i].fcn)(wsession); + found = 1; + } + } + if (!found) + respond_error(wsession, 404, "Unrecognised URL received"); + gwbuf_free(queue); + return 0; +} + +/** + * Diagnostics routine + * + * @param instance The router instance + * @param dcb The DCB for diagnostic output + */ +static void +diagnostic(ROUTER *instance, DCB *dcb) +{ +} + +/** + * Return the router capabilities bitmask + * + * @param inst The router instance + * @param router_session The router session + * @return Router capabilities bitmask + */ +static uint8_t +getCapabilities(ROUTER *inst, void *router_session) +{ + return 0; +} + +/** + * The HTML of the index page. + */ +static char *index_page = +"" +"" +"MaxScale" +"" +"" +"" +"" +"" +""; + +/** + * The HTML of the title page + */ +static char *title_page = +"" +"" +"MaxScale" +"" +"

MaxScale - Status View

" +""; + +/** + * HTML of the main frames, those below the title frame + */ +static char *frame1_page = +"" +"" +"" +"" +"" +""; + +/** + * The menu page HTML + */ +static char *menu_page = +"" +"" +"" +"

Options

" +"

"; + +/** + * A blank page, contents of the display area when we first connect + */ +static char *blank_page = " "; + +/** + * The CSS used for every "page" + */ +static char *css = +"table, td, th { border: 1px solid blue; }\n" +"th { background-color: blue; color: white; padding: 5px }\n" +"td { padding: 5px; }\n" +"table { border-collapse: collapse; }\n" +"a:link { color: #0000FF; }\n" +"a:visted { color: #0000FF; }\n" +"a:hover { color: #FF0000; }\n" +"a:active { color: #0000FF; }\n" +"h1 { color: blue; font-family: serif }\n" +"h2 { color: blue; font-family: serif }\n" +"p { font-family: serif }\n" +"li { font-family: serif }\n"; + +/** + * Send the standard HTTP headers for an HTML file + */ +static void +send_html_header(DCB *dcb) +{ +char date[64] = ""; +const char *fmt = "%a, %d %b %Y %H:%M:%S GMT"; + + time_t httpd_current_time = time(NULL); + + strftime(date, sizeof(date), fmt, localtime(&httpd_current_time)); + + dcb_printf(dcb, "HTTP/1.1 200 OK\r\nDate: %s\r\nServer: %s\r\nConnection: close\r\nContent-Type: text/html\r\n", date, "MaxScale"); + + dcb_printf(dcb, "\r\n"); +} + +/** + * Send a static HTML page + * + * @param dcb The DCB of the connection to the browser + * @param html The HTML to send + */ +static void +send_static_html(DCB *dcb, char *html) +{ + dcb_printf(dcb, html); +} + +/** + * Send the index page + * + * @param session The router session + */ +static void +send_index(WEB_SESSION *session) +{ +DCB *dcb = session->session->client; + + send_html_header(dcb); + send_static_html(dcb, index_page); + dcb_close(dcb); +} + +/** + * Send the CSS + * + * @param session The router session + */ +static void +send_css(WEB_SESSION *session) +{ +DCB *dcb = session->session->client; + + send_html_header(dcb); + send_static_html(dcb, css); + dcb_close(dcb); +} + +/** + * Send the title page + * + * @param session The router session + */ +static void +send_title(WEB_SESSION *session) +{ +DCB *dcb = session->session->client; + + send_html_header(dcb); + send_static_html(dcb, title_page); + dcb_close(dcb); +} + +/** + * Send the frame1 page + * + * @param session The router session + */ +static void +send_frame1(WEB_SESSION *session) +{ +DCB *dcb = session->session->client; + + send_html_header(dcb); + send_static_html(dcb, frame1_page); + dcb_close(dcb); +} + +/** + * Send the menu page + * + * @param session The router session + */ +static void +send_menu(WEB_SESSION *session) +{ +DCB *dcb = session->session->client; + + send_html_header(dcb); + send_static_html(dcb, menu_page); + dcb_close(dcb); +} + +/** + * Send a blank page + * + * @param session The router session + */ +static void +send_blank(WEB_SESSION *session) +{ +DCB *dcb = session->session->client; + + send_html_header(dcb); + send_static_html(dcb, blank_page); + dcb_close(dcb); +} + +/** + * Write a table row for a service. This is called using the service + * iterator function + * + * @param service The service to display + * @param dcb The DCB to print the HTML to + */ +static void +service_row(SERVICE *service, DCB *dcb) +{ + dcb_printf(dcb, "%s%s%d%d\n", + service->name, service->routerModule, + service->stats.n_current, service->stats.n_sessions); +} + +/** + * Send the services page. This produces a table by means of the + * serviceIterate call. + * + * @param session The router session + */ +static void +send_services(WEB_SESSION *session) +{ +DCB *dcb = session->session->client; + + send_html_header(dcb); + dcb_printf(dcb, ""); + dcb_printf(dcb, ""); + dcb_printf(dcb, "

Services

"); + dcb_printf(dcb, "\n"); + serviceIterate(service_row, dcb); + dcb_printf(dcb, "
NameRouter"); + dcb_printf(dcb, "Current SessionsTotal Sessions
\n"); + dcb_close(dcb); +} + +/** + * Write a session row for a session. this is called using the session + * iterator function + * + * @param session The session to display + * @param dcb The DCB to send the HTML to + */ +static void +session_row(SESSION *session, DCB *dcb) +{ + dcb_printf(dcb, "%-16p%s%s%s\n", + session, ((session->client && session->client->remote) + ? session->client->remote : ""), + (session->service && session->service->name + ? session->service->name : ""), + session_state(session->state)); +} + +/** + * Send the sessions page. The produces a table of all the current sessions + * display. It makes use of the sessionIterate call to call the function + * session_row() with each session. + * + * @param session The router session + */ +static void +send_sessions(WEB_SESSION *session) +{ +DCB *dcb = session->session->client; + + send_html_header(dcb); + dcb_printf(dcb, ""); + dcb_printf(dcb, ""); + dcb_printf(dcb, "

Sessions

"); + dcb_printf(dcb, "\n"); + sessionIterate(session_row, dcb); + dcb_printf(dcb, "
SessionClient"); + dcb_printf(dcb, "ServiceState
\n"); + dcb_close(dcb); +} + +/** + * Display a table row for a particular server. This is called via the + * serverIterate call in send_servers. + * + * @param server The server to print + * @param dcb The DCB to send the HTML to + */ +static void +server_row(SERVER *server, DCB *dcb) +{ + dcb_printf(dcb, "%s%s%d%s%d\n", + server->unique_name, server->name, server->port, + server_status(server), server->stats.n_current); +} + +/** + * Send the servers page + * + * @param session The router session + */ +static void +send_servers(WEB_SESSION *session) +{ +DCB *dcb = session->session->client; + + send_html_header(dcb); + dcb_printf(dcb, ""); + dcb_printf(dcb, ""); + dcb_printf(dcb, "

Servers

"); + dcb_printf(dcb, "\n"); + serverIterate(server_row, dcb); + dcb_printf(dcb, "
ServerAddress"); + dcb_printf(dcb, "PortStateConnections
\n"); + dcb_close(dcb); +} + +/** + * Print a table row for the monitors table + * + * @param monitor The monitor to print + * @param dcb The DCB to print to + */ +static void +monitor_row(MONITOR *monitor, DCB *dcb) +{ + dcb_printf(dcb, "%s%s\n", + monitor->name, monitor->state & MONITOR_STATE_RUNNING + ? "Running" : "Stopped"); +} + +/** + * Send the monitors page. This iterates on all the monitors and send + * the rows via the monitor_monitor. + * + * @param session The router session + */ +static void +send_monitors(WEB_SESSION *session) +{ +DCB *dcb = session->session->client; + + send_html_header(dcb); + dcb_printf(dcb, ""); + dcb_printf(dcb, ""); + dcb_printf(dcb, "

Monitors

"); + dcb_printf(dcb, "\n"); + monitorIterate(monitor_row, dcb); + dcb_printf(dcb, "
MonitorState
\n"); + dcb_close(dcb); +} + +/** + * Respond with an HTTP error + * + * @param session The router session + * @param err The HTTP error code to send + * @param msg The message to print + */ +static void +respond_error(WEB_SESSION *session, int err, char *msg) +{ +DCB *dcb = session->session->client; + + dcb_printf(dcb, "HTTP/1.1 %d %s\n", err, msg); + dcb_printf(dcb, "Content-Type: text/html\n"); + dcb_printf(dcb, "\n"); + dcb_printf(dcb, "\n"); + dcb_printf(dcb, "MaxScale webserver plugin unable to satisfy request.\n"); + dcb_printf(dcb, "

Code: %d, %s\n", err, msg); + dcb_printf(dcb, ""); + dcb_close(dcb); +} diff --git a/server/test/MaxScale_test.cnf b/server/test/MaxScale_test.cnf index 89f65b9a0..7c2e8972e 100644 --- a/server/test/MaxScale_test.cnf +++ b/server/test/MaxScale_test.cnf @@ -20,11 +20,13 @@ threads=1 # user = # passwd= +# monitor_interval= [MySQL Monitor] type=monitor module=mysqlmon -servers=server1,server2,server3 +servers=server1,server2,server3,server4 user=maxuser passwd=maxpwd @@ -36,25 +38,46 @@ passwd=maxpwd # servers=,,... # user= # passwd= +# enable_root_user=<0 or 1, default is 0> +# version_string= # # Valid router modules currently are: # readwritesplit, readconnroute and debugcli + [RW Split Router] type=service router=readwritesplit -servers=server1,server2,server3 +servers=server1,server2,server3,server4 +max_slave_connections=90% +write_ses_variables_to_all=Yes +read_ses_variables_from_slaves=Yes user=maxuser passwd=maxpwd +filters=Hint + +[RW Split Hint Router] +type=service +router=readwritesplit +servers=server1,server2,server3,server4 +max_slave_connections=90% +write_ses_variables_to_all=Yes +read_ses_variables_from_slaves=Yes +user=maxuser +passwd=maxpwd +filters=Hint + [Read Connection Router] type=service router=readconnroute -router_options=slave -servers=server1,server2,server3 +router_options=master +servers=server1 user=maxuser passwd=maxpwd + [HTTPD Router] type=service router=testroute @@ -64,6 +87,12 @@ servers=server1,server2,server3 type=service router=debugcli + +[Hint] +type=filter +module=hintfilter + + # Listener definitions for the services # # Valid options are: @@ -71,6 +100,8 @@ router=debugcli # service= # protocol= # port= +# address=

+# socket= [RW Split Listener] type=listener @@ -78,17 +109,25 @@ service=RW Split Router protocol=MySQLClient port=4006 +[RW Split Hint Listener] +type=listener +service=RW Split Hint Router +protocol=MySQLClient +port=4009 + [Read Connection Listener] type=listener service=Read Connection Router protocol=MySQLClient port=4008 +#socket=/tmp/readconn.sock [Debug Listener] type=listener service=Debug Interface protocol=telnetd port=4442 +#address=127.0.0.1 [HTTPD Listener] type=listener @@ -115,3 +154,9 @@ type=server address=127.0.0.1 port=3002 protocol=MySQLBackend + +[server4] +type=server +address=127.0.0.1 +port=3003 +protocol=MySQLBackend diff --git a/test.inc b/test.inc index b020368d8..7c5e5c571 100644 --- a/test.inc +++ b/test.inc @@ -19,6 +19,11 @@ TPORT_RCONN := # TPORT_RW := # +# port of read/write split router module with hints, for example: +# TPORT_RW_HINT := 4009 +# +TPORT_RW_HINT := +# # username of MaxScale user, for example: # TUSER := maxuser # diff --git a/utils/makefile b/utils/makefile index a8df09a7f..0faa27144 100644 --- a/utils/makefile +++ b/utils/makefile @@ -3,6 +3,7 @@ include ../makefile.inc CC = gcc CPP = g++ +UTILS_PATH := $(ROOT_PATH)/utils makeall: clean all @@ -13,7 +14,7 @@ clean: - $(DEL) *~ all: - $(CPP) -c $(CFLAGS) \ + $(CPP) -c $(CFLAGS) -I$(UTILS_PATH) \ -fPIC skygw_utils.cc -o skygw_utils.o cleantests: diff --git a/utils/skygw_debug.h b/utils/skygw_debug.h index 43a609a40..352d736f9 100644 --- a/utils/skygw_debug.h +++ b/utils/skygw_debug.h @@ -123,7 +123,8 @@ typedef enum skygw_chk_t { CHK_NUM_SESCMD_CUR, CHK_NUM_BACKEND, CHK_NUM_BACKEND_REF, - CHK_NUM_PREP_STMT + CHK_NUM_PREP_STMT, + CHK_NUM_PINFO } skygw_chk_t; # define STRBOOL(b) ((b) ? "true" : "false") @@ -133,7 +134,8 @@ typedef enum skygw_chk_t { ((t) == QUERY_TYPE_SESSION_WRITE ? "QUERY_TYPE_SESSION_WRITE" : \ ((t) == QUERY_TYPE_UNKNOWN ? "QUERY_TYPE_UNKNOWN" : \ ((t) == QUERY_TYPE_LOCAL_READ ? "QUERY_TYPE_LOCAL_READ" : \ - "Unknown query type"))))) + ((t) == QUERY_TYPE_EXEC_STMT ? "QUERY_TYPE_EXEC_STMT" : \ + "Unknown query type")))))) #define STRLOGID(i) ((i) == LOGFILE_TRACE ? "LOGFILE_TRACE" : \ ((i) == LOGFILE_MESSAGE ? "LOGFILE_MESSAGE" : \ @@ -231,11 +233,13 @@ typedef enum skygw_chk_t { ((c) == LEAST_BEHIND_MASTER ? "LEAST_BEHIND_MASTER" : \ ((c) == LEAST_CURRENT_OPERATIONS ? "LEAST_CURRENT_OPERATIONS" : "Unknown criteria"))))) -#define STRSRVSTATUS(s) ((SERVER_IS_RUNNING(s) && SERVER_IS_MASTER(s)) ? "RUNNING MASTER" : \ - ((SERVER_IS_RUNNING(s) && SERVER_IS_SLAVE(s)) ? "RUNNING SLAVE" : \ - ((SERVER_IS_RUNNING(s) && SERVER_IS_JOINED(s)) ? "RUNNING JOINED" : \ +#define STRSRVSTATUS(s) (SERVER_IS_MASTER(s) ? "RUNNING MASTER" : \ + (SERVER_IS_SLAVE(s) ? "RUNNING SLAVE" : \ + (SERVER_IS_JOINED(s) ? "RUNNING JOINED" : \ + (SERVER_IS_NDB(s) ? "RUNNING NDB" : \ ((SERVER_IS_RUNNING(s) && SERVER_IN_MAINT(s)) ? "RUNNING MAINTENANCE" : \ - (SERVER_IS_RUNNING(s) ? "RUNNING (only)" : "NO STATUS"))))) + (SERVER_IS_RELAY_SERVER(s) ? "RUNNING RELAY" : \ + (SERVER_IS_RUNNING(s) ? "RUNNING (only)" : "NO STATUS"))))))) #define CHK_MLIST(l) { \ ss_info_dassert((l->mlist_chk_top == CHK_NUM_MLIST && \ @@ -486,6 +490,13 @@ typedef enum skygw_chk_t { "Prepared statement struct has invalid check fields"); \ } +#define CHK_PARSING_INFO(p) { \ + ss_info_dassert((p)->pi_chk_top == CHK_NUM_PINFO && \ + (p)->pi_chk_tail == CHK_NUM_PINFO, \ + "Parsing info struct has invalid check fields"); \ +} + + #if defined(SS_DEBUG) bool conn_open[10240]; diff --git a/utils/skygw_types.h b/utils/skygw_types.h index d497fbfd7..a82db80ab 100644 --- a/utils/skygw_types.h +++ b/utils/skygw_types.h @@ -44,4 +44,7 @@ # endif #endif +#define MAX_ERROR_MSG PATH_MAX +#define array_nelems(a) ((uint)(sizeof(a)/sizeof(a[0]))) + #endif /* SKYGW_TYPES_H */ diff --git a/utils/skygw_utils.cc b/utils/skygw_utils.cc index 741861e84..4845c73a2 100644 --- a/utils/skygw_utils.cc +++ b/utils/skygw_utils.cc @@ -23,9 +23,10 @@ #include #include #include - +#include +#include #include "skygw_debug.h" -#include "skygw_types.h" +#include #include "skygw_utils.h" const char* timestamp_formatstr = "%04d-%02d-%02d %02d:%02d:%02d "; @@ -1863,3 +1864,104 @@ void skygw_file_done( free(file); } } + + +/** + * Find the given needle - user-provided literal - and replace it with + * replacement string. Separate user-provided literals from matching table names + * etc. by searching only substrings preceded by non-letter and non-number. + * + * @param haystack Plain text query string, not to be freed + * @param needle Substring to be searched, not to be freed + * @param replacement Replacement text, not to be freed + * + * @return newly allocated string where needle is replaced + */ +char* replace_literal( + char* haystack, + const char* needle, + const char* replacement) +{ + const char* prefix = "[ ='\",\\(]"; /*< ' ','=','(',''',''"',',' are allowed before needle */ + const char* suffix = "([^[:alnum:]]|$)"; /*< alpha-num chars aren't allowed after the needle */ + char* search_re; + char* newstr; + regex_t re; + regmatch_t match; + int rc; + size_t rlen = strlen(replacement); + size_t nlen = strlen(needle); + size_t hlen = strlen(haystack); + + search_re = (char *)malloc(strlen(prefix)+nlen+strlen(suffix)+1); + + if (search_re == NULL) + { + fprintf(stderr, "Regex memory allocation failed : %s\n", + strerror(errno)); + newstr = haystack; + goto retblock; + } + + sprintf(search_re, "%s%s%s", prefix, needle, suffix); + /** Allocate memory for new string +1 for terminating byte */ + newstr = (char *)malloc(hlen-nlen+rlen+1); + + if (newstr == NULL) + { + fprintf(stderr, "Regex memory allocation failed : %s\n", + strerror(errno)); + free(search_re); + free(newstr); + newstr = haystack; + goto retblock; + } + + rc = regcomp(&re, search_re, REG_EXTENDED|REG_ICASE); + ss_dassert(rc == 0); + + if (rc != 0) + { + char error_message[MAX_ERROR_MSG]; + regerror (rc, &re, error_message, MAX_ERROR_MSG); + fprintf(stderr, + "Regex error compiling '%s': %s\n", + search_re, + error_message); + free(search_re); + free(newstr); + newstr = haystack; + goto retblock; + } + rc = regexec(&re, haystack, 1, &match, 0); + + if (rc != 0) + { + free(search_re); + free(newstr); + regfree(&re); + newstr = haystack; + goto retblock; + } + memcpy(newstr, haystack, match.rm_so+1); + memcpy(newstr+match.rm_so+1, replacement, rlen); + /** +1 is terminating byte */ + memcpy(newstr+match.rm_so+1+rlen, haystack+match.rm_so+1+nlen, hlen-(match.rm_so+1)-nlen+1); + + regfree(&re); + free(haystack); + free(search_re); +retblock: + return newstr; +} + + + + + + + + + + + diff --git a/utils/skygw_utils.h b/utils/skygw_utils.h index 992f169d2..a54859392 100644 --- a/utils/skygw_utils.h +++ b/utils/skygw_utils.h @@ -83,6 +83,7 @@ typedef enum { THR_INIT, THR_RUNNING, THR_STOPPED, THR_DONE } skygw_thr_state_t; typedef enum { MES_RC_FAIL, MES_RC_SUCCESS, MES_RC_TIMEOUT } skygw_mes_rc_t; EXTERN_C_BLOCK_BEGIN + slist_cursor_t* slist_init(void); void slist_done(slist_cursor_t* c); @@ -192,4 +193,12 @@ int skygw_rwlock_init(skygw_rwlock_t** rwlock); int atomic_add(int *variable, int value); +EXTERN_C_BLOCK_BEGIN + +char* replace_literal(char* haystack, + const char* needle, + const char* replacement); + +EXTERN_C_BLOCK_END + #endif /* SKYGW_UTILS_H */