/* * Copyright (c) 2016 MariaDB Corporation Ab * * Use of this software is governed by the Business Source License included * in the LICENSE.TXT file and at www.mariadb.com/bsl11. * * Change Date: 2020-01-01 * * On the date above, in accordance with the Business Source License, use * of this software will be governed by version 2 or later of the General * Public License. */ #include "readwritesplit.hh" #include "rwsplit_internal.hh" #include #include #include #include #include #include #include #include #include #include #include #include #include #define RWSPLIT_TRACE_MSG_LEN 1000 /** * Functions within the read-write split router that are specific to * MySQL. The aim is to either remove these into a separate module or to * move them into the MySQL protocol modules. */ /* * The following functions are called from elsewhere in the router and * are defined in rwsplit_internal.hh. They are not intended to be called * from outside this router. */ /* This could be placed in the protocol, with a new API entry point * It is certainly MySQL specific. Packet types are DB specific, but can be * assumed to be enums, which can be handled as integers without knowing * which DB is involved until the packet type needs to be interpreted. * */ /** * @brief Determine the type of a query * * @param querybuf GWBUF containing the query * @param packet_type Integer denoting DB specific enum * @param non_empty_packet Boolean to be set by this function * * @return uint32_t the query type; also the non_empty_packet bool is set */ uint32_t determine_query_type(GWBUF *querybuf, int command) { uint32_t type = QUERY_TYPE_UNKNOWN; switch (command) { case MYSQL_COM_QUIT: /*< 1 QUIT will close all sessions */ case MYSQL_COM_INIT_DB: /*< 2 DDL must go to the master */ case MYSQL_COM_REFRESH: /*< 7 - I guess this is session but not sure */ case MYSQL_COM_DEBUG: /*< 0d all servers dump debug info to stdout */ case MYSQL_COM_PING: /*< 0e all servers are pinged */ case MYSQL_COM_CHANGE_USER: /*< 11 all servers change it accordingly */ case MYSQL_COM_SET_OPTION: /*< 1b send options to all servers */ type = QUERY_TYPE_SESSION_WRITE; break; case MYSQL_COM_CREATE_DB: /**< 5 DDL must go to the master */ case MYSQL_COM_DROP_DB: /**< 6 DDL must go to the master */ case MYSQL_COM_STMT_CLOSE: /*< free prepared statement */ case MYSQL_COM_STMT_SEND_LONG_DATA: /*< send data to column */ case MYSQL_COM_STMT_RESET: /*< resets the data of a prepared statement */ type = QUERY_TYPE_WRITE; break; case MYSQL_COM_QUERY: type = qc_get_type_mask(querybuf); break; case MYSQL_COM_STMT_PREPARE: type = qc_get_type_mask(querybuf); type |= QUERY_TYPE_PREPARE_STMT; break; case MYSQL_COM_STMT_EXECUTE: /** Parsing is not needed for this type of packet */ type = QUERY_TYPE_EXEC_STMT; break; case MYSQL_COM_SHUTDOWN: /**< 8 where should shutdown be routed ? */ case MYSQL_COM_STATISTICS: /**< 9 ? */ case MYSQL_COM_PROCESS_INFO: /**< 0a ? */ case MYSQL_COM_CONNECT: /**< 0b ? */ case MYSQL_COM_PROCESS_KILL: /**< 0c ? */ case MYSQL_COM_TIME: /**< 0f should this be run in gateway ? */ case MYSQL_COM_DELAYED_INSERT: /**< 10 ? */ case MYSQL_COM_DAEMON: /**< 1d ? */ default: break; } return type; } /* * This appears to be MySQL specific */ /** * @brief Determine if a packet contains a SQL query * * Packet type tells us this, but in a DB specific way. This function is * provided so that code that is not DB specific can find out whether a packet * contains a SQL query. Clearly, to be effective different functions must be * called for different DB types. * * @param packet_type Type of packet (integer) * @return bool indicating whether packet contains a SQL query */ bool is_packet_a_query(int packet_type) { return (packet_type == MYSQL_COM_QUERY); } /* * This one is problematic because it is MySQL specific, but also router * specific. */ /** * @brief Log the transaction status * * The router session and the query buffer are used to log the transaction * status, along with the query type (which is a generic description that * should be usable across all DB types). * * @param rses Router session * @param querybuf Query buffer * @param qtype Query type */ void log_transaction_status(RWSplitSession *rses, GWBUF *querybuf, uint32_t qtype) { if (rses->load_data_state == LOAD_DATA_INACTIVE) { uint8_t *packet = GWBUF_DATA(querybuf); unsigned char command = packet[4]; int len = 0; char* sql; modutil_extract_SQL(querybuf, &sql, &len); char *qtypestr = qc_typemask_to_string(qtype); if (len > RWSPLIT_TRACE_MSG_LEN) { len = RWSPLIT_TRACE_MSG_LEN; } MXS_SESSION *ses = rses->client_dcb->session; const char *autocommit = session_is_autocommit(ses) ? "[enabled]" : "[disabled]"; const char *transaction = session_trx_is_active(ses) ? "[open]" : "[not open]"; const char *querytype = qtypestr == NULL ? "N/A" : qtypestr; const char *hint = querybuf->hint == NULL ? "" : ", Hint:"; const char *hint_type = querybuf->hint == NULL ? "" : STRHINTTYPE(querybuf->hint->type); MXS_INFO("> Autocommit: %s, trx is %s, cmd: (0x%02x) %s, type: %s, stmt: %.*s%s %s", autocommit, transaction, command, STRPACKETTYPE(command), querytype, len, sql, hint, hint_type); MXS_FREE(qtypestr); } else { MXS_INFO("> Processing LOAD DATA LOCAL INFILE: %lu bytes sent.", rses->rses_load_data_sent); } } /* * This is mostly router code, but it contains MySQL specific operations that * maybe could be moved to the protocol module. The modutil functions are mostly * MySQL specific and could migrate to the MySQL protocol; likewise the * utility to convert packet type to a string. The aim is for most of this * code to remain as part of the router. */ /** * @brief Operations to be carried out if request is for all backend servers * * If the choice of sending to all backends is in conflict with other bit * settings in route_target, then error messages are written to the log. * * Otherwise, the function route_session_write is called to carry out the * actual routing. * * @param route_target Bit map indicating where packet should be routed * @param inst Router instance * @param rses Router session * @param querybuf Query buffer containing packet * @param packet_type Integer (enum) indicating type of packet * @param qtype Query type * @return bool indicating whether the session can continue */ bool handle_target_is_all(route_target_t route_target, RWSplit *inst, RWSplitSession *rses, GWBUF *querybuf, int packet_type, uint32_t qtype) { bool result = false; if (TARGET_IS_MASTER(route_target) || TARGET_IS_SLAVE(route_target)) { /** * Conflicting routing targets. Return an error to the client. */ char *query_str = modutil_get_query(querybuf); char *qtype_str = qc_typemask_to_string(qtype); MXS_ERROR("Can't route %s:%s:\"%s\". SELECT with session data " "modification is not supported if configuration parameter " "use_sql_variables_in=all .", STRPACKETTYPE(packet_type), qtype_str, (query_str == NULL ? "(empty)" : query_str)); GWBUF *errbuf = modutil_create_mysql_err_msg(1, 0, 1064, "42000", "Routing query to backend failed. " "See the error log for further details."); if (errbuf) { rses->client_dcb->func.write(rses->client_dcb, errbuf); result = true; } MXS_FREE(query_str); MXS_FREE(qtype_str); } else if (route_session_write(rses, gwbuf_clone(querybuf), packet_type, qtype)) { result = true; atomic_add_uint64(&inst->stats().n_all, 1); } return result; } /* * Probably MySQL specific because of modutil function */ /** * @brief Write an error message to the log for closed session * * This happens if a request is received for a session that is already * closing down. * * @param querybuf Query buffer containing packet */ void closed_session_reply(GWBUF *querybuf) { uint8_t* data = GWBUF_DATA(querybuf); if (GWBUF_LENGTH(querybuf) >= 5 && !MYSQL_IS_COM_QUIT(data)) { /* Note that most modutil functions are MySQL specific */ char *query_str = modutil_get_query(querybuf); MXS_ERROR("Can't route %s:\"%s\" to backend server. Router is closed.", STRPACKETTYPE(data[4]), query_str ? query_str : "(empty)"); MXS_FREE(query_str); } } /* * Uses MySQL specific mechanisms */ /** * @brief Check the reply from a backend server to a session command * * If the reply is an error, a message is logged. * * @param buffer Query buffer containing reply data * @param backend Router session data for a backend server */ void check_session_command_reply(GWBUF *buffer, SRWBackend backend) { if (MYSQL_IS_ERROR_PACKET(((uint8_t *)GWBUF_DATA(buffer)))) { size_t replylen = MYSQL_GET_PAYLOAD_LEN(GWBUF_DATA(buffer)); char replybuf[replylen]; gwbuf_copy_data(buffer, 0, gwbuf_length(buffer), (uint8_t*)replybuf); std::string err; std::string msg; err.append(replybuf + 8, 5); msg.append(replybuf + 13, replylen - 4 - 5); MXS_ERROR("Failed to execute session command in %s. Error was: %s %s", backend->uri(), err.c_str(), msg.c_str()); } } /** * @brief Send an error message to the client telling that the server is in read only mode * * @param dcb Client DCB * * @return True if sending the message was successful, false if an error occurred */ bool send_readonly_error(DCB *dcb) { bool succp = false; const char* errmsg = "The MariaDB server is running with the --read-only" " option so it cannot execute this statement"; GWBUF* err = modutil_create_mysql_err_msg(1, 0, ER_OPTION_PREVENTS_STATEMENT, "HY000", errmsg); if (err) { succp = dcb->func.write(dcb, err); } else { MXS_ERROR("Memory allocation failed when creating client error message."); } return succp; }