
The two operations return different types of results and need to be treated differently in order for them to be handled correctly in 2.2. This fixes the unexpected internal state errors that happened in all 2.2 versions due to a wrong assumption made by readwritesplit. This fix is not necessary for newer versions as the LOAD DATA LOCAL INFILE processing is done with a simpler, and more robust, method.
1691 lines
49 KiB
C++
1691 lines
49 KiB
C++
/*
|
|
* 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 "schemarouter.hh"
|
|
#include "schemaroutersession.hh"
|
|
#include "schemarouterinstance.hh"
|
|
|
|
#include <inttypes.h>
|
|
|
|
#include <maxscale/alloc.h>
|
|
#include <maxscale/modutil.h>
|
|
#include <maxscale/poll.h>
|
|
#include <maxscale/query_classifier.h>
|
|
|
|
namespace schemarouter
|
|
{
|
|
|
|
bool connect_backend_servers(SSRBackendList& backends, MXS_SESSION* session);
|
|
|
|
enum route_target get_shard_route_target(uint32_t qtype);
|
|
bool change_current_db(std::string& dest, Shard& shard, GWBUF* buf);
|
|
bool extract_database(GWBUF* buf, char* str);
|
|
bool detect_show_shards(GWBUF* query);
|
|
void write_error_to_client(DCB* dcb, int errnum, const char* mysqlstate, const char* errmsg);
|
|
|
|
SchemaRouterSession::SchemaRouterSession(MXS_SESSION* session, SchemaRouter* router,
|
|
SSRBackendList& backends):
|
|
mxs::RouterSession(session),
|
|
m_closed(false),
|
|
m_client(session->client_dcb),
|
|
m_mysql_session((MYSQL_session*)session->client_dcb->data),
|
|
m_backends(backends),
|
|
m_config(&router->m_config),
|
|
m_router(router),
|
|
m_shard(m_router->m_shard_manager.get_shard(m_client->user, m_config->refresh_min_interval)),
|
|
m_state(0),
|
|
m_sent_sescmd(0),
|
|
m_replied_sescmd(0),
|
|
m_load_target(NULL)
|
|
{
|
|
char db[MYSQL_DATABASE_MAXLEN + 1] = "";
|
|
MySQLProtocol* protocol = (MySQLProtocol*)session->client_dcb->protocol;
|
|
bool using_db = false;
|
|
bool have_db = false;
|
|
const char* current_db = mxs_mysql_get_current_db(session);
|
|
|
|
/* To enable connecting directly to a sharded database we first need
|
|
* to disable it for the client DCB's protocol so that we can connect to them*/
|
|
if (protocol->client_capabilities & GW_MYSQL_CAPABILITIES_CONNECT_WITH_DB &&
|
|
(have_db = *current_db))
|
|
{
|
|
protocol->client_capabilities &= ~GW_MYSQL_CAPABILITIES_CONNECT_WITH_DB;
|
|
strcpy(db, current_db);
|
|
mxs_mysql_set_current_db(session, "");
|
|
using_db = true;
|
|
MXS_INFO("Client logging in directly to a database '%s', "
|
|
"postponing until databases have been mapped.", db);
|
|
}
|
|
|
|
if (using_db)
|
|
{
|
|
m_state |= INIT_USE_DB;
|
|
}
|
|
|
|
if (db[0])
|
|
{
|
|
/* Store the database the client is connecting to */
|
|
m_connect_db = db;
|
|
}
|
|
|
|
atomic_add(&m_router->m_stats.sessions, 1);
|
|
}
|
|
|
|
SchemaRouterSession::~SchemaRouterSession()
|
|
{
|
|
}
|
|
|
|
void SchemaRouterSession::close()
|
|
{
|
|
ss_dassert(!m_closed);
|
|
|
|
/**
|
|
* Lock router client session for secure read and update.
|
|
*/
|
|
if (!m_closed)
|
|
{
|
|
m_closed = true;
|
|
|
|
for (SSRBackendList::iterator it = m_backends.begin(); it != m_backends.end(); it++)
|
|
{
|
|
SSRBackend& bref = *it;
|
|
/** The backends are closed here to trigger the shutdown of
|
|
* the connected DCBs */
|
|
if (bref->in_use())
|
|
{
|
|
bref->close();
|
|
}
|
|
}
|
|
|
|
spinlock_acquire(&m_router->m_lock);
|
|
if (m_router->m_stats.longest_sescmd < m_stats.longest_sescmd)
|
|
{
|
|
m_router->m_stats.longest_sescmd = m_stats.longest_sescmd;
|
|
}
|
|
double ses_time = difftime(time(NULL), m_client->session->stats.connect);
|
|
if (m_router->m_stats.ses_longest < ses_time)
|
|
{
|
|
m_router->m_stats.ses_longest = ses_time;
|
|
}
|
|
if (m_router->m_stats.ses_shortest > ses_time && m_router->m_stats.ses_shortest > 0)
|
|
{
|
|
m_router->m_stats.ses_shortest = ses_time;
|
|
}
|
|
|
|
m_router->m_stats.ses_average =
|
|
(ses_time + ((m_router->m_stats.sessions - 1) * m_router->m_stats.ses_average)) /
|
|
(m_router->m_stats.sessions);
|
|
|
|
spinlock_release(&m_router->m_lock);
|
|
}
|
|
}
|
|
|
|
static void inspect_query(GWBUF* pPacket, uint32_t* type, qc_query_op_t* op, uint8_t* command)
|
|
{
|
|
uint8_t* data = GWBUF_DATA(pPacket);
|
|
*command = data[4];
|
|
|
|
switch (*command)
|
|
{
|
|
case MXS_COM_QUIT: /*< 1 QUIT will close all sessions */
|
|
case MXS_COM_INIT_DB: /*< 2 DDL must go to the master */
|
|
case MXS_COM_REFRESH: /*< 7 - I guess this is session but not sure */
|
|
case MXS_COM_DEBUG: /*< 0d all servers dump debug info to stdout */
|
|
case MXS_COM_PING: /*< 0e all servers are pinged */
|
|
case MXS_COM_CHANGE_USER: /*< 11 all servers change it accordingly */
|
|
case MXS_COM_STMT_CLOSE: /*< free prepared statement */
|
|
case MXS_COM_STMT_SEND_LONG_DATA: /*< send data to column */
|
|
case MXS_COM_STMT_RESET: /*< resets the data of a prepared statement */
|
|
*type = QUERY_TYPE_SESSION_WRITE;
|
|
break;
|
|
|
|
case MXS_COM_CREATE_DB: /**< 5 DDL must go to the master */
|
|
case MXS_COM_DROP_DB: /**< 6 DDL must go to the master */
|
|
*type = QUERY_TYPE_WRITE;
|
|
break;
|
|
|
|
case MXS_COM_QUERY:
|
|
*type = qc_get_type_mask(pPacket);
|
|
*op = qc_get_operation(pPacket);
|
|
break;
|
|
|
|
case MXS_COM_STMT_PREPARE:
|
|
*type = qc_get_type_mask(pPacket);
|
|
*type |= QUERY_TYPE_PREPARE_STMT;
|
|
break;
|
|
|
|
case MXS_COM_STMT_EXECUTE:
|
|
/** Parsing is not needed for this type of packet */
|
|
*type = QUERY_TYPE_EXEC_STMT;
|
|
break;
|
|
|
|
case MXS_COM_SHUTDOWN: /**< 8 where should shutdown be routed ? */
|
|
case MXS_COM_STATISTICS: /**< 9 ? */
|
|
case MXS_COM_PROCESS_INFO: /**< 0a ? */
|
|
case MXS_COM_CONNECT: /**< 0b ? */
|
|
case MXS_COM_PROCESS_KILL: /**< 0c ? */
|
|
case MXS_COM_TIME: /**< 0f should this be run in gateway ? */
|
|
case MXS_COM_DELAYED_INSERT: /**< 10 ? */
|
|
case MXS_COM_DAEMON: /**< 1d ? */
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (MXS_LOG_PRIORITY_IS_ENABLED(LOG_INFO))
|
|
{
|
|
char *sql;
|
|
int sql_len;
|
|
char* qtypestr = qc_typemask_to_string(*type);
|
|
int rc = modutil_extract_SQL(pPacket, &sql, &sql_len);
|
|
|
|
MXS_INFO("> Command: %s, stmt: %.*s %s%s",
|
|
STRPACKETTYPE(*command), rc ? sql_len : 0, rc ? sql : "",
|
|
(pPacket->hint == NULL ? "" : ", Hint:"),
|
|
(pPacket->hint == NULL ? "" : STRHINTTYPE(pPacket->hint->type)));
|
|
|
|
MXS_FREE(qtypestr);
|
|
}
|
|
}
|
|
|
|
SERVER* SchemaRouterSession::resolve_query_target(GWBUF* pPacket,
|
|
uint32_t type,
|
|
uint8_t command,
|
|
enum route_target& route_target)
|
|
{
|
|
SERVER* target = NULL;
|
|
|
|
if (route_target != TARGET_NAMED_SERVER)
|
|
{
|
|
/** We either don't know or don't care where this query should go */
|
|
target = get_shard_target(pPacket, type);
|
|
|
|
if (target && SERVER_IS_RUNNING(target))
|
|
{
|
|
route_target = TARGET_NAMED_SERVER;
|
|
}
|
|
}
|
|
|
|
if (TARGET_IS_UNDEFINED(route_target))
|
|
{
|
|
/** We don't know where to send this. Route it to either the server with
|
|
* the current default database or to the first available server. */
|
|
target = get_shard_target(pPacket, type);
|
|
|
|
if ((target == NULL && command != MXS_COM_INIT_DB && m_current_db.length() == 0) ||
|
|
command == MXS_COM_FIELD_LIST ||
|
|
m_current_db.length() == 0)
|
|
{
|
|
/** No current database and no databases in query or the database is
|
|
* ignored, route to first available backend. */
|
|
route_target = TARGET_ANY;
|
|
}
|
|
}
|
|
|
|
if (TARGET_IS_ANY(route_target))
|
|
{
|
|
for (SSRBackendList::iterator it = m_backends.begin(); it != m_backends.end(); it++)
|
|
{
|
|
SERVER *server = (*it)->backend()->server;
|
|
if (SERVER_IS_RUNNING(server))
|
|
{
|
|
route_target = TARGET_NAMED_SERVER;
|
|
target = server;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (TARGET_IS_ANY(route_target))
|
|
{
|
|
/**No valid backends alive*/
|
|
MXS_ERROR("Failed to route query, no backends are available.");
|
|
}
|
|
}
|
|
|
|
return target;
|
|
}
|
|
|
|
static bool is_empty_packet(GWBUF* pPacket)
|
|
{
|
|
bool rval = false;
|
|
uint8_t len[3];
|
|
|
|
if (gwbuf_length(pPacket) == 4 &&
|
|
gwbuf_copy_data(pPacket, 0, 3, len) == 3 &&
|
|
gw_mysql_get_byte3(len) == 0)
|
|
{
|
|
rval = true;
|
|
}
|
|
|
|
return rval;
|
|
}
|
|
|
|
int32_t SchemaRouterSession::routeQuery(GWBUF* pPacket)
|
|
{
|
|
if (m_closed)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
if (m_shard.empty())
|
|
{
|
|
/* Generate database list */
|
|
query_databases();
|
|
}
|
|
|
|
int ret = 0;
|
|
|
|
/**
|
|
* If the databases are still being mapped or if the client connected
|
|
* with a default database but no database mapping was performed we need
|
|
* to store the query. Once the databases have been mapped and/or the
|
|
* default database is taken into use we can send the query forward.
|
|
*/
|
|
if (m_state & (INIT_MAPPING | INIT_USE_DB))
|
|
{
|
|
m_queue.push_back(pPacket);
|
|
ret = 1;
|
|
|
|
if (m_state == (INIT_READY | INIT_USE_DB))
|
|
{
|
|
/**
|
|
* This state is possible if a client connects with a default database
|
|
* and the shard map was found from the router cache
|
|
*/
|
|
if (!handle_default_db())
|
|
{
|
|
ret = 0;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
uint8_t command = 0;
|
|
SERVER* target = NULL;
|
|
uint32_t type = QUERY_TYPE_UNKNOWN;
|
|
qc_query_op_t op = QUERY_OP_UNDEFINED;
|
|
enum route_target route_target = TARGET_UNDEFINED;
|
|
|
|
if (m_load_target)
|
|
{
|
|
/** A load data local infile is active */
|
|
target = m_load_target;
|
|
route_target = TARGET_NAMED_SERVER;
|
|
|
|
if (is_empty_packet(pPacket))
|
|
{
|
|
m_load_target = NULL;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
inspect_query(pPacket, &type, &op, &command);
|
|
|
|
/** Create the response to the SHOW DATABASES from the mapped databases */
|
|
if (qc_query_is_type(type, QUERY_TYPE_SHOW_DATABASES))
|
|
{
|
|
if (send_databases())
|
|
{
|
|
ret = 1;
|
|
}
|
|
|
|
gwbuf_free(pPacket);
|
|
return ret;
|
|
}
|
|
else if (detect_show_shards(pPacket))
|
|
{
|
|
if (send_shards())
|
|
{
|
|
ret = 1;
|
|
}
|
|
gwbuf_free(pPacket);
|
|
return ret;
|
|
}
|
|
|
|
/** The default database changes must be routed to a specific server */
|
|
if (command == MXS_COM_INIT_DB || op == QUERY_OP_CHANGE_DB)
|
|
{
|
|
if (!change_current_db(m_current_db, m_shard, pPacket))
|
|
{
|
|
char db[MYSQL_DATABASE_MAXLEN + 1];
|
|
extract_database(pPacket, db);
|
|
gwbuf_free(pPacket);
|
|
|
|
char errbuf[128 + MYSQL_DATABASE_MAXLEN];
|
|
snprintf(errbuf, sizeof (errbuf), "Unknown database: %s", db);
|
|
|
|
if (m_config->debug)
|
|
{
|
|
sprintf(errbuf + strlen(errbuf),
|
|
" ([%" PRIu64 "]: DB change failed)",
|
|
m_client->session->ses_id);
|
|
}
|
|
|
|
write_error_to_client(m_client,
|
|
SCHEMA_ERR_DBNOTFOUND,
|
|
SCHEMA_ERRSTR_DBNOTFOUND,
|
|
errbuf);
|
|
return 1;
|
|
}
|
|
|
|
route_target = TARGET_UNDEFINED;
|
|
target = m_shard.get_location(m_current_db);
|
|
|
|
if (target)
|
|
{
|
|
MXS_INFO("INIT_DB for database '%s' on server '%s'",
|
|
m_current_db.c_str(), target->unique_name);
|
|
route_target = TARGET_NAMED_SERVER;
|
|
}
|
|
else
|
|
{
|
|
MXS_INFO("INIT_DB with unknown database");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
route_target = get_shard_route_target(type);
|
|
}
|
|
|
|
/**
|
|
* Find a suitable server that matches the requirements of @c route_target
|
|
*/
|
|
|
|
if (TARGET_IS_ALL(route_target))
|
|
{
|
|
/** Session commands, route to all servers */
|
|
if (route_session_write(pPacket, command))
|
|
{
|
|
atomic_add(&m_router->m_stats.n_sescmd, 1);
|
|
atomic_add(&m_router->m_stats.n_queries, 1);
|
|
ret = 1;
|
|
}
|
|
}
|
|
else if (target == NULL)
|
|
{
|
|
target = resolve_query_target(pPacket, type, command, route_target);
|
|
}
|
|
}
|
|
|
|
DCB* target_dcb = NULL;
|
|
|
|
if (TARGET_IS_NAMED_SERVER(route_target) && target &&
|
|
get_shard_dcb(&target_dcb, target->unique_name))
|
|
{
|
|
/** We know where to route this query */
|
|
SSRBackend bref = get_bref_from_dcb(target_dcb);
|
|
|
|
if (op == QUERY_OP_LOAD_LOCAL)
|
|
{
|
|
m_load_target = bref->backend()->server;
|
|
}
|
|
|
|
MXS_INFO("Route query to \t%s:%d <", bref->backend()->server->name, bref->backend()->server->port);
|
|
|
|
if (bref->session_command_count())
|
|
{
|
|
/** Store current statement if execution of the previous
|
|
* session command hasn't been completed. */
|
|
bref->store_command(pPacket);
|
|
pPacket = NULL;
|
|
ret = 1;
|
|
}
|
|
else if (bref->write(pPacket))
|
|
{
|
|
/** Add one query response waiter to backend reference */
|
|
atomic_add(&m_router->m_stats.n_queries, 1);
|
|
atomic_add_uint64(&bref->server()->stats.packets, 1);
|
|
ret = 1;
|
|
}
|
|
else
|
|
{
|
|
MXS_ERROR("Routing query failed.");
|
|
gwbuf_free(pPacket);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
void SchemaRouterSession::handle_mapping_reply(SSRBackend& bref, GWBUF** pPacket)
|
|
{
|
|
int rc = inspect_mapping_states(bref, pPacket);
|
|
|
|
if (rc == 1)
|
|
{
|
|
synchronize_shards();
|
|
m_state &= ~INIT_MAPPING;
|
|
|
|
/* Check if the session is reconnecting with a database name
|
|
* that is not in the hashtable. If the database is not found
|
|
* then close the session. */
|
|
|
|
if (m_state & INIT_USE_DB)
|
|
{
|
|
if (!handle_default_db())
|
|
{
|
|
rc = -1;
|
|
}
|
|
}
|
|
else if (m_queue.size() && rc != -1)
|
|
{
|
|
ss_dassert(m_state == INIT_READY || m_state == INIT_USE_DB);
|
|
MXS_INFO("Routing stored query");
|
|
route_queued_query();
|
|
}
|
|
}
|
|
|
|
if (rc == -1)
|
|
{
|
|
poll_fake_hangup_event(m_client);
|
|
}
|
|
}
|
|
|
|
void SchemaRouterSession::process_sescmd_response(SSRBackend& bref, GWBUF** ppPacket)
|
|
{
|
|
if (bref->session_command_count())
|
|
{
|
|
/** We are executing a session command */
|
|
if (GWBUF_IS_TYPE_SESCMD_RESPONSE((*ppPacket)))
|
|
{
|
|
uint64_t id = bref->complete_session_command();
|
|
|
|
if (m_replied_sescmd < m_sent_sescmd && id == m_replied_sescmd + 1)
|
|
{
|
|
/** First reply to this session command, route it to the client */
|
|
++m_replied_sescmd;
|
|
}
|
|
else
|
|
{
|
|
/** The reply to this session command has already been sent to
|
|
* the client, discard it */
|
|
gwbuf_free(*ppPacket);
|
|
*ppPacket = NULL;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void SchemaRouterSession::clientReply(GWBUF* pPacket, DCB* pDcb)
|
|
{
|
|
SSRBackend bref = get_bref_from_dcb(pDcb);
|
|
|
|
if (m_closed || bref.get() == NULL) // The bref should always be valid
|
|
{
|
|
gwbuf_free(pPacket);
|
|
return;
|
|
}
|
|
|
|
MXS_DEBUG("Reply from [%s] session [%p]"
|
|
" mapping [%s] queries queued [%s]",
|
|
bref->backend()->server->unique_name,
|
|
m_client->session,
|
|
m_state & INIT_MAPPING ? "true" : "false",
|
|
m_queue.size() == 0 ? "none" :
|
|
m_queue.size() > 0 ? "multiple" : "one");
|
|
|
|
if (m_state & INIT_MAPPING)
|
|
{
|
|
handle_mapping_reply(bref, &pPacket);
|
|
}
|
|
else if (m_state & INIT_USE_DB)
|
|
{
|
|
MXS_DEBUG("Reply to USE '%s' received for session %p",
|
|
m_connect_db.c_str(), m_client->session);
|
|
m_state &= ~INIT_USE_DB;
|
|
m_current_db = m_connect_db;
|
|
ss_dassert(m_state == INIT_READY);
|
|
|
|
if (m_queue.size())
|
|
{
|
|
route_queued_query();
|
|
}
|
|
}
|
|
|
|
else if (m_queue.size())
|
|
{
|
|
ss_dassert(m_state == INIT_READY);
|
|
route_queued_query();
|
|
}
|
|
else
|
|
{
|
|
process_sescmd_response(bref, &pPacket);
|
|
|
|
ss_dassert(bref->is_waiting_result());
|
|
/** Set response status as replied */
|
|
bref->ack_write();
|
|
|
|
if (pPacket)
|
|
{
|
|
MXS_SESSION_ROUTE_REPLY(pDcb->session, pPacket);
|
|
pPacket = NULL;
|
|
}
|
|
|
|
if (bref->execute_session_command())
|
|
{
|
|
MXS_INFO("Backend %s:%d processed reply and starts to execute active cursor.",
|
|
bref->backend()->server->name, bref->backend()->server->port);
|
|
}
|
|
else if (bref->write_stored_command())
|
|
{
|
|
atomic_add(&m_router->m_stats.n_queries, 1);
|
|
}
|
|
}
|
|
|
|
gwbuf_free(pPacket);
|
|
}
|
|
|
|
void SchemaRouterSession::handleError(GWBUF* pMessage,
|
|
DCB* pProblem,
|
|
mxs_error_action_t action,
|
|
bool* pSuccess)
|
|
{
|
|
ss_dassert(pProblem->dcb_role == DCB_ROLE_BACKEND_HANDLER);
|
|
CHK_DCB(pProblem);
|
|
SSRBackend bref = get_bref_from_dcb(pProblem);
|
|
|
|
if (bref.get() == NULL) // Should never happen
|
|
{
|
|
return;
|
|
}
|
|
|
|
switch (action)
|
|
{
|
|
case ERRACT_NEW_CONNECTION:
|
|
if (bref->is_waiting_result())
|
|
{
|
|
/** If the client is waiting for a reply, send an error. */
|
|
m_client->func.write(m_client, gwbuf_clone(pMessage));
|
|
}
|
|
|
|
*pSuccess = have_servers();
|
|
break;
|
|
|
|
case ERRACT_REPLY_CLIENT:
|
|
if (m_client->session->state == SESSION_STATE_ROUTER_READY)
|
|
{
|
|
m_client->func.write(m_client, gwbuf_clone(pMessage));
|
|
}
|
|
|
|
*pSuccess = false; /*< no new backend servers were made available */
|
|
break;
|
|
|
|
default:
|
|
*pSuccess = false;
|
|
break;
|
|
}
|
|
|
|
bref->close();
|
|
}
|
|
|
|
/**
|
|
* Private functions
|
|
*/
|
|
|
|
|
|
/**
|
|
* Synchronize the router client session shard map with the global shard map for
|
|
* this user.
|
|
*
|
|
* If the router doesn't have a shard map for this user then the current shard map
|
|
* of the client session is added to the m_router-> If the shard map in the router is
|
|
* out of date, its contents are replaced with the contents of the current client
|
|
* session. If the router has a usable shard map, the current shard map of the client
|
|
* is discarded and the router's shard map is used.
|
|
* @param client Router session
|
|
*/
|
|
void SchemaRouterSession::synchronize_shards()
|
|
{
|
|
m_router->m_stats.shmap_cache_miss++;
|
|
m_router->m_shard_manager.update_shard(m_shard, m_client->user);
|
|
}
|
|
|
|
/**
|
|
* Extract the database name from a COM_INIT_DB or literal USE ... query.
|
|
* @param buf Buffer with the database change query
|
|
* @param str Pointer where the database name is copied
|
|
* @return True for success, false for failure
|
|
*/
|
|
bool extract_database(GWBUF* buf, char* str)
|
|
{
|
|
uint8_t* packet;
|
|
char *saved, *tok, *query = NULL;
|
|
bool succp = true;
|
|
unsigned int plen;
|
|
|
|
packet = GWBUF_DATA(buf);
|
|
plen = gw_mysql_get_byte3(packet) - 1;
|
|
|
|
/** Copy database name from MySQL packet to session */
|
|
if (mxs_mysql_get_command(buf) == MXS_COM_QUERY &&
|
|
qc_get_operation(buf) == QUERY_OP_CHANGE_DB)
|
|
{
|
|
const char *delim = "` \n\t;";
|
|
|
|
query = modutil_get_SQL(buf);
|
|
tok = strtok_r(query, delim, &saved);
|
|
|
|
if (tok == NULL || strcasecmp(tok, "use") != 0)
|
|
{
|
|
MXS_ERROR("extract_database: Malformed chage database packet.");
|
|
succp = false;
|
|
goto retblock;
|
|
}
|
|
|
|
tok = strtok_r(NULL, delim, &saved);
|
|
|
|
if (tok == NULL)
|
|
{
|
|
MXS_ERROR("extract_database: Malformed change database packet.");
|
|
succp = false;
|
|
goto retblock;
|
|
}
|
|
|
|
strncpy(str, tok, MYSQL_DATABASE_MAXLEN);
|
|
}
|
|
else
|
|
{
|
|
memcpy(str, packet + 5, plen);
|
|
memset(str + plen, 0, 1);
|
|
}
|
|
retblock:
|
|
MXS_FREE(query);
|
|
return succp;
|
|
}
|
|
|
|
/**
|
|
* Execute in backends used by current router session.
|
|
* Save session variable commands to router session property
|
|
* struct. Thus, they can be replayed in backends which are
|
|
* started and joined later.
|
|
*
|
|
* Suppress redundant OK packets sent by backends.
|
|
*
|
|
* The first OK packet is replied to the client.
|
|
* Return true if succeed, false is returned if router session was closed or
|
|
* if execute_sescmd_in_backend failed.
|
|
*/
|
|
bool SchemaRouterSession::route_session_write(GWBUF* querybuf, uint8_t command)
|
|
{
|
|
bool succp = false;
|
|
|
|
MXS_INFO("Session write, routing to all servers.");
|
|
atomic_add(&m_stats.longest_sescmd, 1);
|
|
|
|
/** Increment the session command count */
|
|
++m_sent_sescmd;
|
|
|
|
for (SSRBackendList::iterator it = m_backends.begin(); it != m_backends.end(); it++)
|
|
{
|
|
if ((*it)->in_use())
|
|
{
|
|
GWBUF *buffer = gwbuf_clone(querybuf);
|
|
|
|
(*it)->append_session_command(buffer, m_sent_sescmd);
|
|
|
|
if (MXS_LOG_PRIORITY_IS_ENABLED(LOG_INFO))
|
|
{
|
|
MXS_INFO("Route query to %s\t%s:%d",
|
|
SERVER_IS_MASTER((*it)->backend()->server) ? "master" : "slave",
|
|
(*it)->backend()->server->name,
|
|
(*it)->backend()->server->port);
|
|
}
|
|
|
|
if ((*it)->session_command_count() == 1)
|
|
{
|
|
if ((*it)->execute_session_command())
|
|
{
|
|
succp = true;
|
|
atomic_add_uint64(&(*it)->server()->stats.packets, 1);
|
|
}
|
|
else
|
|
{
|
|
MXS_ERROR("Failed to execute session "
|
|
"command in %s:%d",
|
|
(*it)->backend()->server->name,
|
|
(*it)->backend()->server->port);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ss_dassert((*it)->session_command_count() > 1);
|
|
/** The server is already executing a session command */
|
|
MXS_INFO("Backend %s:%d already executing sescmd.",
|
|
(*it)->backend()->server->name,
|
|
(*it)->backend()->server->port);
|
|
succp = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
gwbuf_free(querybuf);
|
|
return succp;
|
|
}
|
|
|
|
/**
|
|
* Check if a router session has servers in use
|
|
* @param rses Router client session
|
|
* @return True if session has a single backend server in use that is running.
|
|
* False if no backends are in use or running.
|
|
*/
|
|
bool SchemaRouterSession::have_servers()
|
|
{
|
|
for (SSRBackendList::iterator it = m_backends.begin(); it != m_backends.end(); it++)
|
|
{
|
|
if ((*it)->in_use() && !(*it)->is_closed())
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Finds out if there is a backend reference pointing at the DCB given as
|
|
* parameter.
|
|
* @param rses router client session
|
|
* @param dcb DCB
|
|
*
|
|
* @return backend reference pointer if succeed or NULL
|
|
*/
|
|
SSRBackend SchemaRouterSession::get_bref_from_dcb(DCB* dcb)
|
|
{
|
|
CHK_DCB(dcb);
|
|
|
|
for (SSRBackendList::iterator it = m_backends.begin(); it != m_backends.end(); it++)
|
|
{
|
|
if ((*it)->dcb() == dcb)
|
|
{
|
|
return *it;
|
|
}
|
|
}
|
|
|
|
// This should not happen
|
|
ss_dassert(false);
|
|
return SSRBackend(reinterpret_cast<SRBackend*>(NULL));
|
|
}
|
|
|
|
/**
|
|
* Detect if a query contains a SHOW SHARDS query.
|
|
* @param query Query to inspect
|
|
* @return true if the query is a SHOW SHARDS query otherwise false
|
|
*/
|
|
bool detect_show_shards(GWBUF* query)
|
|
{
|
|
bool rval = false;
|
|
char *querystr, *tok, *sptr;
|
|
|
|
if (query == NULL)
|
|
{
|
|
MXS_ERROR("NULL value passed at %s:%d", __FILE__, __LINE__);
|
|
return false;
|
|
}
|
|
|
|
if (!modutil_is_SQL(query) && !modutil_is_SQL_prepare(query))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ((querystr = modutil_get_SQL(query)) == NULL)
|
|
{
|
|
MXS_ERROR("Failure to parse SQL at %s:%d", __FILE__, __LINE__);
|
|
return false;
|
|
}
|
|
|
|
tok = strtok_r(querystr, " ", &sptr);
|
|
if (tok && strcasecmp(tok, "show") == 0)
|
|
{
|
|
tok = strtok_r(NULL, " ", &sptr);
|
|
if (tok && strcasecmp(tok, "shards") == 0)
|
|
{
|
|
rval = true;
|
|
}
|
|
}
|
|
|
|
MXS_FREE(querystr);
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* Callback for the shard list result set creation
|
|
*/
|
|
RESULT_ROW* shard_list_cb(struct resultset* rset, void* data)
|
|
{
|
|
ServerMap* pContent = (ServerMap*)data;
|
|
RESULT_ROW* rval = resultset_make_row(rset);
|
|
|
|
if (rval)
|
|
{
|
|
resultset_row_set(rval, 0, pContent->begin()->first.c_str());
|
|
resultset_row_set(rval, 1, pContent->begin()->second->unique_name);
|
|
pContent->erase(pContent->begin());
|
|
}
|
|
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* Send a result set of all shards and their locations to the client.
|
|
* @param rses Router client session
|
|
* @return 0 on success, -1 on error
|
|
*/
|
|
bool SchemaRouterSession::send_shards()
|
|
{
|
|
bool rval = false;
|
|
|
|
ServerMap pContent;
|
|
m_shard.get_content(pContent);
|
|
RESULTSET* rset = resultset_create(shard_list_cb, &pContent);
|
|
|
|
if (rset)
|
|
{
|
|
resultset_add_column(rset, "Database", MYSQL_DATABASE_MAXLEN, COL_TYPE_VARCHAR);
|
|
resultset_add_column(rset, "Server", MYSQL_DATABASE_MAXLEN, COL_TYPE_VARCHAR);
|
|
resultset_stream_mysql(rset, m_client);
|
|
resultset_free(rset);
|
|
rval = true;
|
|
}
|
|
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param dcb
|
|
* @param errnum
|
|
* @param mysqlstate
|
|
* @param errmsg
|
|
*/
|
|
void write_error_to_client(DCB* dcb, int errnum, const char* mysqlstate, const char* errmsg)
|
|
{
|
|
GWBUF* errbuff = modutil_create_mysql_err_msg(1, 0, errnum, mysqlstate, errmsg);
|
|
if (errbuff)
|
|
{
|
|
if (dcb->func.write(dcb, errbuff) != 1)
|
|
{
|
|
MXS_ERROR("Failed to write error packet to client.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MXS_ERROR("Memory allocation failed when creating error packet.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param router_cli_ses
|
|
* @return
|
|
*/
|
|
bool SchemaRouterSession::handle_default_db()
|
|
{
|
|
bool rval = false;
|
|
SERVER* target = m_shard.get_location(m_connect_db);
|
|
|
|
if (target)
|
|
{
|
|
/* Send a COM_INIT_DB packet to the server with the right database
|
|
* and set it as the client's active database */
|
|
|
|
unsigned int qlen = m_connect_db.length();
|
|
GWBUF* buffer = gwbuf_alloc(qlen + 5);
|
|
|
|
if (buffer)
|
|
{
|
|
uint8_t *data = GWBUF_DATA(buffer);
|
|
gw_mysql_set_byte3(data, qlen + 1);
|
|
data[3] = 0x0;
|
|
data[4] = 0x2;
|
|
memcpy(data + 5, m_connect_db.c_str(), qlen);
|
|
SSRBackend backend;
|
|
DCB* dcb = NULL;
|
|
|
|
if (get_shard_dcb(&dcb, target->unique_name) &&
|
|
(backend = get_bref_from_dcb(dcb)))
|
|
{
|
|
backend->write(buffer);
|
|
MXS_DEBUG("USE '%s' sent to %s for session %p",
|
|
m_connect_db.c_str(),
|
|
target->unique_name,
|
|
m_client->session);
|
|
rval = true;
|
|
}
|
|
else
|
|
{
|
|
MXS_INFO("Couldn't find target DCB for '%s'.", target->unique_name);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MXS_ERROR("Buffer allocation failed.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/** Unknown database, hang up on the client*/
|
|
MXS_INFO("Connecting to a non-existent database '%s'", m_connect_db.c_str());
|
|
char errmsg[128 + MYSQL_DATABASE_MAXLEN + 1];
|
|
sprintf(errmsg, "Unknown database '%s'", m_connect_db.c_str());
|
|
if (m_config->debug)
|
|
{
|
|
sprintf(errmsg + strlen(errmsg), " ([%" PRIu64 "]: DB not found on connect)",
|
|
m_client->session->ses_id);
|
|
}
|
|
write_error_to_client(m_client,
|
|
SCHEMA_ERR_DBNOTFOUND,
|
|
SCHEMA_ERRSTR_DBNOTFOUND,
|
|
errmsg);
|
|
}
|
|
|
|
return rval;
|
|
}
|
|
|
|
void SchemaRouterSession::route_queued_query()
|
|
{
|
|
GWBUF* tmp = m_queue.front().release();
|
|
m_queue.pop_front();
|
|
|
|
#ifdef SS_DEBUG
|
|
char* querystr = modutil_get_SQL(tmp);
|
|
MXS_DEBUG("Sending queued buffer for session %p: %s",
|
|
m_client->session,
|
|
querystr);
|
|
MXS_FREE(querystr);
|
|
#endif
|
|
|
|
poll_add_epollin_event_to_dcb(m_client, tmp);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param router_cli_ses Router client session
|
|
* @return 1 if mapping is done, 0 if it is still ongoing and -1 on error
|
|
*/
|
|
int SchemaRouterSession::inspect_mapping_states(SSRBackend& bref,
|
|
GWBUF** wbuf)
|
|
{
|
|
bool mapped = true;
|
|
GWBUF* writebuf = *wbuf;
|
|
|
|
for (SSRBackendList::iterator it = m_backends.begin(); it != m_backends.end(); it++)
|
|
{
|
|
if (bref->dcb() == (*it)->dcb() && !(*it)->is_mapped())
|
|
{
|
|
enum showdb_response rc = parse_mapping_response(*it, &writebuf);
|
|
|
|
if (rc == SHOWDB_FULL_RESPONSE)
|
|
{
|
|
(*it)->set_mapped(true);
|
|
(*it)->ack_write();
|
|
MXS_DEBUG("Received SHOW DATABASES reply from %s for session %p",
|
|
(*it)->backend()->server->unique_name,
|
|
m_client->session);
|
|
}
|
|
else
|
|
{
|
|
ss_dassert(rc != SHOWDB_PARTIAL_RESPONSE);
|
|
DCB* client_dcb = NULL;
|
|
|
|
if ((m_state & INIT_FAILED) == 0)
|
|
{
|
|
if (rc == SHOWDB_DUPLICATE_DATABASES)
|
|
{
|
|
MXS_ERROR("Duplicate databases found, closing session.");
|
|
}
|
|
else
|
|
{
|
|
MXS_ERROR("Fatal error when processing SHOW DATABASES response, closing session.");
|
|
}
|
|
client_dcb = m_client;
|
|
|
|
/** This is the first response to the database mapping which
|
|
* has duplicate database conflict. Set the initialization bitmask
|
|
* to INIT_FAILED */
|
|
m_state |= INIT_FAILED;
|
|
|
|
/** Send the client an error about duplicate databases
|
|
* if there is a queued query from the client. */
|
|
if (m_queue.size())
|
|
{
|
|
GWBUF* error = modutil_create_mysql_err_msg(1, 0,
|
|
SCHEMA_ERR_DUPLICATEDB,
|
|
SCHEMA_ERRSTR_DUPLICATEDB,
|
|
"Error: duplicate databases "
|
|
"found on two different shards.");
|
|
|
|
if (error)
|
|
{
|
|
client_dcb->func.write(client_dcb, error);
|
|
}
|
|
else
|
|
{
|
|
MXS_ERROR("Creating buffer for error message failed.");
|
|
}
|
|
}
|
|
}
|
|
*wbuf = writebuf;
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if ((*it)->in_use() && !(*it)->is_mapped())
|
|
{
|
|
mapped = false;
|
|
MXS_DEBUG("Still waiting for reply to SHOW DATABASES from %s for session %p",
|
|
(*it)->backend()->server->unique_name, m_client->session);
|
|
}
|
|
}
|
|
*wbuf = writebuf;
|
|
return mapped ? 1 : 0;
|
|
}
|
|
|
|
/**
|
|
* Create a fake error message from a DCB.
|
|
* @param fail_str Custom error message
|
|
* @param dcb DCB to use as the origin of the error
|
|
*/
|
|
void create_error_reply(char* fail_str, DCB* dcb)
|
|
{
|
|
MXS_INFO("change_current_db: failed to change database: %s", fail_str);
|
|
GWBUF* errbuf = modutil_create_mysql_err_msg(1, 0, 1049, "42000", fail_str);
|
|
|
|
if (errbuf == NULL)
|
|
{
|
|
MXS_ERROR("Creating buffer for error message failed.");
|
|
return;
|
|
}
|
|
/** Set flags that help router to identify session commands reply */
|
|
gwbuf_set_type(errbuf, GWBUF_TYPE_SESCMD_RESPONSE);
|
|
gwbuf_set_type(errbuf, GWBUF_TYPE_RESPONSE_END);
|
|
|
|
poll_add_epollin_event_to_dcb(dcb, errbuf);
|
|
}
|
|
|
|
/**
|
|
* Read new database name from COM_INIT_DB packet or a literal USE ... COM_QUERY
|
|
* packet, check that it exists in the hashtable and copy its name to MYSQL_session.
|
|
*
|
|
* @param dest Destination where the database name will be written
|
|
* @param dbhash Hashtable containing valid databases
|
|
* @param buf Buffer containing the database change query
|
|
*
|
|
* @return true if new database is set, false if non-existent database was tried
|
|
* to be set
|
|
*/
|
|
bool change_current_db(std::string& dest, Shard& shard, GWBUF* buf)
|
|
{
|
|
bool succp = false;
|
|
char db[MYSQL_DATABASE_MAXLEN + 1];
|
|
|
|
if (GWBUF_LENGTH(buf) <= MYSQL_DATABASE_MAXLEN - 5)
|
|
{
|
|
/** Copy database name from MySQL packet to session */
|
|
if (extract_database(buf, db))
|
|
{
|
|
MXS_INFO("change_current_db: INIT_DB with database '%s'", db);
|
|
/**
|
|
* Update the session's active database only if it's in the hashtable.
|
|
* If it isn't found, send a custom error packet to the client.
|
|
*/
|
|
|
|
SERVER* target = shard.get_location(db);
|
|
|
|
if (target)
|
|
{
|
|
dest = db;
|
|
MXS_INFO("change_current_db: database is on server: '%s'.", target->unique_name);
|
|
succp = true;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MXS_ERROR("change_current_db: failed to change database: Query buffer too large");
|
|
}
|
|
|
|
return succp;
|
|
}
|
|
|
|
/**
|
|
* Convert a length encoded string into a C string.
|
|
* @param data Pointer to the first byte of the string
|
|
* @return Pointer to the newly allocated string or NULL if the value is NULL or an error occurred
|
|
*/
|
|
char* get_lenenc_str(void* data)
|
|
{
|
|
unsigned char* ptr = (unsigned char*)data;
|
|
char* rval;
|
|
uintptr_t size;
|
|
long offset;
|
|
|
|
if (data == NULL)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
if (*ptr < 251)
|
|
{
|
|
size = (uintptr_t) * ptr;
|
|
offset = 1;
|
|
}
|
|
else
|
|
{
|
|
switch (*(ptr))
|
|
{
|
|
case 0xfb:
|
|
return NULL;
|
|
case 0xfc:
|
|
size = *(ptr + 1) + (*(ptr + 2) << 8);
|
|
offset = 2;
|
|
break;
|
|
case 0xfd:
|
|
size = *ptr + (*(ptr + 2) << 8) + (*(ptr + 3) << 16);
|
|
offset = 3;
|
|
break;
|
|
case 0xfe:
|
|
size = *ptr + ((*(ptr + 2) << 8)) + (*(ptr + 3) << 16) +
|
|
(*(ptr + 4) << 24) + ((uintptr_t) * (ptr + 5) << 32) +
|
|
((uintptr_t) * (ptr + 6) << 40) +
|
|
((uintptr_t) * (ptr + 7) << 48) + ((uintptr_t) * (ptr + 8) << 56);
|
|
offset = 8;
|
|
break;
|
|
default:
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
rval = (char*)MXS_MALLOC(sizeof(char) * (size + 1));
|
|
if (rval)
|
|
{
|
|
memcpy(rval, ptr + offset, size);
|
|
memset(rval + size, 0, 1);
|
|
|
|
}
|
|
return rval;
|
|
}
|
|
|
|
bool SchemaRouterSession::ignore_duplicate_database(const char* data)
|
|
{
|
|
bool rval = false;
|
|
|
|
if (m_config->ignored_dbs.find(data) != m_config->ignored_dbs.end())
|
|
{
|
|
rval = true;
|
|
}
|
|
else if (m_config->ignore_regex)
|
|
{
|
|
pcre2_match_data *match_data = pcre2_match_data_create_from_pattern(m_config->ignore_regex, NULL);
|
|
|
|
if (match_data == NULL)
|
|
{
|
|
throw std::bad_alloc();
|
|
}
|
|
|
|
if (pcre2_match(m_config->ignore_regex, (PCRE2_SPTR) data,
|
|
PCRE2_ZERO_TERMINATED, 0, 0, match_data, NULL) >= 0)
|
|
{
|
|
rval = true;
|
|
}
|
|
|
|
pcre2_match_data_free(match_data);
|
|
}
|
|
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* Parses a response set to a SHOW DATABASES query and inserts them into the
|
|
* router client session's database hashtable. The name of the database is used
|
|
* as the key and the unique name of the server is the value. The function
|
|
* currently supports only result sets that span a single SQL packet.
|
|
* @param rses Router client session
|
|
* @param target Target server where the database is
|
|
* @param buf GWBUF containing the result set
|
|
* @return 1 if a complete response was received, 0 if a partial response was received
|
|
* and -1 if a database was found on more than one server.
|
|
*/
|
|
enum showdb_response SchemaRouterSession::parse_mapping_response(SSRBackend& bref, GWBUF** buffer)
|
|
{
|
|
unsigned char* ptr;
|
|
SERVER* target = bref->backend()->server;
|
|
GWBUF* buf;
|
|
bool duplicate_found = false;
|
|
enum showdb_response rval = SHOWDB_PARTIAL_RESPONSE;
|
|
|
|
if (buffer == NULL || *buffer == NULL)
|
|
{
|
|
return SHOWDB_FATAL_ERROR;
|
|
}
|
|
|
|
/** TODO: Don't make the buffer contiguous but process it as a buffer chain */
|
|
*buffer = gwbuf_make_contiguous(*buffer);
|
|
buf = modutil_get_complete_packets(buffer);
|
|
|
|
if (buf == NULL)
|
|
{
|
|
return SHOWDB_PARTIAL_RESPONSE;
|
|
}
|
|
int n_eof = 0;
|
|
|
|
ptr = (unsigned char*) buf->start;
|
|
|
|
if (PTR_IS_ERR(ptr))
|
|
{
|
|
MXS_INFO("SHOW DATABASES returned an error.");
|
|
gwbuf_free(buf);
|
|
return SHOWDB_FATAL_ERROR;
|
|
}
|
|
|
|
if (n_eof == 0)
|
|
{
|
|
/** Skip column definitions */
|
|
while (ptr < (unsigned char*) buf->end && !PTR_IS_EOF(ptr))
|
|
{
|
|
ptr += gw_mysql_get_byte3(ptr) + 4;
|
|
}
|
|
|
|
if (ptr >= (unsigned char*) buf->end)
|
|
{
|
|
MXS_INFO("Malformed packet for SHOW DATABASES.");
|
|
*buffer = gwbuf_append(buf, *buffer);
|
|
return SHOWDB_FATAL_ERROR;
|
|
}
|
|
|
|
n_eof++;
|
|
/** Skip first EOF packet */
|
|
ptr += gw_mysql_get_byte3(ptr) + 4;
|
|
}
|
|
|
|
while (ptr < (unsigned char*) buf->end && !PTR_IS_EOF(ptr))
|
|
{
|
|
int payloadlen = gw_mysql_get_byte3(ptr);
|
|
int packetlen = payloadlen + 4;
|
|
char* data = get_lenenc_str(ptr + 4);
|
|
|
|
if (data)
|
|
{
|
|
if (m_shard.add_location(data, target))
|
|
{
|
|
MXS_INFO("<%s, %s>", target->unique_name, data);
|
|
}
|
|
else
|
|
{
|
|
if (!ignore_duplicate_database(data))
|
|
{
|
|
duplicate_found = true;
|
|
SERVER *duplicate = m_shard.get_location(data);
|
|
|
|
MXS_ERROR("Database '%s' found on servers '%s' and '%s' for user %s@%s.",
|
|
data, target->unique_name, duplicate->unique_name,
|
|
m_client->user, m_client->remote);
|
|
}
|
|
else if (m_config->preferred_server == target)
|
|
{
|
|
/** In conflict situations, use the preferred server */
|
|
MXS_INFO("Forcing location of '%s' from '%s' to '%s'",
|
|
data, m_shard.get_location(data)->unique_name,
|
|
target->unique_name);
|
|
m_shard.replace_location(data, target);
|
|
}
|
|
}
|
|
MXS_FREE(data);
|
|
}
|
|
ptr += packetlen;
|
|
}
|
|
|
|
if (ptr < (unsigned char*) buf->end && PTR_IS_EOF(ptr) && n_eof == 1)
|
|
{
|
|
n_eof++;
|
|
MXS_INFO("SHOW DATABASES fully received from %s.",
|
|
bref->backend()->server->unique_name);
|
|
}
|
|
else
|
|
{
|
|
MXS_INFO("SHOW DATABASES partially received from %s.",
|
|
bref->backend()->server->unique_name);
|
|
}
|
|
|
|
gwbuf_free(buf);
|
|
|
|
if (duplicate_found)
|
|
{
|
|
rval = SHOWDB_DUPLICATE_DATABASES;
|
|
}
|
|
else if (n_eof == 2)
|
|
{
|
|
rval = SHOWDB_FULL_RESPONSE;
|
|
}
|
|
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* Initiate the generation of the database hash table by sending a
|
|
* SHOW DATABASES query to each valid backend server. This sets the session
|
|
* into the mapping state where it queues further queries until all the database
|
|
* servers have returned a result.
|
|
* @param inst Router instance
|
|
* @param session Router client session
|
|
* @return 1 if all writes to backends were succesful and 0 if one or more errors occurred
|
|
*/
|
|
void SchemaRouterSession::query_databases()
|
|
{
|
|
|
|
for (SSRBackendList::iterator it = m_backends.begin(); it != m_backends.end(); it++)
|
|
{
|
|
(*it)->set_mapped(false);
|
|
}
|
|
|
|
m_state |= INIT_MAPPING;
|
|
m_state &= ~INIT_UNINT;
|
|
|
|
GWBUF *buffer = modutil_create_query("SHOW DATABASES");
|
|
gwbuf_set_type(buffer, GWBUF_TYPE_COLLECT_RESULT);
|
|
|
|
for (SSRBackendList::iterator it = m_backends.begin(); it != m_backends.end(); it++)
|
|
{
|
|
if ((*it)->in_use() && !(*it)->is_closed() &
|
|
SERVER_IS_RUNNING((*it)->backend()->server))
|
|
{
|
|
GWBUF* clone = gwbuf_clone(buffer);
|
|
MXS_ABORT_IF_NULL(clone);
|
|
|
|
if (!(*it)->write(clone))
|
|
{
|
|
MXS_ERROR("Failed to write SHOW DATABASES to '%s'",
|
|
(*it)->backend()->server->unique_name);
|
|
}
|
|
}
|
|
}
|
|
gwbuf_free(buffer);
|
|
}
|
|
|
|
/**
|
|
* Check the hashtable for the right backend for this query.
|
|
* @param router Router instance
|
|
* @param client Client router session
|
|
* @param buffer Query to inspect
|
|
* @return Name of the backend or NULL if the query contains no known databases.
|
|
*/
|
|
SERVER* SchemaRouterSession::get_shard_target(GWBUF* buffer, uint32_t qtype)
|
|
{
|
|
SERVER *rval = NULL;
|
|
bool has_dbs = false; /**If the query targets any database other than the current one*/
|
|
|
|
if (mxs_mysql_get_command(buffer) == MXS_COM_QUERY)
|
|
{
|
|
bool uses_current_database = false;
|
|
int n_tables = 0;
|
|
char** tables = qc_get_table_names(buffer, &n_tables, true);
|
|
|
|
for (int i = 0; i < n_tables; i++)
|
|
{
|
|
if (strchr(tables[i], '.') == NULL)
|
|
{
|
|
uses_current_database = true;
|
|
}
|
|
|
|
MXS_FREE(tables[i]);
|
|
}
|
|
|
|
MXS_FREE(tables);
|
|
|
|
if (uses_current_database)
|
|
{
|
|
MXS_INFO("Query uses current database");
|
|
return m_shard.get_location(m_current_db);
|
|
}
|
|
|
|
int n_databases = 0;
|
|
char** databases = qc_get_database_names(buffer, &n_databases);
|
|
|
|
for (int i = 0; i < n_databases; i++)
|
|
{
|
|
if (strcasecmp(databases[i], "information_schema") == 0 && rval == NULL)
|
|
{
|
|
has_dbs = false;
|
|
}
|
|
else
|
|
{
|
|
SERVER* target = m_shard.get_location(databases[i]);
|
|
|
|
if (target)
|
|
{
|
|
if (rval && target != rval)
|
|
{
|
|
MXS_ERROR("Query targets databases on servers '%s' and '%s'. "
|
|
"Cross database queries across servers are not supported.",
|
|
rval->unique_name, target->unique_name);
|
|
}
|
|
else if (rval == NULL)
|
|
{
|
|
rval = target;
|
|
has_dbs = true;
|
|
MXS_INFO("Query targets database '%s' on server '%s'",
|
|
databases[i], rval->unique_name);
|
|
}
|
|
}
|
|
}
|
|
|
|
MXS_FREE(databases[i]);
|
|
}
|
|
|
|
MXS_FREE(databases);
|
|
}
|
|
|
|
/* Check if the query is a show tables query with a specific database */
|
|
if (qc_query_is_type(qtype, QUERY_TYPE_SHOW_TABLES))
|
|
{
|
|
char *query = modutil_get_SQL(buffer);
|
|
char *tmp;
|
|
|
|
if ((tmp = strcasestr(query, "from")))
|
|
{
|
|
const char *delim = "` \n\t;";
|
|
char *saved, *tok = strtok_r(tmp, delim, &saved);
|
|
tok = strtok_r(NULL, delim, &saved);
|
|
|
|
if (tok)
|
|
{
|
|
rval = m_shard.get_location(tok);
|
|
|
|
if (rval)
|
|
{
|
|
MXS_INFO("SHOW TABLES with specific database '%s' on server '%s'", tok, tmp);
|
|
}
|
|
}
|
|
}
|
|
MXS_FREE(query);
|
|
|
|
if (rval == NULL)
|
|
{
|
|
rval = m_shard.get_location(m_current_db);
|
|
|
|
if (rval)
|
|
{
|
|
MXS_INFO("SHOW TABLES query, current database '%s' on server '%s'",
|
|
m_current_db.c_str(), rval->unique_name);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
has_dbs = true;
|
|
}
|
|
}
|
|
else if (buffer->hint && buffer->hint->type == HINT_ROUTE_TO_NAMED_SERVER)
|
|
{
|
|
for (SSRBackendList::iterator it = m_backends.begin(); it != m_backends.end(); it++)
|
|
{
|
|
char *srvnm = (*it)->backend()->server->unique_name;
|
|
|
|
if (strcmp(srvnm, (char*)buffer->hint->data) == 0)
|
|
{
|
|
rval = (*it)->backend()->server;
|
|
MXS_INFO("Routing hint found (%s)", rval->unique_name);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (rval == NULL && !has_dbs && m_current_db.length())
|
|
{
|
|
/**
|
|
* If the target name has not been found and the session has an
|
|
* active database, set is as the target
|
|
*/
|
|
|
|
rval = m_shard.get_location(m_current_db);
|
|
|
|
if (rval)
|
|
{
|
|
MXS_INFO("Using active database '%s' on '%s'",
|
|
m_current_db.c_str(), rval->unique_name);
|
|
}
|
|
}
|
|
|
|
return rval;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* Similarly, if max replication lag is specified, skip backends which lag too
|
|
* much.
|
|
*
|
|
* @param p_dcb Address of the pointer to the resulting DCB
|
|
* @param name Name of the backend which is primarily searched. May be NULL.
|
|
*
|
|
* @return True if proper DCB was found, false otherwise.
|
|
*/
|
|
bool SchemaRouterSession::get_shard_dcb(DCB** p_dcb, char* name)
|
|
{
|
|
bool succp = false;
|
|
ss_dassert(p_dcb != NULL && *(p_dcb) == NULL);
|
|
|
|
for (SSRBackendList::iterator it = m_backends.begin(); it != m_backends.end(); it++)
|
|
{
|
|
SERVER_REF* b = (*it)->backend();
|
|
/**
|
|
* To become chosen:
|
|
* backend must be in use, name must match, and
|
|
* the backend state must be RUNNING
|
|
*/
|
|
if ((*it)->in_use() &&
|
|
(strncasecmp(name, b->server->unique_name, PATH_MAX) == 0) &&
|
|
SERVER_IS_RUNNING(b->server))
|
|
{
|
|
*p_dcb = (*it)->dcb();
|
|
succp = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
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.
|
|
*/
|
|
enum route_target get_shard_route_target(uint32_t qtype)
|
|
{
|
|
enum route_target target = TARGET_UNDEFINED;
|
|
|
|
/**
|
|
* These queries are not affected by hints
|
|
*/
|
|
if (qc_query_is_type(qtype, QUERY_TYPE_SESSION_WRITE) ||
|
|
qc_query_is_type(qtype, QUERY_TYPE_GSYSVAR_WRITE) ||
|
|
qc_query_is_type(qtype, QUERY_TYPE_USERVAR_WRITE) ||
|
|
qc_query_is_type(qtype, QUERY_TYPE_PREPARE_STMT) ||
|
|
qc_query_is_type(qtype, QUERY_TYPE_PREPARE_NAMED_STMT) ||
|
|
qc_query_is_type(qtype, QUERY_TYPE_ENABLE_AUTOCOMMIT) ||
|
|
qc_query_is_type(qtype, QUERY_TYPE_DISABLE_AUTOCOMMIT))
|
|
{
|
|
/** hints don't affect on routing */
|
|
target = TARGET_ALL;
|
|
}
|
|
else if (qc_query_is_type(qtype, QUERY_TYPE_SYSVAR_READ) ||
|
|
qc_query_is_type(qtype, QUERY_TYPE_GSYSVAR_READ))
|
|
{
|
|
target = TARGET_ANY;
|
|
}
|
|
|
|
return target;
|
|
}
|
|
|
|
/**
|
|
* Callback for the database list streaming.
|
|
* @param rset Result set which is being processed
|
|
* @param data Pointer to struct string_array containing the database names
|
|
* @return New resultset row or NULL if no more data is available. If memory allocation
|
|
* failed, NULL is returned.
|
|
*/
|
|
RESULT_ROW *result_set_cb(struct resultset * rset, void *data)
|
|
{
|
|
RESULT_ROW *row = resultset_make_row(rset);
|
|
ServerMap* arr = (ServerMap*) data;
|
|
|
|
if (row)
|
|
{
|
|
if (arr->size() > 0 && resultset_row_set(row, 0, arr->begin()->first.c_str()))
|
|
{
|
|
arr->erase(arr->begin());
|
|
}
|
|
else
|
|
{
|
|
resultset_free_row(row);
|
|
row = NULL;
|
|
}
|
|
}
|
|
|
|
return row;
|
|
}
|
|
|
|
/**
|
|
* Generates a custom SHOW DATABASES result set from all the databases in the
|
|
* hashtable. Only backend servers that are up and in a proper state are listed
|
|
* in it.
|
|
* @param router Router instance
|
|
* @param client Router client session
|
|
* @return True if the sending of the database list was successful, otherwise false
|
|
*/
|
|
bool SchemaRouterSession::send_databases()
|
|
{
|
|
bool rval = false;
|
|
|
|
ServerMap dblist;
|
|
m_shard.get_content(dblist);
|
|
|
|
RESULTSET* resultset = resultset_create(result_set_cb, &dblist);
|
|
|
|
if (resultset_add_column(resultset, "Database", MYSQL_DATABASE_MAXLEN,
|
|
COL_TYPE_VARCHAR))
|
|
{
|
|
resultset_stream_mysql(resultset, m_client);
|
|
rval = true;
|
|
}
|
|
resultset_free(resultset);
|
|
|
|
return rval;
|
|
}
|
|
|
|
} |