From 37f80a7dd8ccc8e04e87a40c289db7019b071bee Mon Sep 17 00:00:00 2001 From: Esa Korhonen Date: Fri, 10 Feb 2017 09:28:31 +0200 Subject: [PATCH] Plugin development manual Plenty of additions and restructuring compared to the first version. Add it to Documentation. --- CMakeLists.txt | 1 + Documentation/CMakeLists.txt | 1 + Documentation/Plugin-development-guide.md | 566 +++++++++++ server/modules/routing/CMakeLists.txt | 1 + .../routing/roundrobinrouter/CMakeLists.txt | 6 + .../roundrobinrouter/roundrobinrouter.cpp | 897 ++++++++++++++++++ 6 files changed, 1472 insertions(+) create mode 100644 Documentation/CMakeLists.txt create mode 100644 Documentation/Plugin-development-guide.md create mode 100644 server/modules/routing/roundrobinrouter/CMakeLists.txt create mode 100644 server/modules/routing/roundrobinrouter/roundrobinrouter.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4c63e163c..07baf45c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -187,6 +187,7 @@ add_subdirectory(plugins) add_subdirectory(query_classifier) add_subdirectory(server) add_subdirectory(include/maxscale) +add_subdirectory(Documentation) if(NOT WITHOUT_MAXADMIN) add_subdirectory(client) endif() diff --git a/Documentation/CMakeLists.txt b/Documentation/CMakeLists.txt new file mode 100644 index 000000000..721544125 --- /dev/null +++ b/Documentation/CMakeLists.txt @@ -0,0 +1 @@ +install_custom_file(Plugin-development-guide.md ${CMAKE_INSTALL_INCLUDEDIR}/maxscale/Documentation devel) diff --git a/Documentation/Plugin-development-guide.md b/Documentation/Plugin-development-guide.md new file mode 100644 index 000000000..5b43d11ed --- /dev/null +++ b/Documentation/Plugin-development-guide.md @@ -0,0 +1,566 @@ +# MariaDB MaxScale plugin development guide + +This document and the attached example code explain prospective plugin +developers the MariaDB MaxScale plugin API and also present and explain some +best practices and possible pitfalls in module development. We predict that +*filters* and *routers* are the module types developers are most likely to work +on, so the APIs of these two are discussed in detail. + +## Table of Contents + +1. [Introduction](#introduction) +* [Module categories](#module-categories) +* [Common definitions and headers](#common-definitions-and-headers) + 1. [Module information container](#module-information-container) +* [Module API](#module-api) + 1. [Overview](#overview) + * [General module management](#general-module-management) + * [Protocol](#protocol) + * [Authenticator](#authenticator) + * [Filter and Router](#filter-and-router) + * [Monitor](#monitor) +* [Compiling, installing and running](#compiling-installing-and-running) + 1. [Hands-on example: RoundRobinRouter](#hands-on-example-roundrobinrouter) +* [Summary and conclusion](#summary-and-conclusion) + +## Introduction + +MariaDB MaxScale is designed to be an extensible program. Much, if not most, of +the actual processing is done by plugin modules. Plugins receive network data, +process it and relay it to its destination. The MaxScale core loads plugins, +manages client sessions and threads and, most importantly, offers a selection of +functions for the plugins to call upon. This collection of functions is called +the *MaxScale Public Interface* or just **MPI** for short. + +The plugin modules are shared libraries (.so-files) implementing a set of +interface functions, the plugin API. Different plugin types have different APIs, +although there are similarities. The MPI is a set of C and C++ header files, +from which the module code includes the ones required. MariaDB MaxScale is +written in C/C++ and the plugin API is in pure C. Although it is possible +to write plugins in any language capable of exposing a C interface and +dynamically binding to the core program, in this document we assume plugin +modules are written in C++. + +The *RoundRobinRouter* is a practical example of a simple router plugin. The +RoundRobinRouter is compiled, installed and ran in [section +5.1](#hands-on-example:-roundrobinrouter). The source for the router is included +in the plugin development package. + +## Module categories + +This section lists all the module types and summarises their core tasks. The +modules are listed in the order a client packet would typically travel through. +For more information about a particular module type, see the corresponding +folder in `MaxScale/Documentation/`, located in the main MariaDB MaxScale +repository. + +**Protocol** modules implement I/O between clients and MaxScale, and between +MaxScale and backend servers. Protocol modules read and write to socket +descriptors using raw I/O functions provided by the MPI, and implement +protocol-specific I/O functions to be used through a common interface. The +Protocol module API is defined in `protocol.h`. Currently, the only implemented +database protocol is *MySQL*. Other protocols currently in use include *HTTPD* +and *maxscaled*, which are used by the MaxInfo and MaxAdmin modules. + +**Authenticator** modules retrieve user account information from the backend +databases, store it and use it to authenticate connecting clients. MariaDB +MaxScale includes authenticators for MySQL (normal and GSSApi). The +authenticator API is defined in `authenticator.h`. + +**Filter** modules process data from clients before routing. A data buffer may +travel through multiple filters before arriving in a router. For a data buffer +going from a backend to the client, the router receives it first and the +filters receive it in reverse order. MaxScale includes a healthly selection of +filters ranging from logging, overwriting query data and caching. The filter +API is defined in `filter.h`. + +**Router** modules route packets from the last filter in the filter chain to +backends and reply data from backends to the last filter. The routing decisions +may be based on a variety of conditions; typically packet contents and backend +status are the most significant factors. Routers are often used for load +balancing, dividing clients and even individual queries between backends. +Routers use protocol functions to write to backends, making them somewhat +protocol-agnostic. The router API is defined in `router.h`. + +**Monitor** modules do not process data flowing through MariaDB MaxScale, but +support the other modules in their operation by updating the status of the +backend servers. Monitors are ran in their own threads to minimize +interference to the worker threads. They periodically connect to all their +assigned backends, query their status and write the results in global structs. +The monitor API is defined in `monitor.h`. + +## Common definitions and headers + +Generally, most type definitions, macros and functions exposed by the MPI to be +used by modules are prefixed with **MXS**. This should avoid name collisions +in the case a module includes many symbols from the MPI. + +Every compilation unit in a module should begin with `#define MXS_MODULE_NAME +""`. This definition will be used by log macros for clarity, prepending +`` to every log message. Next, the module should +`#include ` (for C++) or `#include ` (for +C). These headers contain compilation environment dependent definitions and +global constants, and include some generally useful headers. Including one of +them first in every source file enables later global redefinitions across all +MaxScale modules. If your module is composed of multiple source files, the above +should be placed to a common header file included in the beginning of the source +files. The file with the module API definition should also include the header +for the module type, e.g. `filter.h`. + +Other common MPI header files required by most modules are listed in the table +below. + +Header | Contents +-------|---------- +`alloc.h` | Malloc, calloc etc. replacements +`buffer.h` | Packet buffer management +`config.h` | Configuration settings +`dcb.h` | I/O using descriptor control blocks +`debug.h` | Debugging macros +`modinfo.h` | Module information structure +`server.h` | Backend server information +`service.h` | Service definition +`session.h` | Client session definition +`logmanager.h` | Logging macros and functions + +### Module information container + +A module must implement the `MXS_CREATE_MODULE()`-function, which returns a +pointer to a `MXS_MODULE`-structure. This function is called by the module +loader during program startup. `MXS_MODULE` (type defined in `modinfo.h`) +contains function pointers to further module entrypoints, miscellaneous +information about the module and the configuration parameters accepted by the +module. This function must be exported without C++ name mangling, so in C++ code +it should be defined `extern "C"`. + +The information container describes the module in general and is constructed +once during program excecution. A module may have multiple *instances* with +different values for configuration parameters. For example, a filter module can +be used with two different configurations in different services (or even in the +same service). In this case the loader uses the same module information +container for both but creates two module instances. + +The MariaDB MaxScale configuration file `maxscale.cnf` is parsed by the core. +The core also checks that all the defined parameters are of the correct type for +the module. For this, the `MXS_MODULE`-structure includes a list of parameters +accepted by the module, defining parameter names, types and default values. In +the actual module code, parameter values should be extracted using functions +defined in `config.h`. + +## Module API + +### Overview + +This section explains some general concepts encountered when implementing a +module API. For more detailed information, see the module specific subsection, +header files or the doxygen documentation. + +Modules with configuration data define an *INSTANCE* object, which is created by +the module code in a `createInstance`-function or equivalent. The instance +creation function is called during MaxScale startup, usually when creating +services. MaxScale core holds the module instance data in the +`SERVICE`-structure (or other higher level construct) and gives it as a +parameter when calling functions from the module in question. The instance +structure should contain all non-client-specific information required by the +functions of the module. The core does not know what the object contains (since +it is defined by the module itself), nor will it modify the pointer or the +referenced object in any way. + +Modules dealing with client-specific data require a *SESSION* object for every +client. As with the instance data, the definition of the module session +structure is up to the module writer and MaxScale treats it as an opaque type. +Usually the session contains status indicators and any resources required by the +client. MaxScale core has its own `MXS_SESSION` object, which tracks a variety +of client related information. The `MXS_SESSION` is given as a parameter to +module-specific session creation functions and is required for several typical +operations such as connecting to backends. + +Descriptor control blocks (`DCB`), are generalized I/O descriptor types. DCBs +store the file descriptor, state, remote address, username, session, and other +data. DCBs are created whenever a new socket is created. Typically this happens +when a new client connects or MaxScale connects the client session to backend +servers. The module writer should use DCB handling functions provided by the MPI +to manage connections instead of calling general networking libraries. This +ensures that I/O is handled asynchronously by epoll. In general, module code +should avoid blocking I/O, *sleep*, *yield* or other potentially costly +operations, as the same thread is typically used for many client sessions. + +Network data such as client queries and backend replies are held in a buffer +container called `GWBUF`. Multiple GWBUFs can form a linked list with type +information and properties in each GWBUF-node. Each node includes a pointer to a +reference counted shared buffer (`SHARED_BUF`), which finally points to a slice +of the actual data. In effect, multiple GWBUF-chains can share some data while +keeping some parts private. This construction is meant to minimize the need for +data copying and makes it easy to append more data to partially received data +packets. Plugin writers should use the MPI to manipulate GWBUFs. For more +information on the GWBUF, see [Filter and Router](#filter-and-router). + +### General module management + +```java +int process_init() +void process_finish() +int thread_init() +void thread_finish() +``` + +These four functions are present in all `MXS_MODULE` structs and are not part of +the API of any individual module type. `process_init` and `process_finish` are +called by the module loader right after loading a module and just before +MaxScale terminates, respectively. Usually, these can be set to null in +`MXS_MODULE` unless the module needs some general initializations before +creating any instances. `thread_init` and `thread_finish` are thread-specific +equivalents. + +```java +void diagnostics(INSTANCE *instance, DCB *dcb) +``` + +A diagnostics printing routine is present in nearly all module types, although +with varying signatures. This entrypoint should print various statistics and +status information about the module instance `instance` in string form. The +target of the printing is the given DCB, and printing should be implemented by +calling `dcb_printf`. The diagnostics function is used by the *MaxInfo* and +*MaxAdmin* features. + +### Protocol + +```java +int32_t read(struct dcb *) +int32_t write(struct dcb *, GWBUF *) +int32_t write_ready(struct dcb *) +int32_t error(struct dcb *) +int32_t hangup(struct dcb *) +int32_t accept(struct dcb *) +int32_t connect(struct dcb *, struct server *, struct session *) +int32_t close(struct dcb *) +int32_t listen(struct dcb *, char *) +int32_t auth(struct dcb *, struct server *, struct session *, GWBUF *) +int32_t session(struct dcb *, void *) +char auth_default() +int32_t connlimit(struct dcb *, int limit) +``` + +Protocol modules are laborous to implement due to their low level nature. Each +DCB maintains pointers to the correct protocol functions to be used with it, +allowing the DCB to be used in a protocol-independent manner. + +`read`, `write_ready`, `error` and `hangup` are *epoll* handlers for their +respective events. `write` implements writing and is usually called in a router +module. `accept` is a listener socker handler. `connect` is used during session +creation when connecting to backend servers. `listen` creates a listener socket. +`close` closes a DCB created by `accept`, `connect` or `listen`. + +In the ideal case modules other than the protocol modules themselves should not +be protocol-specific. This is currently difficult to achieve, since many actions +in the modules are dependent on protocol-speficic details. In the future, +protocol modules may be expanded to implement a generic query parsing and +information API, allowing filters and routers to be used with different SQL +variants. + +### Authenticator + +```java +void* initialize(char **options) +void* create(void* instance) +int extract(struct dcb *, GWBUF *) +bool connectssl(struct dcb *) +int authenticate(struct dcb *) +void free(struct dcb *) +void destroy(void *) +int loadusers(struct servlistener *) +void diagnostic(struct dcb*, struct servlistener *) +int reauthenticate(struct dcb *, const char *user, uint8_t *token, + size_t token_len, uint8_t *scramble, size_t scramble_len, + uint8_t *output, size_t output_len); +``` + +Authenticators must communicate with the client or the backends and implement +authentication. The authenticators can be divided to client and backend modules, +although the two types are linked and must be used together. Authenticators are +also dependent on the protocol modules. + +### Filter and Router + +Filter and router APIs are nearly identical and are presented together. Since +these are the modules most likely to be implemented by plugin developers, their +APIs are discussed in more detail. + +```java +INSTANCE* createInstance(SERVICE* service, char** options) +void destroyInstance(INSTANCE* instance) +``` + +`createInstance` should read the `options` and initialize an instance object for +use with `service`. Often, simply saving the configuration values to fields is +enough. `destroyInstance` is called when the service using the module is +deallocated. It should free any resources claimed by the instance. All sessions +created by this instance should be closed before calling the destructor. + +```java +SESSION* newSession(INSTANCE* instance, MXS_SESSION* mxs_session) +void closeSession(INSTANCE* instance, SESSION* session) +void freeSession(INSTANCE* instance, SESSION* session) +``` + +These functions manage sessions. `newSession` should allocate a router or filter +session attached to the client session represented by `mxs_session`. MaxScale +will pass the returned pointer to all the API entrypoints that process user data +for the particular client. `closeSession` should close connections the session +has opened and release any resources specific to the served client. The +*SESSION* structure allocated in `newSession` should not be deallocated by +`closeSession` but in `freeSession`. These two are called in succession +by the core. + +```java +int routeQuery(INSTANCE *instance, SESSION session, GWBUF* queue) void +clientReply(INSTANCE* instance, SESSION session, GWBUF* queue, DCB *backend_dcb) +uint64_t getCapabilities(INSTANCE* instance) +``` + +`routeQuery` is called for client requests which should be routed to backends, +and `clientReply` for backend reply packets which should be routed to the +client. For some modules, MaxScale itself is the backend. For filters, these can +be NULL, in which case the filter will be skipped for that packet type. + +`routeQuery` is often the most complicated function in a router, as it +implements the routing logic. It typically considers the client request `queue`, +the router settings in `instance` and the session state in `session` when making +a routing decision. For filters aswell, `routeQuery` typically implements the +main logic, although the routing target is constant. For router modules, +`routeQuery` should send data forward with `dcb->func.write()`. Filters should +directly call `routeQuery` for the next filter or router in the chain. + +`clientReply` processes data flowing from backend back to client. For routers, +this function is often much simpler than `routeQuery`, since there is only one +client to route to. Depending on the router, some packets may not be routed to +the client. For example, if a client query was routed to multiple backends, +MaxScale will receive multiple replies while the client only expects one. +Routers should pass the reply packet to the last filter in the chain (reversed +order) using the macro `MXS_SESSION_ROUTE_REPLY`. Filters should call the +`clientReply` of the previous filter in the chain. There is no need for filters +to worry about being the first filter in the chain, as this is handled +transparently by the session creation routine. + +Application data is not always received in complete packets from the network +stack. How partial packets are handled by the receiving protocol module depends +on the attached filters and the router, communicated by their +`getCapabilities`-functions. `getCapabilities` should return a bitfield +resulting from ORring the individual capabilities. `routing.h` lists the allowed +capability flags. + +If a router or filter sets no capabilities, `routeQuery` or `clientReply` may be +called to route partial packets. If the routing logic does not require any +information on the contents of the packets or even tracking the number of +packets, this may be fine. For many cases though, receiving a data packet in a +complete GWBUF chain or in one contiguos GWBUF is required. The former can be +requested by `getCapabilities` returning *RCAP_TYPE_STMT*, the latter by +*RCAP_TYPE_CONTIGUOUS*. Separate settings exist for queries and replies. For +replies, an additional value, *RCAP_TYPE_RESULTSET_OUTPUT* is defined. This +requests the protocol module to gather partial results into one result set. +Enforcing complete packets will delay processing, since the protocol module will +have to wait for the entire data packet to arrive before sending it down the +processing chain. + +```java +void handleError(INSTANCE* instance,SESSION* session, GWBUF* errmsgbuf, + DCB* problem_dcb, mxs_error_action_t action, bool* succp); +``` + +This router-only entrypoint is called if `routeQuery` returns an error value or +if an error occurs in one of the connections listened to by the session. The +steps an error handler typically takes depend on the nature of the `problem_dcb` +and the error encountered. If `problem_dcb` is a client socket, then the session +is lost and should be closed. The error handler should not do this by itself and +just report the failure by setting `succp` to false. If `problem_dcb` is a +backend socket, then the error handler should try to connect to another backend +if the routing logic allows this. If the error is simply a failed authentication +on the backend, then it is usually best to send the message directly to the +client and close the session. + +### Monitor + +```java +MONITOR* startMonitor(MXS_MONITOR *monitor, const MXS_CONFIG_PARAMETER *params) +void stopMonitor(MXS_MONITOR *monitor) +void diagnostics(DCB *, const MXS_MONITOR *) +``` + +Monitor modules typically run a repeated monitor routine with a used defined +interval. The `MXS_MONITOR` is a standard monitor definition used for all +monitors and contains a void pointer for storing module specific data. +`startMonitor` should create a new thread for itself using functions in the MPI +and have it regularly run a monitor loop. In the beginning of every monitor +loop, the monitor should lock the `SERVER`-structures of its servers. This +prevents any administrative action from interfering with the monitor during its +pass. + +## Compiling, installing and running + +The requirements for compiling a module are: +* The public headers (MPI) +* A compatible compiler, typically GCC +* Libraries required by the public headers + +Some of the public header files themselves include headers from other libraries. +These libraries need to be installed and it may be required to point out their +location to gcc. Some of the more commonly required libraries are: + * *MySQL Connector-C*, used by the MySQL protocol module + * *pcre2 regular expressions* (libpcre2-dev), used for example by the header +`modutil.h` + +After all dependencies are accounted for, the module should compile with a +command similar to +``` +gcc -I /usr/local/include/mariadb -shared -fPIC -g -o libmymodule.so mymodule.cpp +``` +Large modules composed of several source files and using additional libraries +may require a more complicated compilation scheme, but that is outside the scope +of this document. The result of compiling a plugin should be a single shared +library file. + +The compiled .so-file needs to be copied to the MaxScale library folder, which +is `/usr/local/lib/maxscale` by default. MaxScale expects the filename to be +`lib.so`, where `` must match the module name given in the +configuration file. + +### Hands-on example: RoundRobinRouter + +In this example, the RoundRobinRouter is compiled, installed and tested. The +software environment this section was written and tested is listed below. Any +recent Linux setup should be applicaple. + +* Linux Mint 18 +* gcc 5.4.0, glibc 2.23 +* MariaDB MaxScale 2.1.0 debug build (binaries in `usr/local/maxscale`, modules in +`/usr/local/lib/maxscale`) +* MariaDB Connector-C 2.3.2 (installed to `/usr/local/lib/mariadb`, headers in +`/usr/local/include/mariadb`) +* `roundrobinrouter.cpp` in the current directory +* MaxScale plugin development headers (in `usr/include/maxscale`) + +**Step 1** Compile RoundRobinRouter with `$gcc -I /usr/local/include/mariadb +-shared -fPIC -g -o libroundrobinrouter.so roundrobinrouter.cpp`. +Assuming all headers were found, the shared library `libroundrobinrouter.so` +is produced. + +**Step 2** Copy the compiled module to the MaxScale module directory: `$sudo cp +libroundrobinrouter.so /usr/local/lib/maxscale`. + +**Step 3** Modify the MaxScale configuration file to use the RoundRobinRouter as +a router. Example service and listener definitions are below. The *servers* +and *write_backend*-lines should be configured according to the actual backend +configuration. + +``` +[RR Service] +type=service +router=roundrobinrouter +servers=LocalMaster1,LocalSlave1,LocalSlave2 +user=maxscale +passwd=maxscale +filters=MyLogFilter1 +max_backends=10 +write_backend=LocalMaster1 +print_on_routing=true +dummy_setting=two + +[RR Listener] +type=listener +service=RR Service +protocol=MySQLClient +port=4009 +``` + +**Step 4** Start MaxScale: `$ maxscale -d`. Output: +``` +MariaDB Corporation MaxScale 2.1.0 Mon Feb 20 17:22:18 2017 +------------------------------------------------------ +Info : MaxScale will be run in the terminal process. + See the log from the following log files : + +Configuration file : /etc/maxscale.cnf +Log directory : /var/log/maxscale +Data directory : /var/lib/maxscale +Module directory : /usr/local/lib/maxscale +Service cache : /var/cache/maxscale +``` + +**Step 5** Test with a MySQL client. The RoundRobinRouter has been tested with both a command line and a GUI client. With `DEBUG_RRROUTER` defined and `print_on_routing` enabled, the `/var/log/maxscale/maxscale.log` file will report nearly every action taken by the router. + +``` +2017-02-21 10:37:23 notice : [RoundRobinRouter] Creating instance. +2017-02-21 10:37:23 notice : [RoundRobinRouter] Settings read: +2017-02-21 10:37:23 notice : [RoundRobinRouter] 'max_backends': 10 +2017-02-21 10:37:23 notice : [RoundRobinRouter] 'write_backend': 0xf0ce70 +2017-02-21 10:37:23 notice : [RoundRobinRouter] 'print_on_routing': 1 +2017-02-21 10:37:23 notice : [RoundRobinRouter] 'dummy_setting': 2 +. +. +. +2017-02-21 10:37:37 notice : [RoundRobinRouter] Session with 4 connections created. +2017-02-21 10:37:37 notice : [RoundRobinRouter] QUERY: SHOW VARIABLES WHERE Variable_name in ('max_allowed_packet', 'system_time_zone', 'time_zone', 'sql_mode') +2017-02-21 10:37:37 notice : [RoundRobinRouter] Routing statement of length 110u to backend 'LocalMaster1'. +2017-02-21 10:37:37 notice : [RoundRobinRouter] Replied to client. +2017-02-21 10:37:37 notice : [RoundRobinRouter] QUERY: set session autocommit=1,sql_mode='NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES' +2017-02-21 10:37:37 notice : [RoundRobinRouter] Routing statement of length 103u to 4 backends. +2017-02-21 10:37:37 notice : [RoundRobinRouter] Replied to client. +2017-02-21 10:37:37 notice : [RoundRobinRouter] QUERY: SET @ApplicationName='DBeaver 3.8.5 - Main' +2017-02-21 10:37:37 notice : [RoundRobinRouter] Routing statement of length 48u to 4 backends. +2017-02-21 10:37:37 notice : [RoundRobinRouter] Replied to client. +2017-02-21 10:37:37 notice : [RoundRobinRouter] QUERY: select @@lower_case_table_names +2017-02-21 10:37:37 notice : [RoundRobinRouter] Routing statement of length 36u to backend 'LocalSlave1'. +2017-02-21 10:37:37 notice : [RoundRobinRouter] Replied to client. +``` + +**Step 5** Connect with MaxAdmin, print diagnostics and call a custom command. +``` +$sudo maxadmin +MaxScale> show service "RR Service" + Service: RR Service + Router: roundrobinrouter + State: Started + Queries routed successfully: 37 + Failed routing attempts: 0 + Client replies routed: 38 + Started: Tue Feb 21 11:52:08 2017 + Root user access: Disabled + Filter chain: MyLogFilter1 + Backend databases: + 127.0.0.1:3001 Protocol: MySQLBackend Name: LocalMaster1 + 127.0.0.1:3002 Protocol: MySQLBackend Name: LocalSlave1 + 127.0.0.1:3003 Protocol: MySQLBackend Name: LocalSlave2 + Total connections: 2 + Currently connected: 2 +MaxScale> call command rrrouter test_command "one" 0 +``` + +The result of the `test_command "one" 0` is printed to the terminal MaxScale is +running in: +``` +RoundRobinRouter wishes the Admin a good day. +The module got 2 arguments. +Argument 0: type 'string' value 'one' +Argument 1: type 'boolean' value 'false' +``` + +## Summary and conclusion + +Plugins offer a way to extend MariaDB MaxScale whenever the standard modules are +found insufficient. The plugins need only implement a set API, can be +independently compiled and installation is simply a file copy with some +configuration file modifications. + +Out of the different plugin types, filters are the easiest to implement. They +work independently and have few requirements. Protocol and authenticator modules +require indepth knowledge of the database protocol they implement. Router module +complexity depends on the routing logic requirements. + +The provided RoundRobinRouter example code should serve as a valid starting +point for both filters and routers. Studying the MaxScale Public Interface +headers to get a general idea of what services the core provides for plugins, +is also highly recommeded. + +Lastly, MariaDB MaxScale is an open-source project, so code contributions can be +accepted if they fulfill the +[requirements](https://github.com/mariadb-corporation/MaxScale/wiki/Contributing). diff --git a/server/modules/routing/CMakeLists.txt b/server/modules/routing/CMakeLists.txt index 74d00e3ee..f3034a7f6 100644 --- a/server/modules/routing/CMakeLists.txt +++ b/server/modules/routing/CMakeLists.txt @@ -11,6 +11,7 @@ add_subdirectory(maxinfo) add_subdirectory(readconnroute) add_subdirectory(readwritesplit) add_subdirectory(schemarouter) +add_subdirectory(roundrobinrouter) if(BUILD_TESTS) add_subdirectory(testroute) diff --git a/server/modules/routing/roundrobinrouter/CMakeLists.txt b/server/modules/routing/roundrobinrouter/CMakeLists.txt new file mode 100644 index 000000000..a858bab93 --- /dev/null +++ b/server/modules/routing/roundrobinrouter/CMakeLists.txt @@ -0,0 +1,6 @@ +add_library(roundrobinrouter SHARED roundrobinrouter.cpp) +target_link_libraries(roundrobinrouter maxscale-common) +set_target_properties(roundrobinrouter PROPERTIES VERSION "1.0.0") +install_module(roundrobinrouter core) + +install_custom_file(roundrobinrouter.cpp ${CMAKE_INSTALL_INCLUDEDIR}/maxscale/Documentation devel) diff --git a/server/modules/routing/roundrobinrouter/roundrobinrouter.cpp b/server/modules/routing/roundrobinrouter/roundrobinrouter.cpp new file mode 100644 index 000000000..23b5d9f22 --- /dev/null +++ b/server/modules/routing/roundrobinrouter/roundrobinrouter.cpp @@ -0,0 +1,897 @@ +/* + * Copyright (c) 2017 MariaDB Corporation Ab + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file and at www.mariadb.com/bsl. + * + * Change Date: 2019-07-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2 or later of the General + * Public License. + */ + +/** + * @file roundrobinrouter.c - Round-robin router load balancer + * + * This is an implementation of a simple query router that balances reads on a + * query level. The router is configured with a set of slaves and optionally + * a master. The router balances the client read queries over the set of slave + * servers, sending write operations to the master. Session-operations are sent + * to all slaves and the master. The read query balancing is done in round robin + * style: in each session, the slave servers (and the master if inserted into the + * slave list) take turns processing read queries. + * + * This router is intended to be a rather straightforward example on how to + * program a module for MariaDB MaxScale. The router does not yet support all + * SQL-commands and there are bound to be various limitations yet unknown. It + * does work on basic reads and writes. + * + */ + +/* The log macros use this definition. */ +#define MXS_MODULE_NAME "RoundRobinRouter" + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +//#define DEBUG_RRROUTER +#undef DEBUG_RROUTER + +#ifdef DEBUG_RRROUTER +#define RR_DEBUG(msg, ...) MXS_NOTICE(msg, ##__VA_ARGS__) +#else +#define RR_DEBUG(msg, ...) +#endif + +/* This router handles different query types in a different manner. Some queries + * require that a "write_backend" is set. */ +const uint32_t q_route_to_rr = (QUERY_TYPE_LOCAL_READ | QUERY_TYPE_READ | + QUERY_TYPE_MASTER_READ | QUERY_TYPE_USERVAR_READ | + QUERY_TYPE_SYSVAR_READ | QUERY_TYPE_GSYSVAR_READ | + QUERY_TYPE_SHOW_DATABASES | QUERY_TYPE_SHOW_TABLES); + +const uint32_t q_route_to_all = (QUERY_TYPE_SESSION_WRITE | QUERY_TYPE_USERVAR_WRITE | + QUERY_TYPE_GSYSVAR_WRITE | QUERY_TYPE_ENABLE_AUTOCOMMIT | + QUERY_TYPE_DISABLE_AUTOCOMMIT); + +const uint32_t q_trx_begin = QUERY_TYPE_BEGIN_TRX; + +const uint32_t q_trx_end = (QUERY_TYPE_ROLLBACK | QUERY_TYPE_COMMIT); + +const uint32_t q_route_to_write = (QUERY_TYPE_WRITE | QUERY_TYPE_PREPARE_NAMED_STMT | + QUERY_TYPE_PREPARE_STMT | QUERY_TYPE_EXEC_STMT | + QUERY_TYPE_CREATE_TMP_TABLE | QUERY_TYPE_READ_TMP_TABLE); + +const char MAX_BACKENDS[] = "max_backends"; +const char WRITE_BACKEND[] = "write_backend"; +const char PRINT_ON_ROUTING[] = "print_on_routing"; +const char DUMMY[] = "dummy_setting"; + +/* Enum setting definition example */ +static const MXS_ENUM_VALUE enum_example[] = +{ + {"two", 2}, + {"zero", 0}, + {NULL} /* Last must be NULL */ +}; + +static modulecmd_arg_type_t custom_cmd_args[] = +{ + {MODULECMD_ARG_STRING, "Example string"}, + {(MODULECMD_ARG_BOOLEAN | MODULECMD_ARG_OPTIONAL), "This is an optional bool parameter"} +}; + +bool custom_cmd_example(const MODULECMD_ARG *argv); + +using std::string; +using std::cout; + +typedef std::vector DCB_VEC; + +class RRRouter; +class RRRouterSession; + +/* Each service using this router will have a router object instance. */ +class RRRouter +{ +private: + SERVICE* m_service; /* Service this router is part of */ + /* Router settings */ + unsigned int m_max_backends; /* How many backend servers to use */ + SERVER* m_write_server; /* Where to send write etc. "unsafe" queries */ + bool m_print_on_routing; /* Print a message on every packet routed? */ + uint64_t m_example_enum; /* Not used */ + + void decide_target(RRRouterSession* rses, GWBUF* querybuf, DCB*& target, bool& route_to_all); +public: + /* Statistics, written to by multiple threads */ + volatile unsigned long int m_routing_s; /* Routing success */ + volatile unsigned long int m_routing_f; /* Routing fail */ + volatile unsigned long int m_routing_c; /* Client packets routed */ + + /* Methods */ + RRRouter(SERVICE* service); + ~RRRouter(); + RRRouterSession* create_session(MXS_SESSION* session); + int route_query(RRRouterSession* rses, GWBUF* querybuf); + void client_reply(RRRouterSession* rses, GWBUF* buf, DCB* backend_dcb); + void handle_error(RRRouterSession* rses, GWBUF* message, DCB* problem_dcb, + mxs_error_action_t action, bool* succp); +}; + +/* Every client connection has a corresponding session. */ +class RRRouterSession +{ +public: + bool m_closed; /* true when closeSession is called */ + DCB_VEC m_backend_dcbs; /* backends */ + DCB* m_write_dcb; /* write backend */ + DCB* m_client_dcb; /* client */ + unsigned int m_route_count; /* how many packets have been routed */ + bool m_on_transaction; /* Is the session in transaction mode? */ + unsigned int m_replies_to_ignore; /* Counts how many replies should be ignored. */ + + RRRouterSession(DCB_VEC&, DCB*, DCB*); + ~RRRouterSession(); + void close(); +}; + +RRRouter::RRRouter(SERVICE* service) + : m_service(service) + , m_routing_s(0) + , m_routing_f(0) + , m_routing_c(0) +{ + RR_DEBUG("Creating instance."); + /* Read options specific to round robin router. */ + MXS_CONFIG_PARAMETER* params = service->svc_config_param; + m_max_backends = config_get_integer(params, MAX_BACKENDS); + m_write_server = config_get_server(params, WRITE_BACKEND); + m_print_on_routing = config_get_bool(params, PRINT_ON_ROUTING); + m_example_enum = config_get_enum(params, DUMMY, enum_example); + + RR_DEBUG("Settings read:"); + RR_DEBUG("'%s': %d", MAX_BACKENDS, m_max_backends); + RR_DEBUG("'%s': %p", WRITE_BACKEND, m_write_server); + RR_DEBUG("'%s': %d", PRINT_ON_ROUTING, m_print_on_routing); + RR_DEBUG("'%s': %lu", DUMMY, m_example_enum); +} +RRRouter::~RRRouter() +{ + RR_DEBUG("Deleting router instance."); + RR_DEBUG("Queries routed successfully: %lu", m_routing_s); + RR_DEBUG("Failed routing attempts: %lu", m_routing_f); + RR_DEBUG("Client replies: %lu", m_routing_c); +} + +RRRouterSession* RRRouter::create_session(MXS_SESSION* session) +{ + DCB_VEC backends; + DCB* write_dcb = NULL; + RRRouterSession* rses = NULL; + try + { + /* Try to connect to as many backends as required. */ + SERVER_REF* sref; + for (sref = m_service->dbref; sref != NULL; sref = sref->next) + { + if (SERVER_REF_IS_ACTIVE(sref) && (backends.size() < m_max_backends)) + { + /* Connect to server */ + DCB* conn = dcb_connect(sref->server, session, + sref->server->protocol); + if (conn) + { + /* Success */ + atomic_add(&sref->connections, 1); + conn->service = session->service; + backends.push_back(conn); + } /* Any error by dcb_connect is reported by the function itself */ + } + } + if (m_write_server) + { + /* Connect to write backend server. This is not essential. */ + write_dcb = dcb_connect(m_write_server, session, m_write_server->protocol); + if (write_dcb) + { + /* Success */ + write_dcb->service = session->service; + } + } + if (backends.size() < 1) + { + MXS_ERROR("Session creation failed, could not connect to any " + "read backends."); + } + else + { + rses = new RRRouterSession(backends, write_dcb, session->client_dcb); + RR_DEBUG("Session with %lu connections created.", + backends.size() + (write_dcb ? 1 : 0)); + } + } + catch (const std::exception& x) + { + MXS_ERROR("Caught exception: %s", x.what()); + /* Close any connections already made */ + for (unsigned int i = 0; i < backends.size(); i++) + { + DCB* dcb = backends[i]; + dcb_close(dcb); + atomic_add(&(m_service->dbref->connections), -1); + } + backends.clear(); + if (write_dcb) + { + dcb_close(write_dcb); + } + } + return rses; +} + +int RRRouter::route_query(RRRouterSession* rses, GWBUF* querybuf) +{ + int rval = 0; + const bool print = m_print_on_routing; + DCB* target = NULL; + bool route_to_all = false; + + if (!rses->m_closed) + { + decide_target(rses, querybuf, target, route_to_all); + } + + /* Target selection done, write to dcb. */ + if (target) + { + /* We have one target backend */ + if (print) + { + MXS_NOTICE("Routing statement of length %du to backend '%s'.", + gwbuf_length(querybuf), target->server->unique_name); + } + /* Do not use dcb_write() to output to a dcb. dcb_write() is used only + * for raw write in the procol modules. */ + rval = target->func.write(target, querybuf); + /* After write, the buffer points to non-existing data. */ + querybuf = NULL; + } + else if (route_to_all) + { + int n_targets = rses->m_backend_dcbs.size() + (rses->m_write_dcb ? 1 : 0); + if (print) + { + MXS_NOTICE("Routing statement of length %du to %d backends.", + gwbuf_length(querybuf), n_targets); + } + int route_success = 0; + for (unsigned int i = 0; i < rses->m_backend_dcbs.size(); i++) + { + DCB* dcb = rses->m_backend_dcbs[i]; + /* Need to clone the buffer since write consumes it */ + GWBUF* copy = gwbuf_clone(querybuf); + if (copy) + { + route_success += dcb->func.write(dcb, copy); + } + } + if (rses->m_write_dcb) + { + GWBUF* copy = gwbuf_clone(querybuf); + if (copy) + { + route_success += rses->m_write_dcb->func.write(rses->m_write_dcb, + copy); + } + } + rses->m_replies_to_ignore += route_success - 1; + rval = (route_success == n_targets) ? 1 : 0; + gwbuf_free(querybuf); + } + else + { + MXS_ERROR("Could not find a valid routing backend. Either the " + "'%s' is not set or the command is not recognized.", + WRITE_BACKEND); + gwbuf_free(querybuf); + } + if (rval == 1) + { + /* Non-atomic update of shared data, but contents are non-essential */ + m_routing_s++; + } + else + { + m_routing_f++; + } + return rval; +} + +void RRRouter::client_reply(RRRouterSession* rses, GWBUF* buf, DCB* backend_dcb) +{ + if (rses->m_replies_to_ignore > 0) + { + /* In this case MaxScale cloned the message to many backends but the client + * expects just one reply. Assume that client does not send next query until + * previous has been answered. + */ + rses->m_replies_to_ignore--; + gwbuf_free(buf); + return; + } + + MXS_SESSION_ROUTE_REPLY(backend_dcb->session, buf); + + m_routing_c++; + if (m_print_on_routing) + { + MXS_NOTICE("Replied to client.\n"); + } +} +void RRRouter::handle_error(RRRouterSession* rses, GWBUF* message, + DCB* problem_dcb, mxs_error_action_t action, bool* succp) +{ + /* Don't handle same error twice on same DCB */ + if (problem_dcb->dcb_errhandle_called) + { + /* Assume that previous call succeed. */ + *succp = true; + return; + } + else + { + problem_dcb->dcb_errhandle_called = true; + } + + MXS_SESSION* session = problem_dcb->session; + DCB* client_dcb = session->client_dcb; + mxs_session_state_t sesstate = session->state; + + /* If the erroneous dcb is a client handler, close it. Setting succp to + * false will cause the entire attached session to be closed. + */ + if (problem_dcb->dcb_role == DCB_ROLE_CLIENT_HANDLER) + { + dcb_close(problem_dcb); + *succp = false; + } + else + { + switch (action) + { + case ERRACT_REPLY_CLIENT: + { + /* React to failed authentication, send message to client */ + if (sesstate == SESSION_STATE_ROUTER_READY) + { + /* Send error report to client */ + GWBUF* copy = gwbuf_clone(message); + if (copy) + { + client_dcb->func.write(client_dcb, copy); + } + } + *succp = false; + } + break; + case ERRACT_NEW_CONNECTION: + { + /* React to a failed backend */ + if (problem_dcb->dcb_role == DCB_ROLE_BACKEND_HANDLER) + { + if (problem_dcb == rses->m_write_dcb) + { + dcb_close(rses->m_write_dcb); + rses->m_write_dcb = NULL; + } + else + { + /* Find dcb in the list of backends */ + DCB_VEC::iterator iter = rses->m_backend_dcbs.begin(); + while (iter != rses->m_backend_dcbs.end()) + { + if (*iter == problem_dcb) + { + dcb_close(*iter); + rses->m_backend_dcbs.erase(iter); + break; + } + } + } + + /* If there is still backends remaining, return true since + * router can still function. + */ + *succp = (rses->m_backend_dcbs.size() > 0) ? true : false; + } + } + break; + default: + ss_dassert(!true); + *succp = false; + break; + } + } +} + +RRRouterSession::RRRouterSession(DCB_VEC& backends, DCB* write, DCB* client) + : m_closed(false) + , m_route_count(0) + , m_on_transaction(false) + , m_replies_to_ignore(0) +{ + m_backend_dcbs = backends; + m_write_dcb = write; + m_client_dcb = client; +} + +RRRouterSession::~RRRouterSession() +{ + /* Shouldn't happen. */ + ss_dassert(m_closed); +} + +void RRRouterSession::close() +{ + if (!m_closed) + { + /** + * Mark router session as closed. @c m_closed is checked at the start + * of most API functions to quickly stop the processing of closed sessions. + */ + m_closed = true; + int closed_conns = 0; + for (unsigned int i = 0; i < m_backend_dcbs.size(); i++) + { + DCB* dcb = m_backend_dcbs[i]; + SERVER_REF* sref = dcb->service->dbref; + dcb_close(dcb); + closed_conns++; + atomic_add(&(sref->connections), -1); + } + m_backend_dcbs.clear(); + if (m_write_dcb) + { + dcb_close(m_write_dcb); + m_write_dcb = NULL; + closed_conns++; + } + RR_DEBUG("Session with %d connections closed.", closed_conns); + } +} + +void RRRouter::decide_target(RRRouterSession* rses, GWBUF* querybuf, DCB*& target, bool& route_to_all) +{ + /* Extract the command type from the SQL-buffer */ + mysql_server_cmd_t cmd_type = MYSQL_GET_COMMAND(GWBUF_DATA(querybuf)); + /* The "query_types" is only really valid for query-commands but let's use + * it here for all command types. + */ + uint32_t query_types = 0; + + switch (cmd_type) + { + case MYSQL_COM_QUERY: + { + /* Use the inbuilt query_classifier to get information about + * the query. The default qc works with mySQL-queries. + */ + query_types = qc_get_type_mask(querybuf); + +#ifdef DEBUG_RRROUTER + char* zSql_query = NULL; + int length = 0; + modutil_extract_SQL(querybuf, &zSql_query, &length); + string sql_query(zSql_query, length); + RR_DEBUG("QUERY: %s", sql_query.c_str()); +#endif + } + break; + case MYSQL_COM_INIT_DB: + query_types = q_route_to_all; + RR_DEBUG("MYSQL_COM_INIT_DB"); + break; + case MYSQL_COM_QUIT: + query_types = q_route_to_all; + RR_DEBUG("MYSQL_COM_QUIT"); + break; + case MYSQL_COM_FIELD_LIST: + query_types = q_route_to_rr; + RR_DEBUG("MYSQL_COM_FIELD_LIST"); + break; + default: + /* TODO: Add support for other commands if needed. */ + /* This error message will only print the number of the cmd. */ + MXS_ERROR("Received unexpected sql command type: '%d'.", cmd_type); + break; + } + + if ((query_types & q_route_to_write) != 0) + { + target = rses->m_write_dcb; + } + else + { + /* This is not yet sufficient for handling transactions. */ + if ((query_types & q_trx_begin) != 0) + { + rses->m_on_transaction = true; + } + if (rses->m_on_transaction) + { + /* If a transaction is going on, route all to write backend */ + target = rses->m_write_dcb; + } + if ((query_types & q_trx_end) != 0) + { + rses->m_on_transaction = false; + } + + if (!target && ((query_types & q_route_to_rr) != 0)) + { + /* Round robin backend. */ + unsigned int index = (rses->m_route_count++) % rses->m_backend_dcbs.size(); + target = rses->m_backend_dcbs[index]; + } + /* Some commands and queries are routed to all backends. */ + else if (!target && ((query_types & q_route_to_all) != 0)) + { + route_to_all = true; + } + } +} + +/* + * The functions implementing the router module API. These do not need to be + * "extern C", but they do need to be callable from C code. + */ +static MXS_ROUTER* createInstance(SERVICE* service, char** options); +static void* newSession(MXS_ROUTER* instance, MXS_SESSION* session); +static void closeSession(MXS_ROUTER* instance, void* session); +static void freeSession(MXS_ROUTER* instance, void* session); +static int routeQuery(MXS_ROUTER* instance, void* session, GWBUF* querybuf); +static void diagnostics(MXS_ROUTER* instance, DCB* dcb); +static void clientReply(MXS_ROUTER* instance, void* router_session, GWBUF* resultbuf, + DCB* backend_dcb); +static void handleError(MXS_ROUTER* instance, void* router_session, + GWBUF* errmsgbuf, DCB* backend_dcb, + mxs_error_action_t action, bool* succp); +static uint64_t getCapabilities(MXS_ROUTER *instance); +static void destroyInstance(MXS_ROUTER* instance); +/* The next two entry points are usually optional. */ +static int process_init(); +static void process_finish(); + +/* + * This is called by the module loader during MaxScale startup. A module + * description, including entrypoints and allowed configuration parameters, + * is returned. This function must be exported. + */ +extern "C" MXS_MODULE* MXS_CREATE_MODULE(); +MXS_MODULE* MXS_CREATE_MODULE() +{ + static MXS_ROUTER_OBJECT entryPoints = + { + createInstance, + newSession, + closeSession, + freeSession, + routeQuery, + diagnostics, + clientReply, + handleError, + getCapabilities, + destroyInstance + }; + + static MXS_MODULE moduleObject = + { + MXS_MODULE_API_ROUTER, /* Module type */ + MXS_MODULE_BETA_RELEASE, /* Release status */ + MXS_ROUTER_VERSION, /* Implemented module API version */ + "A simple round robin router", /* Description */ + "V1.1.0", /* Module version */ + &entryPoints, /* Defined above */ + process_init, /* Process init, can be null */ + process_finish, /* Process finish, can be null */ + NULL, /* Thread init */ + NULL, /* Thread finish */ + { + /* Next is an array of MODULE_PARAM structs, max 64 items. These define all + * the possible parameters that this module accepts. This is required + * since the module loader also parses the configuration file for the module. + * Any unrecognised parameters in the config file are discarded. + * + * Note that many common parameters, such as backend servers, are + * already set to the upper level "service"-object. + */ + { /* For simple types, only 3 of the 5 struct fields need to be + * defined. */ + MAX_BACKENDS, /* Setting identifier in maxscale.cnf */ + MXS_MODULE_PARAM_INT, /* Setting type */ + "0" /* Default value */ + }, + {PRINT_ON_ROUTING, MXS_MODULE_PARAM_BOOL, "false"}, + {WRITE_BACKEND, MXS_MODULE_PARAM_SERVER, NULL}, + { /* Enum types require an array with allowed values. */ + DUMMY, + MXS_MODULE_PARAM_ENUM, + "the_answer", + MXS_MODULE_OPT_NONE, + enum_example + }, + {MXS_END_MODULE_PARAMS} + } + }; + return &moduleObject; +} + +/** + * @brief Create an instance of the router (API). + * + * Create an instance of the round robin router. One instance of the router is + * created for each service that is defined in the configuration as using this + * router. One instance of the router will handle multiple connections + * (router sessions). + * + * @param service The service this router is being created for + * @param options The options for this query router + * @return NULL in failure, pointer to router in success. + */ +static MXS_ROUTER* createInstance(SERVICE* service, char** options) +{ + RRRouter* instance = NULL; + /* The core of MaxScale is written in C and does not understand exceptions. + * The macro catches all exceptions. Add custom handling here if required. */ + MXS_EXCEPTION_GUARD(instance = new RRRouter(service)); + /* The MXS_ROUTER is just a void*. Only the router module will dereference + * the pointer. */ + + /* Register a custom command */ + if (!modulecmd_register_command("rrrouter", "test_command", custom_cmd_example, + 2, custom_cmd_args)) + { + MXS_ERROR("Module command registration failed."); + } + return (MXS_ROUTER*)instance; +} + +/** + * @brief Create a new router session for this router instance (API). + * + * Connect a client session to the router instance and return a router session. + * The router session stores all client specific data required by the router. + * + * @param instance The router object instance + * @param session The MaxScale session (generic client connection data) + * @return Client specific data for this router + */ +static void* newSession(MXS_ROUTER* instance, MXS_SESSION* session) +{ + RRRouter* router = (RRRouter*)instance; + RRRouterSession* rses = NULL; + MXS_EXCEPTION_GUARD(rses = router->create_session(session)); + return (void*)rses; +} +/** + * @brief Close an existing router session for this router instance (API). + * + * Close a client session attached to the router instance. This function should + * close connections and release other resources allocated in "newSession" or + * otherwise held by the router session. This function should NOT free the + * session object itself. + * + * @param instance The router object instance + * @param session The router session + */ +static void closeSession(MXS_ROUTER* instance, void* session) +{ + RRRouterSession* rses = (RRRouterSession*)session; + MXS_EXCEPTION_GUARD(rses->close()); +} + +/** + * @brief Free a router session (API). + * + * When a router session has been closed, freeSession may be called to free + * allocated resources. + * + * @param instance The router instance the session belongs to + * @param session Router client session + * + */ +static void freeSession(MXS_ROUTER* instance, void* session) +{ + RRRouterSession* rses = (RRRouterSession*)session; + delete rses; +} + +/** + * @brief Route a packet (API) + * + * The routeQuery function receives a packet and makes the routing decision + * based on the contents of the router instance, router session and the query + * itself. It then sends the query to the target backend(s). + * + * @param instance Router instance + * @param session Router session associated with the client + * @param buffer Buffer containing the query (or command) + * @return 1 on success, 0 on error + */ +static int routeQuery(MXS_ROUTER* instance, void* session, GWBUF* buffer) +{ + RRRouter* router = (RRRouter*)instance; + RRRouterSession* rses = (RRRouterSession*)session; + int rval = 0; + MXS_EXCEPTION_GUARD(rval = router->route_query(rses, buffer)); + return rval; +} + +/** + * @brief Diagnostics routine (API) + * + * Print router statistics to the DCB passed in. This is usually called by the + * MaxInfo or MaxAdmin modules. + * + * @param instance The router instance + * @param dcb The DCB for diagnostic output + */ +static void diagnostics(MXS_ROUTER* instance, DCB* dcb) +{ + RRRouter* router = (RRRouter*)instance; + dcb_printf(dcb, "\t\tQueries routed successfully: %lu\n", router->m_routing_s); + dcb_printf(dcb, "\t\tFailed routing attempts: %lu\n", router->m_routing_f); + dcb_printf(dcb, "\t\tClient replies routed: %lu\n", router->m_routing_c); +} + +/** + * @brief Client Reply routine (API) + * + * This routine receives a packet from a backend server meant for the client. + * Often, there is little logic needed and the packet can just be forwarded to + * the next element in the processing chain. + * + * @param instance The router instance + * @param session The router session + * @param backend_dcb The backend DCB (data source) + * @param queue The GWBUF with reply data + */ +static void clientReply(MXS_ROUTER* instance, void* session, GWBUF* queue, + DCB* backend_dcb) +{ + RRRouter* router = (RRRouter*)instance; + RRRouterSession* rses = (RRRouterSession*)session; + MXS_EXCEPTION_GUARD(router->client_reply(rses, queue, backend_dcb)); +} + +/** + * Error Handler routine (API) + * + * This routine will handle errors that occurred with the session. This function + * is called if routeQuery() returns 0 instead of 1. The client or a backend + * unexpectedly closing a connection also triggers this routine. + * + * @param instance The router instance + * @param session The router session + * @param message The error message to reply + * @param problem_dcb The DCB related to the error + * @param action The action: ERRACT_NEW_CONNECTION or ERRACT_REPLY_CLIENT + * @param succp Output result of action, true if router can continue + */ +static void handleError(MXS_ROUTER* instance, void* session, + GWBUF* message, DCB* problem_dcb, + mxs_error_action_t action, bool* succp) +{ + RRRouter* router = (RRRouter*)instance; + RRRouterSession* rses = (RRRouterSession*)session; + MXS_EXCEPTION_GUARD(router->handle_error(rses, message, problem_dcb, action, succp)); +} + +/** + * @brief Get router capabilities (API) + * + * Return a bit map indicating the characteristics of this router type. + * In this case, the only bit set indicates that the router wants to receive + * data for routing as whole SQL statements. + * + * @param instance The router instance + * @return RCAP_TYPE_CONTIGUOUS_INPUT | RCAP_TYPE_STMT_OUTPUT + */ +static uint64_t getCapabilities(MXS_ROUTER *instance) +{ + /* This router needs to parse client queries, so it should set RCAP_TYPE_CONTIGUOUS_INPUT. + * For output, parsing is not required but counting SQL replies is. RCAP_TYPE_RESULTSET_OUTPUT + * should be sufficient. + */ + return RCAP_TYPE_CONTIGUOUS_INPUT | RCAP_TYPE_RESULTSET_OUTPUT; +} + +/** + * @brief Destroy router instance (API) + * + * A destroy-function is rarely required, since this is usually called only at + * program exit. + * + * @param instance Router instance + */ +static void destroyInstance(MXS_ROUTER* instance) +{ + RRRouter* router = (RRRouter*)instance; + delete router; +} + +/** + * Make any initializations required by the router module as a whole and not + * specific to any individual router instance. + * + * @return 0 on success + */ +static int process_init() +{ + RR_DEBUG("Module loaded."); + return 0; +} +/** + * Undo module initializations. + */ +static void process_finish() +{ + RR_DEBUG("Module unloaded."); +} + +/** + * A function executed as a custom module command through MaxAdmin + * @param argv The arguments + */ +bool custom_cmd_example(const MODULECMD_ARG *argv) +{ + cout << MXS_MODULE_NAME << " wishes the Admin a good day.\n"; + int n_args = argv->argc; + cout << "The module got " << n_args << " arguments.\n"; + for (int i = 0; i < n_args; i++) + { + arg_node node = argv->argv[i]; + string type_str; + string val_str; + switch (MODULECMD_GET_TYPE(&node.type)) + { + case MODULECMD_ARG_STRING: + { + type_str = "string"; + val_str.assign(node.value.string); + } + break; + case MODULECMD_ARG_BOOLEAN: + { + type_str = "boolean"; + val_str.assign((node.value.boolean) ? "true" : "false"); + } + break; + default: + { + type_str = "other"; + val_str.assign("unknown"); + } + break; + } + cout << "Argument " << i << ": type '" << type_str << "' value '" << val_str << + "'\n"; + } + return true; +}